hecks 1.2.0 → 1.4.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 (358) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +7 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +10 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
  7. data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
  9. data/lib/hecks/adapters/driven/heki.rb +13 -7
  10. data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
  12. data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
  13. data/lib/hecks/adapters/driven/lambda.rb +39 -33
  14. data/lib/hecks/adapters/driven/local_storage.rb +17 -10
  15. data/lib/hecks/adapters/driven/memory.rb +205 -9
  16. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
  17. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  18. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  19. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  20. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  21. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  22. data/lib/hecks/adapters/driven/postgres_era.adapter +5 -0
  23. data/lib/hecks/adapters/driven/prism.rb +4 -4
  24. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  25. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  26. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  27. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  28. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  29. data/lib/hecks/adapters/driven.rb +4 -4
  30. data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
  31. data/lib/hecks/behaviors/dsl.rb +2 -2
  32. data/lib/hecks/behaviors/expectations.rb +51 -23
  33. data/lib/hecks/behaviors/ir.rb +1 -1
  34. data/lib/hecks/behaviors/rspec.rb +1 -1
  35. data/lib/hecks/behaviors/runner.rb +2 -2
  36. data/lib/hecks/behaviors.rb +1 -1
  37. data/lib/hecks/bluebook/aggregate.rb +13 -13
  38. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  39. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  40. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  41. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  42. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  43. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  44. data/lib/hecks/bluebook/assembly.rb +9 -9
  45. data/lib/hecks/bluebook/attribute.rb +9 -9
  46. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  47. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  48. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  49. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  50. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
  51. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  52. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  53. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  54. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  55. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  56. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  57. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  58. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  59. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  60. data/lib/hecks/bluebook/capabilities.rb +27 -0
  61. data/lib/hecks/bluebook/chapter.rb +28 -9
  62. data/lib/hecks/bluebook/command.rb +12 -12
  63. data/lib/hecks/bluebook/domain_port.rb +9 -9
  64. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  65. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  66. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  67. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  68. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  69. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  70. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  71. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  72. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  73. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  74. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  75. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  76. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  77. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  78. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  79. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  80. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  81. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  82. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  83. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  84. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  85. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  86. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  87. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  88. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  89. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  90. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  91. data/lib/hecks/bluebook/entity.rb +11 -11
  92. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  93. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  94. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  95. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  97. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  98. data/lib/hecks/bluebook/hexagon.rb +1 -1
  99. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  101. data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
  102. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  103. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  104. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  105. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  106. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  107. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  108. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  109. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  110. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  111. data/lib/hecks/bluebook/model_check.rb +301 -84
  112. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  113. data/lib/hecks/bluebook/policy.rb +15 -13
  114. data/lib/hecks/bluebook/process_manager.rb +14 -14
  115. data/lib/hecks/bluebook/project_register.rb +6 -6
  116. data/lib/hecks/bluebook/query.rb +4 -4
  117. data/lib/hecks/bluebook/read_model.rb +14 -14
  118. data/lib/hecks/bluebook/reference.rb +8 -8
  119. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  120. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  121. data/lib/hecks/bluebook/translation.rb +4 -4
  122. data/lib/hecks/bluebook/value_object.rb +6 -6
  123. data/lib/hecks/bluebook.rb +2 -2
  124. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  125. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  126. data/lib/hecks/codemod.rb +36 -35
  127. data/lib/hecks/construct.rb +6 -6
  128. data/lib/hecks/corpus.rb +317 -0
  129. data/lib/hecks/deprecation.rb +95 -0
  130. data/lib/hecks/doc/reference.rb +19 -19
  131. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  132. data/lib/hecks/facade/cli_door.rb +69 -10
  133. data/lib/hecks/facade/cli_runner.rb +105 -24
  134. data/lib/hecks/facade/command_request.rb +23 -0
  135. data/lib/hecks/facade/handle.rb +79 -32
  136. data/lib/hecks/facade/json_door.rb +106 -25
  137. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  138. data/lib/hecks/facade/surface/chapter.rb +26 -17
  139. data/lib/hecks/facade/surface.rb +16 -3
  140. data/lib/hecks/facade.rb +15 -4
  141. data/lib/hecks/forms/app.rb +46 -30
  142. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  143. data/lib/hecks/forms/field_renderer.rb +142 -6
  144. data/lib/hecks/forms/field_shape.rb +183 -20
  145. data/lib/hecks/forms/html.rb +51 -7
  146. data/lib/hecks/forms/index_renderer.rb +14 -2
  147. data/lib/hecks/forms/params.rb +120 -23
  148. data/lib/hecks/forms/port_argument.rb +2 -2
  149. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_renderer.rb +2 -2
  151. data/lib/hecks/forms/record_table.rb +1 -1
  152. data/lib/hecks/forms/value_object_shape.rb +3 -3
  153. data/lib/hecks/forms.rb +24 -4
  154. data/lib/hecks/fqn.rb +1 -1
  155. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  156. data/lib/hecks/framework.rb +48 -17
  157. data/lib/hecks/freezer.rb +11 -11
  158. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  159. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  160. data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
  161. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  162. data/lib/hecks/fuzzing/differential.rb +158 -0
  163. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  164. data/lib/hecks/fuzzing/era_boundary.rb +124 -0
  165. data/lib/hecks/fuzzing/form_census.rb +199 -0
  166. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  167. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  168. data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
  169. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  170. data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
  171. data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
  172. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
  173. data/lib/hecks/fuzzing/properties/guards.rb +129 -26
  174. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  175. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
  176. data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
  177. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  178. data/lib/hecks/fuzzing/properties.rb +84 -39
  179. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  180. data/lib/hecks/fuzzing/replay.rb +224 -114
  181. data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
  182. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  183. data/lib/hecks/fuzzing/self_consistency.rb +676 -0
  184. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
  189. data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
  190. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  191. data/lib/hecks/fuzzing/structural_skips.rb +39 -0
  192. data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
  193. data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
  194. data/lib/hecks/fuzzing/value_generator.rb +66 -14
  195. data/lib/hecks/fuzzing.rb +11 -0
  196. data/lib/hecks/grammar/evolve.rb +10 -10
  197. data/lib/hecks/grammar.rb +7 -7
  198. data/lib/hecks/ir.rb +13 -13
  199. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  200. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  201. data/lib/hecks/language/bluebook/vocabulary.bluebook +365 -15
  202. data/lib/hecks/language/oidc.json +5 -0
  203. data/lib/hecks/literal.rb +9 -9
  204. data/lib/hecks/naming.rb +89 -21
  205. data/lib/hecks/ports/access_control.rb +58 -2
  206. data/lib/hecks/ports/agent/answers.rb +83 -6
  207. data/lib/hecks/ports/agent.rb +119 -35
  208. data/lib/hecks/ports/authentication.rb +44 -4
  209. data/lib/hecks/ports/authorization.rb +53 -11
  210. data/lib/hecks/ports/clock.rb +42 -23
  211. data/lib/hecks/ports/extraction.rb +16 -0
  212. data/lib/hecks/ports/identity_assignment.rb +24 -2
  213. data/lib/hecks/ports/identity_generation.rb +17 -3
  214. data/lib/hecks/ports/identity_resolution.rb +18 -1
  215. data/lib/hecks/ports/loading.rb +4 -0
  216. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  217. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  218. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  219. data/lib/hecks/ports/persistence/execution.rb +4 -0
  220. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  221. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  222. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  224. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  225. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  226. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +160 -58
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +44 -6
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
  242. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  247. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  248. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  249. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  252. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  253. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  254. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  255. data/lib/hecks/ports/persistence/repository_factory.rb +29 -6
  256. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  257. data/lib/hecks/ports/persistence.rb +36 -1
  258. data/lib/hecks/ports/projection.rb +61 -7
  259. data/lib/hecks/ports/query/in_memory.rb +3 -3
  260. data/lib/hecks/ports/query/ordering.rb +6 -6
  261. data/lib/hecks/ports/query.rb +35 -0
  262. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  263. data/lib/hecks/projections/diagrams.rb +75 -75
  264. data/lib/hecks/projections/glossary/html.rb +250 -0
  265. data/lib/hecks/projections/glossary/markdown.rb +105 -0
  266. data/lib/hecks/projections/glossary/mermaid.rb +110 -0
  267. data/lib/hecks/projections/glossary/page.css +271 -0
  268. data/lib/hecks/projections/glossary/page.js +72 -0
  269. data/lib/hecks/projections/glossary/sections.rb +17 -0
  270. data/lib/hecks/projections/glossary/sentences.rb +205 -0
  271. data/lib/hecks/projections/glossary.rb +214 -286
  272. data/lib/hecks/projections/ir.rb +1 -1
  273. data/lib/hecks/projections/model/deviations.rb +18 -17
  274. data/lib/hecks/projections/model.rb +25 -21
  275. data/lib/hecks/projections/oidc.rb +7 -7
  276. data/lib/hecks/projections/parser_table.rb +5 -5
  277. data/lib/hecks/projections/reference.rb +3 -3
  278. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  279. data/lib/hecks/projections/shape.rb +2 -2
  280. data/lib/hecks/projections/statements.rb +11 -11
  281. data/lib/hecks/projections/vocabulary.rb +9 -9
  282. data/lib/hecks/projections.rb +5 -3
  283. data/lib/hecks/projector/cli_projector.rb +29 -29
  284. data/lib/hecks/projector/docs_projector.rb +13 -13
  285. data/lib/hecks/projector/exporter.rb +42 -21
  286. data/lib/hecks/projector/ir_projector.rb +1 -1
  287. data/lib/hecks/projector/narrate_projector.rb +15 -22
  288. data/lib/hecks/projector/target.rb +13 -13
  289. data/lib/hecks/projector.rb +15 -15
  290. data/lib/hecks/query_ir.rb +47 -47
  291. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  292. data/lib/hecks/query_specification/common/comparison.rb +132 -24
  293. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  294. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  295. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  296. data/lib/hecks/query_specification/common/options.rb +25 -0
  297. data/lib/hecks/query_specification/field_path.rb +69 -15
  298. data/lib/hecks/query_specification/hop_path.rb +57 -20
  299. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  300. data/lib/hecks/rendering.rb +3 -3
  301. data/lib/hecks/router/namespace_installer.rb +3 -3
  302. data/lib/hecks/router.rb +1 -1
  303. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  304. data/lib/hecks/runtime/boot_gates.rb +3 -3
  305. data/lib/hecks/runtime/caller.rb +8 -8
  306. data/lib/hecks/runtime/capability_graph.rb +2 -2
  307. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  308. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
  309. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  310. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  311. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  312. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  313. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  314. data/lib/hecks/runtime/command_rules/references.rb +94 -19
  315. data/lib/hecks/runtime/command_rules.rb +3 -0
  316. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  317. data/lib/hecks/runtime/dispatcher.rb +283 -90
  318. data/lib/hecks/runtime/entity_element.rb +282 -46
  319. data/lib/hecks/runtime/entity_interpreter.rb +160 -69
  320. data/lib/hecks/runtime/errors.rb +19 -19
  321. data/lib/hecks/runtime/event.rb +6 -6
  322. data/lib/hecks/runtime/identity.rb +22 -22
  323. data/lib/hecks/runtime/instance.rb +39 -14
  324. data/lib/hecks/runtime/interpreting.rb +12 -12
  325. data/lib/hecks/runtime/invocation.rb +276 -0
  326. data/lib/hecks/runtime/loader.rb +14 -14
  327. data/lib/hecks/runtime/outbox.rb +23 -23
  328. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  329. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  330. data/lib/hecks/runtime/query_interpreter.rb +111 -56
  331. data/lib/hecks/runtime/reaction_invocation.rb +76 -9
  332. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  333. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  334. data/lib/hecks/runtime/reference_hop.rb +6 -6
  335. data/lib/hecks/runtime/refusal_wording.rb +92 -112
  336. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  337. data/lib/hecks/runtime/registry/verification.rb +36 -26
  338. data/lib/hecks/runtime/registry.rb +56 -27
  339. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  340. data/lib/hecks/runtime/routing.rb +10 -88
  341. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  342. data/lib/hecks/runtime/saga_interpreter.rb +93 -56
  343. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  344. data/lib/hecks/runtime/tenant_check.rb +9 -9
  345. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  346. data/lib/hecks/runtime/value/admission.rb +75 -30
  347. data/lib/hecks/runtime/value/coercion.rb +379 -226
  348. data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
  349. data/lib/hecks/runtime/value.rb +28 -23
  350. data/lib/hecks/runtime.rb +7 -7
  351. data/lib/hecks/storehouse.rb +64 -64
  352. data/lib/hecks/version.rb +3 -3
  353. data/lib/hecks/vocabulary.rb +207 -5
  354. data/lib/hecks.rb +13 -11
  355. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  356. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  357. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  358. metadata +43 -2
