hecks 1.3.0 → 1.5.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 (373) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +72 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +83 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
  7. data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
  9. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  10. data/lib/hecks/adapters/driven/heki.rb +106 -9
  11. data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
  12. data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
  13. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  15. data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
  16. data/lib/hecks/adapters/driven/lambda.rb +82 -35
  17. data/lib/hecks/adapters/driven/local_storage.rb +83 -10
  18. data/lib/hecks/adapters/driven/memory.rb +205 -9
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  24. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  25. data/lib/hecks/adapters/driven/prism.rb +50 -11
  26. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  31. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  32. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  33. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  34. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  35. data/lib/hecks/adapters/driven.rb +6 -4
  36. data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
  37. data/lib/hecks/behaviors/dsl.rb +60 -2
  38. data/lib/hecks/behaviors/expectations.rb +190 -29
  39. data/lib/hecks/behaviors/ir.rb +12 -1
  40. data/lib/hecks/behaviors/rspec.rb +9 -1
  41. data/lib/hecks/behaviors/runner.rb +21 -2
  42. data/lib/hecks/behaviors.rb +9 -1
  43. data/lib/hecks/bluebook/aggregate.rb +43 -13
  44. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
  45. data/lib/hecks/bluebook/assembly/build.rb +26 -5
  46. data/lib/hecks/bluebook/assembly/contract.rb +98 -23
  47. data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
  48. data/lib/hecks/bluebook/assembly/marks.rb +159 -30
  49. data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
  50. data/lib/hecks/bluebook/assembly.rb +32 -14
  51. data/lib/hecks/bluebook/attribute.rb +26 -12
  52. data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
  53. data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
  54. data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
  55. data/lib/hecks/bluebook/behaviour/command.rb +55 -25
  56. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
  57. data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
  58. data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
  59. data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
  60. data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
  61. data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
  62. data/lib/hecks/bluebook/behaviour/query.rb +6 -1
  63. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
  64. data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
  65. data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
  66. data/lib/hecks/bluebook/capabilities.rb +27 -0
  67. data/lib/hecks/bluebook/chapter.rb +51 -9
  68. data/lib/hecks/bluebook/command.rb +62 -17
  69. data/lib/hecks/bluebook/domain_port.rb +34 -9
  70. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  71. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  72. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
  73. data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
  74. data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
  75. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
  76. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
  77. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  78. data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
  79. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  80. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  81. data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
  82. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  83. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
  84. data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
  85. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  86. data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
  87. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  88. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  89. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
  90. data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
  91. data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
  92. data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
  93. data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
  94. data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
  95. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  96. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  97. data/lib/hecks/bluebook/entity.rb +40 -11
  98. data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
  99. data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
  100. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
  101. data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
  102. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
  103. data/lib/hecks/bluebook/expression/resolver.rb +369 -128
  104. data/lib/hecks/bluebook/hexagon.rb +35 -1
  105. data/lib/hecks/bluebook/lifecycle.rb +12 -1
  106. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
  107. data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
  108. data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
  109. data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
  110. data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
  111. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
  112. data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
  113. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
  114. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
  115. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
  116. data/lib/hecks/bluebook/meta_validator.rb +235 -139
  117. data/lib/hecks/bluebook/model_check.rb +434 -104
  118. data/lib/hecks/bluebook/pattern_subset.rb +32 -10
  119. data/lib/hecks/bluebook/policy.rb +15 -13
  120. data/lib/hecks/bluebook/process_manager.rb +27 -14
  121. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  122. data/lib/hecks/bluebook/project_loader.rb +40 -0
  123. data/lib/hecks/bluebook/project_register.rb +50 -6
  124. data/lib/hecks/bluebook/query.rb +31 -4
  125. data/lib/hecks/bluebook/read_model.rb +35 -15
  126. data/lib/hecks/bluebook/reference.rb +26 -13
  127. data/lib/hecks/bluebook/smoke_test.rb +46 -23
  128. data/lib/hecks/bluebook/synthesizer.rb +46 -12
  129. data/lib/hecks/bluebook/translation.rb +34 -5
  130. data/lib/hecks/bluebook/value_object.rb +29 -11
  131. data/lib/hecks/bluebook.rb +5 -6
  132. data/lib/hecks/codemod.rb +138 -50
  133. data/lib/hecks/construct.rb +21 -7
  134. data/lib/hecks/corpus.rb +438 -0
  135. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  136. data/lib/hecks/doc/reference.rb +200 -31
  137. data/lib/hecks/embryonaut_bluebook.rb +38 -15
  138. data/lib/hecks/facade/cli_door.rb +69 -10
  139. data/lib/hecks/facade/cli_runner.rb +105 -24
  140. data/lib/hecks/facade/command_request.rb +23 -0
  141. data/lib/hecks/facade/handle.rb +155 -35
  142. data/lib/hecks/facade/json_door.rb +106 -25
  143. data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
  144. data/lib/hecks/facade/surface/chapter.rb +26 -17
  145. data/lib/hecks/facade/surface.rb +16 -3
  146. data/lib/hecks/facade.rb +15 -4
  147. data/lib/hecks/forms/app.rb +46 -30
  148. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  149. data/lib/hecks/forms/field_renderer.rb +142 -6
  150. data/lib/hecks/forms/field_shape.rb +183 -19
  151. data/lib/hecks/forms/html.rb +51 -7
  152. data/lib/hecks/forms/index_renderer.rb +14 -2
  153. data/lib/hecks/forms/page.rb +14 -0
  154. data/lib/hecks/forms/params.rb +120 -23
  155. data/lib/hecks/forms/port_argument.rb +14 -2
  156. data/lib/hecks/forms/query_form_renderer.rb +65 -2
  157. data/lib/hecks/forms/record_renderer.rb +60 -2
  158. data/lib/hecks/forms/record_table.rb +28 -1
  159. data/lib/hecks/forms/reference_options.rb +24 -0
  160. data/lib/hecks/forms/value_object_shape.rb +13 -3
  161. data/lib/hecks/forms.rb +24 -4
  162. data/lib/hecks/fqn.rb +59 -1
  163. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  164. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  165. data/lib/hecks/framework/oidc.json +15 -0
  166. data/lib/hecks/framework.rb +79 -25
  167. data/lib/hecks/freezer.rb +27 -11
  168. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
  169. data/lib/hecks/fuzzing/combination_miner.rb +178 -0
  170. data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
  171. data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
  172. data/lib/hecks/fuzzing/differential.rb +192 -0
  173. data/lib/hecks/fuzzing/domain_generator.rb +871 -0
  174. data/lib/hecks/fuzzing/era_boundary.rb +68 -19
  175. data/lib/hecks/fuzzing/form_census.rb +121 -20
  176. data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
  177. data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
  178. data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
  179. data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
  180. data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
  181. data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
  182. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
  183. data/lib/hecks/fuzzing/properties/guards.rb +86 -42
  184. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
  185. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
  186. data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
  187. data/lib/hecks/fuzzing/properties/querying.rb +82 -28
  188. data/lib/hecks/fuzzing/properties.rb +84 -51
  189. data/lib/hecks/fuzzing/qa_settings.rb +164 -0
  190. data/lib/hecks/fuzzing/replay.rb +241 -133
  191. data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
  192. data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
  193. data/lib/hecks/fuzzing/self_consistency.rb +315 -137
  194. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
  195. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  196. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  197. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  198. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
  199. data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
  200. data/lib/hecks/fuzzing/shrinker.rb +309 -0
  201. data/lib/hecks/fuzzing/structural_skips.rb +37 -130
  202. data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
  203. data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
  204. data/lib/hecks/fuzzing/value_generator.rb +110 -22
  205. data/lib/hecks/fuzzing.rb +5 -0
  206. data/lib/hecks/grammar/evolve.rb +188 -12
  207. data/lib/hecks/grammar.rb +53 -7
  208. data/lib/hecks/ir.rb +51 -20
  209. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  210. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  211. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  212. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  213. data/lib/hecks/language/oidc.json +5 -0
  214. data/lib/hecks/literal.rb +41 -9
  215. data/lib/hecks/naming.rb +112 -31
  216. data/lib/hecks/ports/access_control.rb +53 -2
  217. data/lib/hecks/ports/agent/answers.rb +83 -6
  218. data/lib/hecks/ports/agent.rb +119 -35
  219. data/lib/hecks/ports/authentication.rb +44 -4
  220. data/lib/hecks/ports/authorization.rb +50 -11
  221. data/lib/hecks/ports/clock.rb +42 -23
  222. data/lib/hecks/ports/extraction.rb +16 -0
  223. data/lib/hecks/ports/identity_assignment.rb +23 -2
  224. data/lib/hecks/ports/identity_generation.rb +17 -3
  225. data/lib/hecks/ports/identity_resolution.rb +17 -1
  226. data/lib/hecks/ports/key_vault.port +6 -0
  227. data/lib/hecks/ports/key_vault.rb +58 -0
  228. data/lib/hecks/ports/loading.rb +4 -0
  229. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  230. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  231. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  232. data/lib/hecks/ports/persistence/execution.rb +4 -0
  233. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  234. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  235. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  236. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  237. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  238. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  239. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  250. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  251. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  252. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
  253. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  254. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  255. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  256. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  257. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  258. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
  259. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  260. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  261. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  262. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
  263. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
  264. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  265. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
  266. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  267. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  268. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  269. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  270. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  271. data/lib/hecks/ports/persistence.rb +36 -1
  272. data/lib/hecks/ports/projection.rb +61 -7
  273. data/lib/hecks/ports/query/in_memory.rb +41 -8
  274. data/lib/hecks/ports/query/ordering.rb +21 -6
  275. data/lib/hecks/ports/query.rb +35 -0
  276. data/lib/hecks/ports.rb +1 -0
  277. data/lib/hecks/projections/bootstrap_table.rb +147 -0
  278. data/lib/hecks/projections/diagrams.rb +315 -79
  279. data/lib/hecks/projections/glossary/html.rb +91 -3
  280. data/lib/hecks/projections/glossary/markdown.rb +50 -3
  281. data/lib/hecks/projections/glossary/mermaid.rb +49 -1
  282. data/lib/hecks/projections/glossary/sections.rb +1 -1
  283. data/lib/hecks/projections/glossary/sentences.rb +110 -5
  284. data/lib/hecks/projections/glossary.rb +173 -31
  285. data/lib/hecks/projections/ir.rb +1 -1
  286. data/lib/hecks/projections/model/deviations.rb +62 -17
  287. data/lib/hecks/projections/model.rb +75 -21
  288. data/lib/hecks/projections/oidc.rb +45 -8
  289. data/lib/hecks/projections/parser_table.rb +53 -12
  290. data/lib/hecks/projections/reference.rb +15 -3
  291. data/lib/hecks/projections/rust_vocabulary.rb +646 -0
  292. data/lib/hecks/projections/shape.rb +10 -2
  293. data/lib/hecks/projections/statements.rb +69 -22
  294. data/lib/hecks/projections/vocabulary.rb +26 -9
  295. data/lib/hecks/projections.rb +5 -3
  296. data/lib/hecks/projector/cli_projector.rb +245 -37
  297. data/lib/hecks/projector/docs_projector.rb +154 -28
  298. data/lib/hecks/projector/exporter.rb +104 -29
  299. data/lib/hecks/projector/ir_projector.rb +7 -1
  300. data/lib/hecks/projector/narrate_projector.rb +144 -23
  301. data/lib/hecks/projector/target.rb +42 -18
  302. data/lib/hecks/projector.rb +86 -18
  303. data/lib/hecks/query_ir.rb +94 -47
  304. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  305. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  306. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  307. data/lib/hecks/query_specification/common/null_policy.rb +61 -15
  308. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  309. data/lib/hecks/query_specification/common/options.rb +25 -0
  310. data/lib/hecks/query_specification/field_path.rb +69 -15
  311. data/lib/hecks/query_specification/hop_path.rb +57 -20
  312. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  313. data/lib/hecks/rendering.rb +9 -3
  314. data/lib/hecks/router/namespace_installer.rb +16 -3
  315. data/lib/hecks/router.rb +56 -1
  316. data/lib/hecks/runtime/aggregate_lock.rb +20 -11
  317. data/lib/hecks/runtime/boot_gates.rb +21 -3
  318. data/lib/hecks/runtime/caller.rb +40 -8
  319. data/lib/hecks/runtime/capability_graph.rb +13 -2
  320. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
  321. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
  322. data/lib/hecks/runtime/command_interpreter.rb +195 -113
  323. data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
  324. data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
  325. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  326. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  327. data/lib/hecks/runtime/command_rules/references.rb +145 -55
  328. data/lib/hecks/runtime/command_rules.rb +3 -0
  329. data/lib/hecks/runtime/dependency_planning.rb +56 -11
  330. data/lib/hecks/runtime/dispatcher.rb +263 -92
  331. data/lib/hecks/runtime/entity_element.rb +304 -67
  332. data/lib/hecks/runtime/entity_interpreter.rb +149 -88
  333. data/lib/hecks/runtime/errors.rb +37 -23
  334. data/lib/hecks/runtime/event.rb +15 -10
  335. data/lib/hecks/runtime/identity.rb +91 -23
  336. data/lib/hecks/runtime/instance.rb +103 -18
  337. data/lib/hecks/runtime/interpreting.rb +24 -16
  338. data/lib/hecks/runtime/invocation.rb +358 -0
  339. data/lib/hecks/runtime/loader.rb +105 -19
  340. data/lib/hecks/runtime/outbox.rb +164 -26
  341. data/lib/hecks/runtime/policy_interpreter.rb +73 -60
  342. data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
  343. data/lib/hecks/runtime/query_interpreter.rb +93 -74
  344. data/lib/hecks/runtime/reaction_invocation.rb +73 -28
  345. data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
  346. data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
  347. data/lib/hecks/runtime/reference_hop.rb +48 -6
  348. data/lib/hecks/runtime/refusal_wording.rb +142 -115
  349. data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
  350. data/lib/hecks/runtime/registry/verification.rb +153 -28
  351. data/lib/hecks/runtime/registry.rb +202 -32
  352. data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
  353. data/lib/hecks/runtime/routing.rb +36 -154
  354. data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
  355. data/lib/hecks/runtime/saga_interpreter.rb +90 -76
  356. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  357. data/lib/hecks/runtime/tenant_check.rb +33 -13
  358. data/lib/hecks/runtime/tenant_scope.rb +23 -5
  359. data/lib/hecks/runtime/value/admission.rb +75 -30
  360. data/lib/hecks/runtime/value/coercion.rb +555 -142
  361. data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
  362. data/lib/hecks/runtime/value.rb +71 -21
  363. data/lib/hecks/runtime.rb +39 -7
  364. data/lib/hecks/storehouse.rb +368 -72
  365. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  366. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  367. data/lib/hecks/version.rb +3 -3
  368. data/lib/hecks/vocabulary.rb +205 -4
  369. data/lib/hecks.rb +91 -11
  370. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
  371. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
  372. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
  373. metadata +28 -2
@@ -8,20 +8,22 @@ require_relative "projector"
8
8
  require_relative "runtime/errors"
9
9
 
10
10
  module Hecks
11
- # THE BUS, NOT A DOOR — `docs/hecks-survey-what-we-wish-we-had.md` and
11
+ # **The bus, not a door** — `docs/hecks-survey-what-we-wish-we-had.md` and
12
12
  # `docs/future-features.md` both name the sibling project's own
13
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
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
16
  # dispatch/query/state/... surface below is a pure function of a
17
17
  # `Runtime::Dispatcher` plus plain Ruby arguments, no IO, no protocol
18
- # awareness. `bin/hecks_mcp_door` is ONE projection of it — MCP over
18
+ # awareness. `bin/hecks_mcp_door` is one projection of it — MCP over
19
19
  # stdio — not the whole thing; a plain CLI door, an HTTP door, a second
20
20
  # transport of any shape, would sit beside it on the exact same bus,
21
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.)
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
25
27
  #
26
28
  # catalog — what aggregates a domain declares, and what each can do
27
29
  # describe — one aggregate's full command/query/refusal contract
@@ -30,21 +32,23 @@ module Hecks
30
32
  # dispatch — issue a command (dry_run: preview, steps: batch)
31
33
  # query — ask a question
32
34
  # 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