@@ -9,17 +9,27 @@ module Hecks
9
9
  # singleton, one aggregate door per declared head, and the
10
10
  # declaration hook for names the door does not carry.
11
11
  module Chapter
12
+ # Builds the anonymous module that stands for one booted chapter: `vision`,
13
+ # `aggregates`, `docs`, `narrate` and `project` as singleton methods, plus one
14
+ # nested constant per aggregate holding that aggregate's door.
15
+ #
16
+ # @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted
17
+ # dispatcher each aggregate door closes over
18
+ # @param bluebook [Bluebook::Chapter] the chapter to project into a module
19
+ # @return [Module] a fresh, unnamed module; `Surface.install` gives it its
20
+ # top-level name
21
+ # @raise [NameError] if an aggregate's name is not a valid constant name
12
22
  def chapter_module(dispatcher, bluebook)
13
23
  chapter = Module.new
14
24
  chapter.define_singleton_method(:vision) { bluebook.vision }
15
25
  chapter.define_singleton_method(:aggregates) { bluebook.aggregates.map(&:name).sort }
16
26
 
17
- # THE CHAPTER, EXPLAINING ITSELF. `Projector::DocsProjector` reads
27
+ # **The chapter, explaining itself**. `Projector::DocsProjector` reads
18
28
  # nothing but this bluebook's own IR, so the document cannot drift