+ # events — what happened, with payloads, to one record — not just its
36
+ # current state; events this bus witnessed, sourced from
35
37
  # its own audit log, not a full event-sourcing replay
36
38
  # history — the full append-only journal, not just current state
37
39
  # behaviors — run a domain's hand-curated `.behaviors` examples
38
40
  # follow — tail this bus's own dispatch/query/state audit log
39
41
  #
40
- # `dispatch`/`query`/`state` EACH TAKE A `summary` — the survey's "every
42
+ # ## Auditing and caller identity
43
+ #
44
+ # `dispatch`/`query`/`state` each take a `summary` — the survey's "every
41
45
  # audit row carries human intent for free" — and `dispatch`/`query`
42
46
  # additionally take an optional `source:` (`SOURCE_TAGS`, the survey's
43
- # `SourceTag`: who is calling) AND an optional `role:`/`actor_id:` —
47
+ # `SourceTag`: who is calling) and an optional `role:`/`actor_id:` —
44
48
  # a real caller identity, bound for the call's duration via `Hecks.
45
49
  # as_caller`, checked against a `role`-gated command's own declared
46
50
  # role (`CommandRules::Authorization`) rather than merely documented by
47
- # `describe`. `dispatch` REQUIRES it for any command that declares a
51
+ # `describe`. `dispatch` requires it for any command that declares a
48
52
  # role — `require_caller_for_role_gated!` refuses an unbound dispatch