19
29
  # from the domain — the same guarantee `bin/reference` gives the DSL
20
30
  # reference by generating it from the Syntax chapter.
21
31
  #
22
- # A METHOD RATHER THAN ONLY A SCRIPT because that is what gets it
32
+ # A method rather than only a script because that is what gets it
23
33
  # read: `QualityControl.docs` in a console, beside `vision` and
24
34
  # `aggregates`, at the moment somebody is wondering what a verb
25
35
  # wants. Projected on each call rather than memoised — it is a pure
@@ -29,14 +39,14 @@ module Hecks
29
39
  Projector.call(:docs, bluebook: bluebook, options: options)
30
40
  end
31
41
 
32
- # THE CHAPTER, READ BACK IN ENGLISH — `Projector::NarrateProjector`
42
+ # **The chapter, read back in english** — `Projector::NarrateProjector`
33
43
  # beside `:docs`, for the reader who needs to confirm the domain is
34
44
  # right rather than call it: `QualityControl.narrate` in a console.
35
45
  chapter.define_singleton_method(:narrate) do |**options|
36
46
  Projector.call(:narrate, bluebook: bluebook, options: options)
37
47
  end
38
48
 
39
- # THE DOMAIN'S OWN IR, PROJECTED. `Projector` has taken
49
+ # **The domain's own IR, projected**. `Projector` has taken
40
50
  # `call(name, bluebook:, options:)` since §30, but nothing could
41
51
  # reach it from a booted domain — this module already closes
42
52
  # over the one `Bluebook` every projector wants and simply
@@ -69,26 +79,25 @@ module Hecks
69
79
  chapter.const_set(aggregate.hecks_name, aggregate_module(dispatcher, bluebook.name, aggregate))
70
80
  end
71
81
 
72
- # INSIDE A HECKSAGON, A NAME IS A DECLARATION, NOT A LOOKUP. A
73
- # `.hecksagon` may name an aggregate this door does not carry — a STALE
74
- # door from an earlier boot resolving another registry's chapter, the
75
- # exact hazard the constant tree used to hide by reinstalling on every
76
- # load. With a collector open, the name becomes a `BindingProxy`
82
+ # Inside a hecksagon, a name is a declaration, not a lookup. A
83
+ # `.hecksagon` may name an aggregate this door does not carry — a stale
84
+ # door from an earlier boot resolving another registry's chapter, which
85
+ # can happen because the facade is installed once a boot finishes, not
86
+ # on every load. With a collector open, the name becomes a `BindingProxy`
77
87
  # recording the same bind the aggregate module would ; without one, it
78
- # is a genuine NameError, exactly as before.
88
+ # is a genuine NameError.
79
89
  #
80
- # THE OTHER READER OF A MISS is S0b's own bridge (docs/dsl-work-
90
+ # The other reader of a miss is S0b's own bridge (docs/dsl-work-
81
91
  # slices.md, const_shim.rb's `ScopedConstant`): a bluebook still being
82
- # DECLARED that names `#{bluebook.name}::Something` — an event, a
83
- # command reference, an `admits:` — reaches HERE, not
92
+ # declared that names `#{bluebook.name}::Something` — an event, a
93
+ # command reference, an `admits:` — reaches here, not
84
94
  # `Object.const_missing`, the moment this chapter's own facade already
85
95
  # exists (a previous boot in the same process, or this same chapter
86
96
  # re-entering its own name). `ConstShim.active?` is exactly as true
87
97
  # here as it is at the top level, mid-declaration, and consulting the
88
- # SAME resolver is what makes a scoped reference resolve identically
89
- # whether or not a facade happens to be built yet — the earlier
90
- # attempt's own failure mode (this file's own history) was a bridge
91
- # that worked only BEFORE any facade existed.
98
+ # same resolver is what makes a scoped reference resolve identically
99
+ # whether or not a facade happens to be built yet — a bridge consulted
100
+ # only at the top level works only before any facade exists.
92
101
  chapter.define_singleton_method(:const_missing) do |name|
93
102
  collector = Bluebook::DSL::HecksagonBuilder.collector
94
103
  return Bluebook::DSL::BindingProxy.new("#{bluebook.name}::#{name}", collector) if collector
@@ -3,9 +3,9 @@ require_relative "surface/aggregate_door"
3
3
 
4
4
  module Hecks
5
5
  module Facade
6
- # THE DOOR, WITHOUT THE CLASSES.
6
+ # **The door, without the classes**.
7
7
  #
8
- # `Pizzas::Pizza.create_pizza(...)` is the public surface (HANDOVER rule 3),
8
+ # `Pizzas::Pizza.create_pizza!(...)` is the public surface (handover rule 3),
9
9
  # and this is what serves it now : anonymous per-boot modules whose
10
10
  # singleton methods close over the dispatcher and dispatch by FQN — the
11
11
  # same shape `Router::NamespaceInstaller` proved. Nothing here is a domain
@@ -17,7 +17,7 @@ module Hecks
17
17
  # The hexagon-binding door rides along : `Pizzas::Pizza.persisted_by("Heki")`
18
18
  # in a `.hecksagon` file lands on the module's `method_missing`, which
19
19
  # records an `Bind` into whatever `HecksagonBuilder.collector` is open
20
- # AT CALL TIME — so even a facade left over from a previous boot records
20
+ # at call time — so even a facade left over from a previous boot records
21
21
  # into the current builder, and a chapter with no constant at all falls
22
22
  # through to `ConstShim` → `BindingProxy`, which mints byte-identical binds.
23
23
  module Surface
@@ -30,6 +30,19 @@ module Hecks
30
30
 
31
31
  module_function
32
32
 
33
+ # Installs one top-level module per booted chapter, and one per aggregate, each
34
+ # closing over `dispatcher`, so `Pizzas::Pizza` and the bare `Pizza` both open the
35
+ # door of the boot that ran last.
36
+ #
37
+ # An aggregate sharing its chapter's name gets no second constant: the chapter
38
+ # module already holds that name. A name that user code or the stdlib already owns
39
+ # is left alone with a warning (see `Namespace.install`).
40
+ #
41
+ # @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted
42
+ # dispatcher every installed door dispatches and queries through
43
+ # @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the same `dispatcher`,
44
+ # so a boot can return the call's result
45
+ # @raise [NameError] if a chapter or aggregate name is not a valid constant name
33
46
  def install(dispatcher)