49
53
  # against one rather than silently running it unchecked; `query`'s own
50
54
  # authorization runs on a separate mechanism (`Runtime::TenantScope`)
@@ -54,21 +58,25 @@ module Hecks
54
58
  # header for what that does and does not guarantee. Every call through
55
59
  # those three, plus a dry run, is appended to a per-domain JSONL audit
56
60
  # log (`record!`) `follow` tails back — a record of what the caller
57
- # SAID it was, not independently verified identity. `catalog`/
61
+ # said it was, not independently verified identity. `catalog`/
58
62
  # `describe`/`validate`/`domains`/`history`/`behaviors`/`events` need
59
63
  # neither — they change nothing and commit nothing to any log.
60
64
  #
61
- # A CALLER HANDS IN AN ALREADY-BOOTED `runtime`, the same division of
65
+ # ## Booting is the caller's job
66
+ #
67
+ # A caller hands in an already-booted `runtime`, the same division of
62
68
  # labor `Facade::CliRunner` already keeps against `bin/run`: booting a
63
69
  # domain from a path is IO the calling `bin/` script owns, this stays a
64
70
  # pure function of a `Runtime::Dispatcher` plus plain Ruby arguments —
65
71
  # no different from `CliRunner.call(runtime:, argv:)` itself.
66
72
  # `validate` and `domains` are the two exceptions: `validate`'s whole
67
73
  # 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
74
+ # takes the domain path instead and boots it; `domains` has no
69
75
  # domain to be handed one of yet, that's what it's answering.
70
76
  #
71
- # NO NEW VOCABULARY otherwise. Every method here composes doors that
77
+ # ## Built from existing doors
78
+ #
79
+ # No new vocabulary otherwise. Every method here composes doors that
72
80
  # already exist — `Projector.call(:cli, ...)` for verb/question alias
73
81
  # resolution (the identical table `CliRunner` itself resolves against),
74
82
  # `Projector.call(:docs, ...)` for `describe`, `Facade::JsonDoor` for
@@ -82,21 +90,21 @@ module Hecks
82
90
  module Storehouse
83
91
  module_function
84
92
 
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
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
87
95
  # that omits it gets `source: nil` recorded, honestly, rather than a
88
96
  # guessed default.
89
97
  SOURCE_TAGS = %w[process-manager operator hook sidequest-agent cascade daemon].freeze
90
98
 
91
- # THE BUS'S OWN AUDIT TRAIL — a JSONL file per domain, one line per
99
+ # **The bus's own audit trail** — a JSONL file per domain, one line per
92
100
  # `dispatch`/`query`/`state`/dry-run call, `follow` tails it back.
93
- # `tmp/`, not the domain's own directory: this is the BUS's record
101
+ # `tmp/`, not the domain's own directory: this is the bus's record
94
102
  # of what was asked of it, not part of the domain's own persisted
95
103
  # state, and `tmp/` is already gitignored for exactly this kind of
96
104
  # local, disposable-but-useful-while-it-lasts file.
97
105
  LOG_ROOT = File.expand_path("../../tmp/storehouse", __dir__)
98
106
 
99
- # THE ROOT EVERY `domain:`/`under:` MUST RESOLVE UNDER — the project
107
+ # The root every `domain:`/`under:` must resolve under — the project
100
108
  # directory by default, `HECKS_STOREHOUSE_ROOT` to widen or move it.
101
109
  # `Hecks.boot` `Kernel.load`s the `.hecksagon`/`.bluebook`/`.world`
102
110
  # files a domain path resolves to, and those are Ruby, not a data
@@ -108,11 +116,19 @@ module Hecks
108
116
  # project tree.
109
117
  BOOT_ROOT = File.expand_path(ENV["HECKS_STOREHOUSE_ROOT"] || File.expand_path("../..", __dir__))
110
118
 
111
- # REFUSED, NOT SILENTLY CLAMPED — a path outside `BOOT_ROOT` is either
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
112
122
  # an honest mistake (a relative path typed against the wrong cwd) or
113
123
  # the exact thing this check exists to catch, and both deserve the
114
124
  # same clear refusal rather than a silent rewrite to something the