34
47
  dispatcher.registry.bluebooks.each_value do |bluebook|
35
48
  chapter = chapter_module(dispatcher, bluebook)
data/lib/hecks/facade.rb CHANGED
@@ -7,15 +7,15 @@ module Hecks
7
7
 
8
8
  # The one careful way to put something at top level.
9
9
  #
10
- # Overwrites only what it itself installed (tracked in GENERATED), refuses —
10
+ # Overwrites only what it itself installed (tracked in `GENERATED`), refuses —
11
11
  # with a warning, never a raise — to clobber a constant belonging to user
12
12
  # code or the stdlib. Which is why a chapter named `Set` never becomes a
13
13
  # constant, and why `Registry` keeps its own table of chapters rather than
14
- # trusting Ruby's. Each re-install REPLACES the previous boot's entry, so
15
- # what GENERATED retains is one small facade module per name, not a graph
14
+ # trusting Ruby's. Each re-install replaces the previous boot's entry, so
15
+ # what `GENERATED` retains is one small facade module per name, not a graph
16
16
  # per boot.
17
17
  module Namespace
18
- # NOT frozen — a real registry, mutated below
18
+ # Not frozen — a real registry, mutated below
19
19
  # (`GENERATED[[container, name]] = value`). False positive for
20
20
  # Style/MutableConstant.
21
21
  # rubocop:disable-next Style/MutableConstant
@@ -23,6 +23,17 @@ module Hecks
23
23
 
24
24
  module_function
25
25
 
26
+ # Sets `container::name` to `value`, replacing only a constant this module installed
27
+ # itself and leaving any other existing constant in place with a warning.
28
+ #
29
+ # @param container [Module] the namespace to define the constant in; every caller in
30
+ # this repository passes `Object`
31
+ # @param name [String, Symbol] the constant name, such as a chapter or aggregate name
32
+ # @param value [Module] the facade module to install under that name
33
+ # @return [Object] `value` when it was installed; otherwise the constant already
34
+ # defined under `name`, which this module does not own and did not replace
35
+ # @raise [NameError] if `name` is not a valid constant name (raised by
36
+ # `const_defined?`)
26
37
  def install(container, name, value)
27
38
  name = name.to_s
28
39
 
@@ -14,17 +14,24 @@ module Hecks
14
14
  module Forms
15
15
  # The content-negotiated router: `GET /Banking/Account/Overdrawn.html`
16
16
  # renders the query view built in this directory; the identical path
17
- # with no extension (or `.json`) dispatches the SAME ask through
17
+ # with no extension (or `.json`) dispatches the same ask through
18
18
  # `Runtime::Dispatcher#query` and answers with its raw result — one
19
19
  # route, two representations, exactly the "change the file format on
20
20
  # the route" mechanism this was built around
21
21
  # (docs/command-form-and-query-form-bluebook.md).
22
22
  #
23
23
  # A plain Rack app (`#call(env)`) — no Sinatra, no Rails. `rack` itself
24
- # is a LAZY dependency the same way `pg`/`oauth2`/`aws-sdk-lambda` are
24
+ # is a lazy dependency the same way `pg`/`oauth2`/`aws-sdk-lambda` are
25
25
  # for their own adapters (see the Gemfile's own comment) : a project
26
26
  # that never boots this file never needs it installed.
27
27
  class App
28
+ # Builds the Rack app for a configured app name, exposing exactly the chapters its
29
+ # `Forms.configure` block declared.
30
+ #
31
+ # @param registry [Runtime::Registry] the booted registry holding the exposed chapters
32
+ # @param app_name [String, Symbol] the name an earlier `Forms.configure` call registered
33
+ # @return [Forms::App] a Rack app routing only the configured chapters
34
+ # @raise [ArgumentError] if no app of that name has been configured
28
35
  def self.for(registry:, app_name:)
29
36
  config = Forms.config(app_name) ||
30
37
  raise(ArgumentError, "no app #{app_name.inspect} configured — " \
@@ -32,12 +39,23 @@ module Hecks
32
39
  new(registry: registry, exposed: config.exposes)
33
40
  end
34
41
 
42
+ # @param registry [Runtime::Registry] the booted registry holding the exposed chapters
43
+ # @param exposed [Array<String>] names of the chapters (domains) this app routes; a
44
+ # request for any other domain is answered 404
45
+ # @param dispatcher [Runtime::Dispatcher, nil] the dispatcher commands and queries go
46
+ # through; nil builds a `Runtime::Dispatcher` over `registry`
35
47
  def initialize(registry:, exposed:, dispatcher: nil)
36
48
  @registry = registry
37
49
  @exposed = exposed
38
50
  @dispatcher = dispatcher || Runtime::Dispatcher.new(registry)
39
51
  end
40
52
 
53
+ # Answers one Rack request, routing on the path and its trailing `.html`/`.json` format;
54
+ # an unknown or unexposed route is answered as a plain-text 404 rather than raised.
55
+ #
56
+ # @param env [Hash{String => Object}] the Rack environment for the request
57
+ # @return [Array(Integer, Hash{String => String}, Array<String>)] the Rack response
58
+ # triple of status, headers and body
41
59
  def call(env)
42
60
  request = Rack::Request.new(env)
43
61
  route(request)
@@ -81,19 +99,18 @@ module Hecks
81
99
  raise RouteNotFound, "#{domain.inspect} is not exposed by this app — declared chapters: #{@exposed.join(', ')}"
82
100
  end
83
101
 
84
- # H12 (docs/audits/2026-08-10-main-bug-audit.md) — splitting on the
85
- # FIRST "." truncated any identity value containing a dot (an email
102
+ # H12 (docs/audits/2026-08-10-main-bug-audit.md) — only a literal
103
+ # trailing ".html"/".json" counts as a format; every other dot in the
104
+ # segment is just part of the identity. Splitting on the first "."
105
+ # instead truncates any identity value containing a dot (an email
86
106
  # `identified_by { email.address }`, a decimal-ish reference — an
87
107
  # aggregate's identity is free-form unless its value object declares
88
108
  # a `pattern:`, see S3 in the same audit) at its own first dot, so
89
- # `reference.value=c.1` 404'd everywhere: detail page, JSON view, and
90
- # its own index-table link. Only a LITERAL trailing ".html"/".json"
91
- # now counts as a format — every other dot in the segment is just
92
- # part of the identity. An identity that itself happens to end in
109
+ # `reference.value=c.1` 404s everywhere: detail page, JSON view, and
110
+ # its own index-table link. An identity that itself happens to end in
93
111
  # exactly ".html" or ".json" is still ambiguous with a real format
94
- # suffix (the same tension any extension-based content-negotiation
95
- # scheme has), but that was already true before this fix and is not
96
- # this bug.
112
+ # suffix the same tension any extension-based content-negotiation
113
+ # scheme has, and not what H12 is about.
97
114
  def split_format(segment)
98
115
  segment = segment.to_s
99
116
  return [Regexp.last_match(1), Regexp.last_match(2)] if segment =~ /\A(.*)\.(html|json)\z/
@@ -123,11 +140,11 @@ module Hecks
123
140
  breadcrumbs: [[chapter.name, "/"], [aggregate.hecks_name, nil]])
124
141
  else
125
142
  instances = @registry.repository(chapter.name, aggregate).all
126
- # id LAST — see Instance#to_h's own comment: an aggregate free
143
+ # id last — see Instance#to_h's own comment: an aggregate free
127
144
  # to declare its own attribute literally named `id` has that
128
145
  # attribute's own wrapped value sitting in `i.state[:id]`
129
- # already, which used to silently clobber the correct bare
130
- # identity when merged first.
146
+ # already, which silently clobbers the correct bare identity
147
+ # if `id:` is merged first.
131
148
  json(200, instances.map { |i| i.state.merge(id: i.id) })
132
149
  end
133
150
  end
@@ -141,14 +158,13 @@ module Hecks
141
158
  # L11 (docs/audits/2026-08-10-main-bug-audit.md) — a record's own
142
159
  # id is free-form (S3) and can collide with one of its own
143
160
  # aggregate's command/query names ("Close", "Overdrawn", ...).
144
- # A GET for such an id must still be able to reach that RECORD's
161
+ # A GET for such an id must still be able to reach that record's
145
162
  # own detail page when a record with that literal id actually
146
- # exists — checking the verb first (the previous order) meant a
147
- # record unlucky enough to be named after a real verb could never
148
- # be viewed again. POST never means "view a record" at all
149
- # (`record_route` only ever answers GET), so command submission
150
- # there is unambiguous and is left to match the verb first, same
151
- # as before.
163
+ # exists — checking the verb first would mean a record unlucky
164
+ # enough to be named after a real verb could never be viewed.
165
+ # POST never means "view a record" at all (`record_route` only
166
+ # ever answers GET), so command submission there is unambiguous
167
+ # and matches the verb first.
152
168
  if request.get? && (instance = @registry.repository(domain, aggregate).find(verb_or_id))
153
169
  return record_route(request, domain, aggregate, verb_or_id, format, instance: instance)
154
170
  end
@@ -187,7 +203,7 @@ module Hecks
187
203
 
188
204
  def submit_command(request, domain, aggregate, command, action)
189
205
  raw, envelope = submitted_command(request, aggregate, command)
190
- result = @dispatcher.dispatch("#{domain}::#{aggregate.hecks_name}.#{command.hecks_name}", **envelope)
206
+ result = @dispatcher.dispatch_flat("#{domain}::#{aggregate.hecks_name}.#{command.hecks_name}", envelope)
191
207
  # L12 — the id is free-form (S3), so it must be percent-encoded as
192
208
  # a path segment here, not just interpolated raw.
193
209
  redirect("/#{domain}/#{aggregate.hecks_name}/#{Escape.path(result.id)}.html")
@@ -201,8 +217,8 @@ module Hecks
201
217
  return respond(405, "text/plain", "GET or POST only") unless request.post?
202
218
 
203
219
  _, envelope = submitted_command(request, aggregate, command)