115
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`
116
132
  def confine!(path, label)
117
133
  resolved = File.expand_path(path.to_s, BOOT_ROOT)
118
134
  return resolved if resolved == BOOT_ROOT || resolved.start_with?("#{BOOT_ROOT}#{File::SEPARATOR}")
@@ -124,25 +140,43 @@ module Hecks
124
140
 
125
141
  # ── shared resolution helpers ────────────────────────────────────
126
142
 
127
- # THE ONE BLUEBOOK A DOMAIN DIRECTORY BOOTS. Every `bin/*` script that
143
+ # The one bluebook a domain directory boots. Every `bin/*` script that
128
144
  # projects a whole-domain CLI or doc set makes this same assumption
129
145
  # (`Facade::CliRunner#call`'s own `bluebook = runtime.registry.
130
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
131
152
  def bluebook_for(runtime)
132
153
  runtime.registry.bluebooks.values.first or
133
154
  raise Runtime::NotFound, "this boot loaded no bluebook"
134
155
  end
135
156
 
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`
136
163
  def aggregate_ir!(bluebook, name)
137
164
  bluebook.aggregate(name) or
138
165
  raise Runtime::NotFound, "#{bluebook.name} declares no aggregate named #{name.inspect} — " \
139
166
  "known: #{bluebook.aggregates.map(&:hecks_name).sort.join(', ')}"
140
167
  end
141
168
 
142
- # THE SAME ALIAS TABLE `CliRunner` RESOLVES A TYPED WORD AGAINST — a
169
+ # The same alias table `CliRunner` resolves a typed word against — a
143
170
  # short name when it's unambiguous, the qualified `Aggregate.Verb`
144
171
  # form always. Shared here so `dispatch` and `query` (and their error
145
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
146
180
  def resolve!(cli, name, asking:)
147
181
  pool = asking ? cli[:questions] : cli[:verbs]
148
182
  key = cli[:names][asking ? :question : :command][name]
@@ -153,6 +187,11 @@ module Hecks
153
187
  raise Runtime::NotFound, "no such #{asking ? 'query' : 'command'}: #{name.inspect} — known: #{known}"
154
188
  end
155
189
 
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
156
195
  def require_summary!(summary)
157
196
  return unless summary.nil? || summary.to_s.strip.empty?
158
197
 
@@ -160,58 +199,80 @@ module Hecks
160
199
  "a one-line summary: is required on dispatch/query/state — it is what makes an audit row legible later"
161
200
  end
162
201
 
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`
163
207
  def valid_source!(source)
164
208
  return if source.nil? || SOURCE_TAGS.include?(source.to_s)
165
209
 
166
210
  raise Runtime::TypeMismatch, "source: #{source.inspect} is not one of #{SOURCE_TAGS.join(', ')}"
167
211
  end
168
212
 
169
- # `actor_id` NAMES WHO, `role` NAMES WHAT THEY HOLD — `Hecks.
213
+ # `actor_id` names who, `role` names what they hold — `Hecks.
170
214
  # as_caller` requires the latter always, the former is additive
171
215
  # (`Runtime::Caller::Current`'s own shape). An `actor_id` with no
172
216
  # `role` would silently do nothing rather than bind a real caller,
173
217
  # which is worse than refusing: a caller who thinks they've
174
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`
175
224
  def valid_caller!(role, actor_id)
176
225
  return unless actor_id && role.nil?
177
226
 
178
227
  raise Runtime::TypeMismatch, "actor_id: requires role: too — a caller names WHO through WHICH role they hold"
179
228
  end
180
229
 
181
- # BOUND FOR THE DURATION OF ONE CALL, THEN GONE — `Hecks.as_caller`
230
+ # Bound for the duration of one call, then gone — `Hecks.as_caller`
182
231
  # is itself a `Thread.current`-scoped `ensure`-guarded block, so
183
232
  # nothing here needs its own cleanup. `role: nil` yields unbound —
184
233
  # for `query`, exactly as before: `CommandRules::Authorization#
185
- # refuse_role_mismatch` is OPT-IN on the domain side (`return unless
234
+ # refuse_role_mismatch` is opt-in on the domain side (`return unless
186
235
  # caller`), and query authorization runs on a wholly separate
187
236
  # mechanism (`authorize policy, tenant: :field`, checked against an
188
237
  # explicit `tenant:` argument — see `Runtime::TenantScope`), so
189
- # binding a caller around a query has no effect on it TODAY; it is
238
+ # binding a caller around a query has no effect on it today; it is
190
239
  # still accepted here, for symmetry and for the audit log, against
191
240
  # the day a read model does check `Caller.current`. For `dispatch`,
192
- # `require_caller_for_role_gated!` (below) now refuses BEFORE this
241
+ # `require_caller_for_role_gated!` (below) now refuses before this
193
242
  # is ever reached when the command declares a role and no caller is
194
243
  # bound — so an unbound `dispatch` here means either the command
195
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
196
251
  def with_caller(role, actor_id, &block)
197
252
  return block.call if role.nil?
198
253
 
199
254
  Hecks.as_caller(role: role, actor_id: actor_id, &block)
200
255
  end
201
256
 
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
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
204
259
  # (a live `Governance::RoleAssignment` lookup instead of a bare
205
260
  # string match), but changed nothing about a caller who binds no
206
261
  # role at all: `refuse_role_mismatch` `return`s immediately when
207
262
  # `Caller.current` is nil, so a bus caller who simply omits `role:`
208
263
  # sails past a role-gated command unchecked, not denied. That is a
209
- # property of THIS BUS choosing to dispatch unbound, not of the
264
+ # property of this bus choosing to dispatch unbound, not of the
210
265
  # domain rule — `bin/run`, the human CLI, has no such gap because a
211
266
  # human always dispatches through a real `Hecks.as_caller` binding
212
267
  # upstream of it. Refusing here, before `with_caller`/`dispatch` are
213
268
  # ever reached, makes the bus keep the same promise: a command whose
214
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
215
276
  def require_caller_for_role_gated!(spec, role)
216
277
  return unless spec[:role_gated] && role.nil?
217
278
 
@@ -220,17 +281,26 @@ module Hecks
220
281
  "(role:/actor_id:) is bound; dispatching it unbound is refused, not silently unchecked"
221
282
  end
222
283
 
223
- # `dry_run?` (Runtime::Dispatcher) understands only the OLD flat
284
+ # `dry_run?` (Runtime::Dispatcher) understands only the old flat
224
285
  # legacy args shape — no to:/with: envelope, `route:` never passed
225
286
  # (its own header explains why: built directly against
226
287
  # CommandInterpreter/EntityInterpreter's pre-envelope contract,
227
288
  # never updated because nothing else needed it to be — a real
228
289
  # record of history, not a defect this bus should paper over
229
290
  # silently). `Facade::CommandRequest`/`spec[:legacy_receiver]`
230
- # already know how to NAME that same flat shape for every receiver
291
+ # already know how to name that same flat shape for every receiver
231
292
  # kind (a bare id under one string key for :aggregate, a
232
293
  # {aggregate:, entity:} pair of keys for :entity) — this is the one
233
- # door back INTO it.
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
234
304
  def flatten_legacy(envelope, receiver, legacy_receiver)
235
305
  facts = envelope[:with] || {}
236
306
  return facts unless envelope.key?(:to)
@@ -247,14 +317,34 @@ module Hecks
247
317
 
248
318
  # ── the audit log `follow` reads back ───────────────────────────────
249
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`
250
325
  def log_path(domain_name)
251
326
  File.join(LOG_ROOT, "#{domain_name.to_s.gsub(/[^A-Za-z0-9_-]/, '_')}.jsonl")
252
327
  end
253
328
 
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`
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`
256
333
  # feature going dark, not a reason to refuse the dispatch/query/
257
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]
258
348
  def record!(domain_name, tool:, summary:, source:, outcome:, verb: nil, role: nil, actor_id: nil)
259
349
  return unless domain_name
260
350
 
@@ -269,12 +359,25 @@ module Hecks
269
359
 
270
360
  # ── the three that drive it ────────────────────────────────────────
271
361
 
272
- # `dry_run: true` ANSWERS A DIFFERENT QUESTION than a real dispatch
362
+ # `dry_run: true` answers a different question than a real dispatch
273
363
  # does — "would this succeed", not "here is what happened" — so a
274
- # domain refusal is the legitimate, complete ANSWER (`ok: true,
364
+ # domain refusal is the legitimate, complete answer (`ok: true,
275
365
  # would_succeed: false`), not a failed call. A malformed request
276
366
  # (unknown command, a bad args shape) is still a failed call
277
367
  # (`ok: false`) either way — it never reached the domain to be asked.
368
+ #
369
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
370
+ # @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
278
381
  def dispatch(runtime:, command:, summary:, args: {}, source: nil, dry_run: false, role: nil, actor_id: nil)
279
382
  bluebook = bluebook_for(runtime)
280
383
  tool = dry_run ? "dry_run" : "dispatch"
@@ -290,6 +393,24 @@ module Hecks
290
393
  outcome
291
394
  end
292
395
 
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
293
414
  def perform_dispatch(runtime, bluebook, command, summary, args, source, dry_run, role, actor_id)
294
415
  require_summary!(summary)
295
416
  valid_source!(source)
@@ -307,14 +428,30 @@ module Hecks
307
428
  result.merge(verb: spec[:verb])
308
429
  end
309
430
 
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)
310
439
  def real_dispatch(runtime, spec, envelope, summary)
311
- result = runtime.dispatch(spec[:verb], **envelope)
440
+ result = runtime.dispatch_flat(spec[:verb], envelope)
312
441
  ok(summary: summary,
313
442
  id: result.id,
314
443
  state: result.state.nil? ? nil : Facade::JsonDoor.materialize(result.state),
315
444
  events: result.events.map { |event| { name: event.name, payload: Facade::JsonDoor.materialize(event.payload) } })
316
445
  end
317
446
 
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
318
455
  def dry_run_outcome(runtime, spec, envelope, summary:)
319
456
  flat = flatten_legacy(envelope, spec[:receiver], spec[:legacy_receiver])
320
457
  runtime.dry_run?(spec[:verb], **flat)
@@ -323,8 +460,10 @@ module Hecks
323
460
  ok(summary: summary, would_succeed: false, error: e.message)
324
461
  end
325
462
 
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
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
328
467
  # account, then fund it) pays one round trip instead of N. Every step
329
468
  # goes through `dispatch` itself — same resolution, same audit log
330
469
  # line per step, same summary/source stamped on all of them since
@@ -333,6 +472,16 @@ module Hecks
333
472
  # refusing — a later step naming a record an earlier step never
334
473
  # created will refuse honestly on its own account, which is more
335
474
  # informative than silently dropping the rest of the batch.
475
+ #
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
336
485
  def dispatch_batch(runtime:, steps:, summary:, source: nil, role: nil, actor_id: nil)
337
486
  require_summary!(summary)
338
487
  results = Array(steps).map do |raw|
@@ -345,6 +494,17 @@ module Hecks
345
494
  refused(e, summary: summary)
346
495
  end
347
496
 
497
+ # Answers one declared query.
498
+ #
499
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
500
+ # @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
348
508
  def query(runtime:, question:, summary:, args: {}, source: nil, role: nil, actor_id: nil)
349
509
  bluebook = bluebook_for(runtime)
350
510
  outcome = perform_query(bluebook, runtime, question, summary, args, source, role, actor_id)
@@ -359,6 +519,20 @@ module Hecks
359
519
  outcome
360
520
  end
361
521
 
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
362
536
  def perform_query(bluebook, runtime, question, summary, args, source, role, actor_id)
363
537
  require_summary!(summary)
364
538
  valid_source!(source)
@@ -370,12 +544,22 @@ module Hecks
370
544
  ok(summary: summary, rows: rows.map { |row| Facade::JsonDoor.materialize(row) }).merge(verb: spec[:verb])
371
545
  end
372
546
 
373
- # WHAT IS ACTUALLY STORED — no verb, no interpretation, the repository
547
+ # Reads one aggregate's stored records directly, bypassing any declared query.
548
+ #
549
+ # **What is actually stored** — no verb, no interpretation, the repository
374
550
  # itself. `id:` given answers one record (`NotFound` when it names
375
551
  # nothing); omitted answers every record the aggregate currently
376
552
  # holds. This is the difference `query` can't cover: a query answers a
377
- # DECLARED question, and an aggregate that never declared "list
553
+ # declared question, and an aggregate that never declared "list
378
554
  # everything" has no query this could reuse.
555
+ #
556
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
557
+ # @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
559
+ # @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
379
563
  def state(runtime:, aggregate:, summary:, id: nil, source: nil)
380
564
  bluebook = bluebook_for(runtime)
381
565
  outcome = perform_state(runtime, bluebook, aggregate, summary, id)
@@ -388,6 +572,18 @@ module Hecks
388
572
  outcome
389
573
  end
390
574
 
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
391
587
  def perform_state(runtime, bluebook, aggregate, summary, id)
392
588
  require_summary!(summary)
393
589
  ir = aggregate_ir!(bluebook, aggregate)
@@ -405,13 +601,21 @@ module Hecks
405
601
 
406
602
  # ── the four zoom levels ─────────────────────────────────────────
407
603
 
408
- # ZOOM LEVEL ZERO — every domain directory a root actually holds,
604
+ # Lists every domain directory under a root.
605
+ #
606
+ # **Zoom level zero** — every domain directory a root actually holds,
409
607
  # discovered rather than typed from memory. Every other tool takes
410
608
  # `domain:` as a directory it assumes the caller already knows; this
411
609
  # is how a caller who doesn't finds out. `Adapters::Folder#domain?`
412
610
  # is the same predicate `domain_root`/`nearest_domain` already walk
413
611
  # up directories checking — a bare `.hecksagon` or one under
414
612
  # `bluebook/`, the two real shapes this corpus uses.
613
+ #
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`
415
619
  def domains(under: "examples")
416
620
  root = confine!(under, "under")
417
621
  return ok(under: under, domains: []) unless Dir.exist?(root)
@@ -422,10 +626,17 @@ module Hecks
422
626
  ok(under: under, domains: found.map { |name| File.join(under, name) })
423
627
  end
424
628
 
425
- # ZOOM LEVEL ONE — every aggregate this domain declares, and every
629
+ # Lists a domain's aggregates and their command/query names.
630
+ #
631
+ # **Zoom level one** — every aggregate this domain declares, and every
426
632
  # command/query name each answers to, snake_cased exactly as
427
633
  # `dispatch`/`query` want it. Enough to pick a target; `describe` is
428
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
429
640
  def catalog(runtime:)
430
641
  bluebook = bluebook_for(runtime)
431
642
 
@@ -439,12 +650,21 @@ module Hecks
439
650
  refused(e)
440
651
  end
441
652
 
442
- # ZOOM LEVEL TWO — the exact same usage document a human gets from
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
443
656
  # `bin/docs <domain> [aggregate]` (`Projector::DocsProjector`, the
444
657
  # identical projection `Surface::AggregateDoor#docs` calls one door
445
658
  # over): every command's arguments, the states it may be issued
446
659
  # from, and every way it can refuse. `aggregate:` omitted answers the
447
660
  # whole chapter.
661
+ #
662
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted runtime
663
+ # @param aggregate [String, Symbol, nil] one aggregate's name, or nil for the
664
+ # 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
448
668
  def describe(runtime:, aggregate: nil)
449
669
  bluebook = bluebook_for(runtime)
450
670
  options = aggregate ? { aggregate: aggregate_ir!(bluebook, aggregate).hecks_name } : {}
@@ -454,19 +674,21 @@ module Hecks
454
674
  refused(e)
455
675
  end
456
676
 
457
- # ZOOM LEVEL THREE — is the wiring sound at all: every bind names a
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
458
680
  # declared aggregate, every adapter satisfies the port it claims, the
459
681
  # default adapter is usable. `Registry#verify!` (`runtime/registry/
460
682
  # verification.rb`) is the one place this repo already answers that
461
683
  # 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
684
+ # step of every boot — so this is the one method here that boots for
463
685
  # itself rather than taking a `runtime:` already in hand, because a
464
686
  # runtime that successfully reached this line already answered the
465
- # question. Given a domain PATH, not a booted runtime, deliberately:
687
+ # question. Given a domain path, not a booted runtime, deliberately:
466
688
  # asking "is this valid" about a domain that failed to boot at all
467
689
  # has to be askable without a runtime to hand it.
468
690
  #
469
- # `deep: true` GOES PAST WIRING INTO LOGIC — `Bluebook::ModelCheck`,
691
+ # `deep: true` goes past wiring into logic — `Bluebook::ModelCheck`,
470
692
  # the lightweight-formal-methods leg (dead lifecycle transitions, a
471
693
  # saga state no handler chain reaches, a dispatch to nowhere). Opt-in
472
694
  # and separate from the base check on purpose: a wiring defect is
@@ -474,11 +696,17 @@ module Hecks
474
696
  # part of it can never fire" — two different questions, and the
475
697
  # first is far cheaper to ask on every boot.
476
698
  #
477
- # ANY BOOT FAILURE ANSWERS THE QUESTION, not only `WiringError` — a
699
+ # Any boot failure answers the question, not only `WiringError` — a
478
700
  # domain path with no `.hecksagon`, a malformed bluebook, is just as
479
701
  # much "not valid" as a real wiring mismatch, and this tool exists
480
702
  # precisely so none of those ever cross a projection of this bus as
481
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
482
710
  def validate(domain:, deep: false)
483
711
  runtime = Hecks.boot(confine!(domain, "domain"), install_facade: false)
484
712
  result = { ok: true, domain: domain, valid: true }
@@ -496,11 +724,20 @@ module Hecks
496
724
 
497
725
  # ── beyond the zoom levels ──────────────────────────────────────────
498
726
 
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.
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.
736
+ #
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
504
741
  def history(runtime:)
505
742
  bluebook = bluebook_for(runtime)
506
743
  entries = bluebook.aggregates.each_with_object({}) do |aggregate, all|
@@ -513,19 +750,30 @@ module Hecks
513
750
  refused(e)
514
751
  end
515
752
 
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
516
759
  def journal_entries(repository)
517
- return [] unless repository.is_a?(Ports::Persistence::AppendOnly)
518
-
519
760
  repository.entries.map { |entry| { operation: entry.operation, id: entry.id, state: Facade::JsonDoor.materialize(entry.state) } }
520
761
  end
521
762
 
522
- # `.behaviors` FILES, RUN AND REPORTED — hand-curated examples of how
763
+ # `.behaviors` files, run and reported — hand-curated examples of how
523
764
  # to use a domain, in domain vocabulary (`docs/guides/behaviors.md`),
524
765
  # the survey's own "honest-refusal", generated-example-suite items.
525
766
  # `Hecks::Behaviors` boots each test fresh through `Hecks.boot_files`
526
- # itself — `target:` names a `.behaviors` file OR a directory to
767
+ # itself — `target:` names a `.behaviors` file or a directory to
527
768
  # sweep, never a `runtime:`, the one other method here besides
528
769
  # `validate` that takes a path instead.
770
+ #
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
529
777
  def behaviors(target:)
530
778
  require_relative "behaviors"
531
779
  raise Runtime::NotFound, "no such file or directory: #{target.inspect}" unless target && File.exist?(target)
@@ -541,13 +789,18 @@ module Hecks
541
789
  refused(e)
542
790
  end
543
791
 
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`)
544
797
  def behaviors_file(result)
545
798
  { path: result.path,
546
799
  parse_error: result.parse_error,
547
800
  runs: Array(result.runs).map { |run| { description: run.description, status: run.status, message: run.message } } }
548
801
  end
549
802
 
550
- # A LIVE TAIL WITHOUT A LIVE PROCESS — `bin/hecks_mcp_door` (its
803
+ # A live tail without a live process — `bin/hecks_mcp_door` (its
551
804
  # transport of MCP-over-stdio) answers one request at a time, no push
552
805
  # channel to a client that only ever asks. This is the honest version
553
806
  # of the survey's `storehouse follow` for that shape: not a
@@ -555,6 +808,12 @@ module Hecks
555
808
  # dry-run call appends to (`record!`), tailed back here. Still real,
556
809
  # still cross-process — the log outlives any one door's own process —
557
810
  # just pull instead of push.
811
+ #
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
558
817
  def follow(runtime:, limit: 20)
559
818
  bluebook = bluebook_for(runtime)
560
819
  entries = log_lines(bluebook.name).last([limit.to_i, 1].max)
@@ -564,6 +823,11 @@ module Hecks
564
823
  refused(e)
565
824
  end
566
825
 
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
567
831
  def log_lines(domain_name)
568
832
  path = log_path(domain_name)
569
833
  return [] unless File.exist?(path)
@@ -571,21 +835,32 @@ module Hecks
571
835
  File.readlines(path).map { |line| JSON.parse(line, symbolize_names: true) }
572
836
  end
573
837
 
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:
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:
577
841
  # "one boot per call" means `runtime.events` is always empty except
578
842
  # during the very call that populated it, discarded the moment that
579
843
  # 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
844
+ # So this reads the same durable audit log `follow` already tails
581
845
  # (`record!` now stamps a successful dispatch's own announced
582
846
  # 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
847
+ # called, in order, across every tool"; this answers "what happened
848
+ # to one aggregate/record" — events this bus witnessed, which is
585
849
  # every real dispatch ever routed through it, but no more than that.
586
850
  # `aggregate:` narrows to one aggregate; `id:` (requires
587
851
  # `aggregate:` — an id alone is not unique across aggregates)
588
852
  # narrows to one record's own events.
853
+ #
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
589
864
  def events(runtime:, aggregate: nil, id: nil, limit: nil)
590
865
  raise Runtime::TypeMismatch, "id: requires aggregate: too — an id alone is not unique across aggregates" if id && !aggregate
591
866
 
@@ -606,6 +881,14 @@ module Hecks
606
881
  # self-contained per-entry check with nothing to share with its
607
882
  # neighbors, extracted only to keep `events` itself to the query's
608
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
609
892
  def entry_events(entry, fqn, id)
610
893
  return unless entry[:tool] == "dispatch" && entry[:ok] && entry[:events]
611
894
  return if fqn && !entry[:verb].to_s.start_with?("#{fqn}.")
@@ -616,24 +899,37 @@ module Hecks
616
899
 
617
900
  # ── shared shape ────────────────────────────────────────────────────
618
901
 
619
- # `Runtime::WiringError` BELONGS HERE TOO, alongside the true domain
620
- # refusals — not because it IS one (it's a structural defect, not a
902
+ # `Runtime::WiringError` belongs here too, alongside the true domain
903
+ # refusals — not because it is one (it's a structural defect, not a
621
904
  # rule the caller broke), but because "this domain isn't wired to
622
905
  # answer what you're asking" (a `role:`+`actor_id:` caller reaching
623
906
  # an authorization port nothing implements, a dry run against a
624
907
  # port verb) is exactly the shape this bus promises never crashes
625
908
  # through it. `dry_run_outcome` already treats it this way locally;
626
- # this makes every OTHER caller of `refusal_classes` do the same.
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
627
913
  def refusal_classes = [Runtime::NotFound, Runtime::TypeMismatch, Runtime::WiringError, *Runtime::DOMAIN_REFUSALS]
628
914
 
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`
629
919
  def ok(**fields) = { ok: true }.merge(fields)
630
920
 
631
- # AN HONEST REFUSAL, NOT A CRASH — the survey's own item #9: "an
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
632
924
  # explicit, structured refusal a caller can act on" rather than a
633
925
  # stack trace an agent has to parse to find the one line that
634
926
  # mattered. The domain's own refusal text travels verbatim
635
927
  # (`RefusalWording` already renders every one of these to be read),
636
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:}`
637
933
  def refused(error, summary: nil) = { ok: false, summary: summary, error: error.message }
638
934
  end
639
935
  end