204
- result = @dispatcher.dispatch("#{domain}::#{aggregate.hecks_name}.#{command.hecks_name}", **envelope)
205
- # id LAST — same reasoning as the other JSON-serializing call
220
+ result = @dispatcher.dispatch_flat("#{domain}::#{aggregate.hecks_name}.#{command.hecks_name}", envelope)
221
+ # id last — same reasoning as the other JSON-serializing call
206
222
  # sites in this file (see aggregate_route's own comment).
207
223
  json(201, result.state.merge(id: result.id))
208
224
  rescue *Runtime::DOMAIN_REFUSALS, ArgumentError, TypeError, JSON::ParserError => e
@@ -239,7 +255,7 @@ module Hecks
239
255
  results, error = run_query(domain, aggregate, query, fields, asked)
240
256
  return json(422, { error: error.class.name.split("::").last, message: error.message }) if error
241
257
 
242
- # id LAST — same reasoning as the other JSON-serializing call
258
+ # id last — same reasoning as the other JSON-serializing call
243
259
  # sites in this file (see aggregate_route's own comment).
244
260
  json(200, results.map { |i| i.state.merge(id: i.id) })
245
261
  end
@@ -259,10 +275,10 @@ module Hecks
259
275
  # JSON (the honest fallback for a multi-attribute list element this
260
276
  # prototype's textarea doesn't build a second widget for). A caller
261
277
  # who types a non-JSON line into that field raises `JSON::ParserError`
262
- # BEFORE dispatch ever sees it both command submission paths
263
- # already rescue it (`submit_command`, `command_json`); this one
264
- # didn't, so a malformed list-of-VO query 500'd instead of showing
265
- # the same 422 every other bad-input path shows.
278
+ # before dispatch ever sees it. It is rescued here, as both command
279
+ # submission paths rescue it (`submit_command`, `command_json`), so
280
+ # a malformed list-of-VO query shows the same 422 every other
281
+ # bad-input path shows rather than a 500.
266
282
  rescue *Runtime::DOMAIN_REFUSALS, ArgumentError, TypeError, JSON::ParserError => e
267
283
  [nil, e]
268
284
  end
@@ -272,7 +288,7 @@ module Hecks
272
288
 
273
289
  instance ||= @registry.repository(domain, aggregate).find(id)
274
290
  return not_found(aggregate, id, format) unless instance
275
- # id LAST — same reasoning as the other JSON-serializing call
291
+ # id last — same reasoning as the other JSON-serializing call
276
292
  # sites in this file (see aggregate_route's own comment).
277
293
  return json(200, instance.state.merge(id: instance.id)) if format != "html"
278
294
 
@@ -13,22 +13,46 @@ module Hecks
13
13
  # renders through here — nothing here is per-command code; see that
14
14
  # doc for the 1:1 rule this deliberately holds to for now.
15
15
  module CommandFormRenderer
16
- # `registry` is only used to populate a `:reference` field's
17
- # `<select>` with real records (including the identity picker itself,
18
- # for a non-creating command — see `identity_field` below). `values`/
19
- # `error` carry a sticky re-render after a refused submission; leave
20
- # both nil/`{}` for a fresh form. `prefill` carries values a caller
21
- # arrived WITH (typically `?to=...` off a record's own detail page) —
22
- # kept separate from `values` because a prefill is not an error retry
23
- # and should not be treated as one by a future reader of this code.
24
- # The SAME field list a POST handler needs to cast raw params against
16
+ # Lists every field a command's form shows: the `to` identity picker first (unless the
17
+ # command creates its aggregate), then one field per command attribute.
18
+ #
19
+ # The same field list a POST handler needs to cast raw params against
25
20
  # (params.rb's `Params.extract`) — one derivation, so a page never
26
21
  # renders an input the submit handler doesn't also expect.
22
+ #
23
+ # @param aggregate [Bluebook::Aggregate] the aggregate that declares the command
24
+ # @param command [Bluebook::Command] the command the form submits
25
+ # @return [Array<Forms::Field>] the form's top-level fields, in render order
26
+ # @raise [Bluebook::DSL::Malformed] if a `reference_to` attribute cannot say which
27
+ # aggregate declares it
27
28
  def self.fields_for(aggregate, command)
28
29
  addressing = command.creates? ? [] : [identity_field(aggregate)]
29
30
  addressing + command.attributes.map { |a| FieldShape.resolve(a, aggregate: aggregate) }
30
31
  end
31
32
 
33
+ # Renders the page body for one command: header, refusal banner, the form itself, and
34
+ # the inspect panel.
35
+ #
36
+ # `registry` serves only to populate a `:reference` field's
37
+ # `<select>` with real records (including the identity picker itself,
38
+ # for a non-creating command — see `identity_field` below). `values`/
39
+ # `error` carry a sticky re-render after a refused submission; leave
40
+ # both nil for a fresh form. `prefill` carries values a caller
41
+ # arrived with (typically `?to=...` off a record's own detail page) —
42
+ # kept separate from `values` because a prefill is not an error retry
43
+ # and should not be treated as one by a future reader of this code.
44
+ #
45
+ # @param registry [Runtime::Registry] the booted registry, read for reference options
46
+ # @param domain [String] name of the domain (chapter) the aggregate belongs to
47
+ # @param aggregate [Bluebook::Aggregate] the aggregate that declares the command
48
+ # @param command [Bluebook::Command] the command the form submits
49
+ # @param action [String] URL path the form posts to
50
+ # @param values [Hash{String => Object}, nil] the raw submission to re-show after a
51
+ # refusal, keyed by dotted field path; nil falls back to `prefill`
52
+ # @param error [Exception, nil] the refusal to show in the banner; nil for a fresh form
53
+ # @param prefill [Hash{String => String}] values the caller arrived with, keyed by
54
+ # dotted field path; ignored when `values` is given
55
+ # @return [String] the HTML page body, without the surrounding page chrome
32
56
  def self.render(registry:, domain:, aggregate:, command:, action:, values: nil, error: nil, prefill: {})
33
57
  all_fields = fields_for(aggregate, command)
34
58
  reference_options = ReferenceOptions.collect(registry, domain, all_fields)
@@ -48,12 +72,24 @@ module Hecks
48
72
  HTML
49
73
  end
50
74
 
75
+ # Builds the `to` field a non-creating command uses to name the record it acts on,
76
+ # labelled with the aggregate's identity paths.
77
+ #
78
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose records the picker offers
79
+ # @return [Forms::Field] a `:reference` field at path `"to"` targeting `aggregate`
51
80
  def self.identity_field(aggregate)
52
81
  Field.new(path: "to", label: "#{aggregate.hecks_name} (#{aggregate.identity_paths.join(', ')})",
53
82
  kind: :reference, html_type: "text", target_aggregate: aggregate,
54
83
  help: "The record this command acts on.")
55
84
  end
56
85
 
86
+ # Renders the form's heading: the command's qualified name, its role and creates
87
+ # badges, its goal, and its preconditions callout.
88
+ #
89
+ # @param domain [String] name of the domain (chapter) the aggregate belongs to
90
+ # @param aggregate [Bluebook::Aggregate] the aggregate that declares the command
91
+ # @param command [Bluebook::Command] the command being described
92
+ # @return [String] HTML for the heading block
57
93
  def self.header(domain, aggregate, command)
58
94
  <<~HTML
59
95
  <h1>#{Escape.html("#{domain}::#{aggregate.hecks_name}.#{command.hecks_name}")}</h1>
@@ -64,6 +100,11 @@ module Hecks
64
100
  HTML
65
101
  end
66
102
 
103
+ # Renders the command's `given` preconditions as a callout, so a caller sees what
104
+ # would refuse the submission before making it.
105
+ #
106
+ # @param command [Bluebook::Command] the command whose givens are listed
107
+ # @return [String] HTML for the callout, or `""` when the command declares no givens
67
108
  def self.givens_callout(command)
68
109
  return "" if command.givens.empty?
69
110
 
@@ -71,6 +112,11 @@ module Hecks
71
112
  %(<div class="callout"><strong>Preconditions</strong> — refused if any fail:<ul>#{items.join}</ul></div>)
72
113
  end
73
114
 
115
+ # Renders a refused submission as an alert banner carrying the refusal's class name
116
+ # (without its namespace) and message.
117
+ #
118
+ # @param error [Exception, nil] the refusal raised by the submission
119
+ # @return [String] HTML for the banner, or `""` when `error` is nil
74
120
  def self.error_banner(error)
75
121
  return "" unless error
76
122
 
@@ -81,14 +127,29 @@ module Hecks
81
127
  HTML
82
128
  end
83
129
 
130
+ # Maps a refusal to per-field messages; always empty, so every refusal is shown in
131
+ # the banner rather than beside a field.
132
+ #
84
133
  # No structured field attribution exists on a domain refusal today
85
134
  # (it is a typed exception with a rendered message — see
86
135
  # docs/command-form-and-query-form-bluebook.md's note on
87
136
  # `RefusalWording`), so this returns empty rather than guessing which
88
137
  # field a message meant; the banner above carries the real text
89
138
  # instead of a misattributed hint.
139
+ #
140
+ # @param _error [Exception, nil] the refusal raised by the submission; ignored
141
+ # @return [Hash] always empty; keys would be dotted field paths
90
142
  def self.field_errors(_error) = {}
91
143
 
144
+ # Renders the collapsed "Inspect" panel: the events the command emits, an equivalent
145
+ # `curl` request, its field paths, and its declaration as JSON.
146
+ #
147
+ # @param domain [String] name of the domain (chapter) the aggregate belongs to
148
+ # @param aggregate [Bluebook::Aggregate] the aggregate that declares the command
149
+ # @param command [Bluebook::Command] the command being described
150
+ # @param action [String] URL path the form posts to, shown in the `curl` line
151
+ # @param fields [Array<Forms::Field>] the form's fields, as `fields_for` returns them
152
+ # @return [String] HTML for the `<details>` panel
92
153
  def self.inspect_panel(domain, aggregate, command, action, fields)
93
154
  verb = "#{domain}::#{aggregate.hecks_name}.#{command.hecks_name}"
94
155
  paths = Params.paths(fields)