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
@@ -11,8 +11,14 @@ module Hecks
11
11
  # account number a support rep typed into a form and got wrong — flows
12
12
  # through here on every render, so escaping is not optional decoration.
13
13
  module Escape
14
+ # Escapes a value for HTML text content, replacing `&`, `<`, `>`, `"` and `'` with
15
+ # entities so nothing in it becomes a tag.
16
+ #
14
17
  # Order matters — `&` first, or every escape this method itself just
15
18
  # wrote (`&amp;`, `&lt;`, ...) gets re-escaped a second time.
19
+ #
20
+ # @param value [Object, nil] the value to show, rendered with `to_s`; nil renders as `""`
21
+ # @return [String] the escaped text
16
22
  def self.html(value)
17
23
  value.to_s
18
24
  .gsub("&", "&amp;")
@@ -22,14 +28,22 @@ module Hecks
22
28
  .gsub("'", "&#39;")
23
29
  end
24
30
 
31
+ # Escapes a value for use inside a quoted HTML attribute.
32
+ #
25
33
  # Safe inside a double-quoted HTML attribute specifically — `html`
26
34
  # already covers this (it escapes `"`), kept as a named alias so a
27
35
  # call site reads "this value fills an attribute" rather than repeating
28
36
  # the same escaping and leaving the reader to check they match.
37
+ #
38
+ # @param value [Object, nil] the attribute value, rendered with `to_s`; nil renders
39
+ # as `""`
40
+ # @return [String] the escaped text, without surrounding quotes
29
41
  def self.attr(value) = html(value)
30
42
 
43
+ # Percent-encodes a value for use as a query-string value.
44
+ #
31
45
  # L12 (docs/audits/2026-08-10-main-bug-audit.md) — safe as a
32
- # query-string VALUE. `html`/`attr` guard against the value becoming
46
+ # query-string value. `html`/`attr` guard against the value becoming
33
47
  # markup, but say nothing about it staying inside the URL syntax
34
48
  # position it was placed in: an aggregate's identity is free-form
35
49
  # unless its value object declares a `pattern:` (see S3 in the same
@@ -38,23 +52,32 @@ module Hecks
38
52
  # interpolation (a stray `&` smuggles a second query parameter, `#`
39
53
  # truncates the path at a fragment, `/` splits the path into an
40
54
  # extra segment, ...). Percent-encodes via
41
- # `application/x-www-form-urlencoded` (`+` for space) — correct ONLY
55
+ # `application/x-www-form-urlencoded` (`+` for space) — correct only
42
56
  # for a query-string value (query_form_renderer.rb's `quick_links`,
43
- # record_renderer.rb's `?to=`). For a URL PATH segment use `path`
57
+ # record_renderer.rb's `?to=`). For a URL path segment use `path`
44
58
  # below instead — `+` is a literal plus there, not an escaped space,
45
59
  # so this method would corrupt any id containing a space. Callers
46
- # still wrap the ASSEMBLED href/Location in `attr` (or `html`) as
60
+ # still wrap the assembled href/Location in `attr` (or `html`) as
47
61
  # usual — this only covers the id's own component, not the
48
62
  # surrounding markup.
63
+ #
64
+ # @param value [Object, nil] the value to encode, rendered with `to_s`
65
+ # @return [String] the `application/x-www-form-urlencoded` form, a space rendered as `+`
49
66
  def self.url(value) = URI.encode_www_form_component(value.to_s)
50
67
 
51
- # Same guard as `url`, for a URL PATH segment instead of a
68
+ # Percent-encodes a value for use as one URL path segment.
69
+ #
70
+ # Same guard as `url`, for a URL path segment instead of a
52
71
  # query-string value. `encode_www_form_component` renders space as
53
72
  # `+`, which is only meaningful inside a query string — in a path
54
73
  # segment `+` is a literal plus, so an id like "John Smith" would
55
74
  # round-trip to "John+Smith" and 404 against the real id "John
56
75
  # Smith". Reuse the same percent-encoding and just correct that one
57
76
  # character back to `%20`.
77
+ #
78
+ # @param value [Object, nil] the segment to encode, rendered with `to_s`
79
+ # @return [String] the percent-encoded segment, a space rendered as `%20` and `/` as
80
+ # `%2F`
58
81
  def self.path(value) = URI.encode_www_form_component(value.to_s).gsub("+", "%20")
59
82
  end
60
83
 
@@ -64,6 +87,14 @@ module Hecks
64
87
  # `true` renders as a bare boolean attribute (`required`, not
65
88
  # `required="true"`); `nil`/`false` are dropped entirely.
66
89
  module Tag
90
+ # Renders a Hash of attributes as the text that sits inside an opening tag, escaping
91
+ # every value and spelling an underscored name with hyphens.
92
+ #
93
+ # @param pairs [Hash{Symbol => Object}] attribute values by name; `true`
94
+ # renders the bare name, nil and `false` drop the attribute, anything else is
95
+ # rendered with `to_s`
96
+ # @return [String] space-separated attributes, such as `required aria-describedby="x"`;
97
+ # `""` when every pair is dropped
67
98
  def self.attrs(pairs)
68
99
  pairs.filter_map do |name, value|
69
100
  next if value.nil? || value == false
@@ -73,15 +104,28 @@ module Hecks
73
104
  end.join(" ")
74
105
  end
75
106
 
107
+ # Renders an opening tag with its attributes. The tag name is interpolated unescaped,
108
+ # so it must come from code, never from domain data.
109
+ #
110
+ # @param name [String] the element name, such as `"input"`
111
+ # @param pairs [Hash{Symbol => Object}] attributes, rendered as `attrs` renders them
112
+ # @return [String] the opening tag, such as `<input type="text" required>`
76
113
  def self.open(name, **pairs)
77
114
  rendered = attrs(pairs)
78
115
  rendered.empty? ? "<#{name}>" : "<#{name} #{rendered}>"
79
116
  end
80
117
 
81
- # `self.open(...)`, not bare `open(...)` this Html.open (an HTML tag
118
+ # Renders a void element such as `<input>`. A self-closing tag reads the same as an
119
+ # opening one; HTML5 needs no slash.
120
+ #
121
+ # `self.open(...)`, not bare `open(...)` — this `Tag.open` (an HTML tag
82
122
  # renderer, right above) shadows Kernel#open safely either way, but the
83
123
  # explicit receiver also settles Security/Open's static ambiguity.
84
- def self.void(name, **pairs) = self.open(name, **pairs) # self-closing tags read the same; HTML5 needs no slash
124
+ #
125
+ # @param name [String] the element name, such as `"input"`
126
+ # @param pairs [Hash{Symbol => Object}] attributes, rendered as `attrs` renders them
127
+ # @return [String] the tag, identical to what `open` renders
128
+ def self.void(name, **pairs) = self.open(name, **pairs)
85
129
  end
86
130
  end
87
131
  end
@@ -4,9 +4,15 @@ module Hecks
4
4
  module Forms
5
5
  # The home page: every exposed chapter, every aggregate on it — the
6
6
  # entry point into what would otherwise be a URL you'd have to already
7
- # know. `chapters` is `{domain_name => Bluebook}`, in `expose` order
8
- # (see config.rb).
7
+ # know. `chapters` is `{domain_name => Bluebook::Chapter}`, in `expose`
8
+ # order (see `Forms::Config` in forms.rb).
9
9
  module IndexRenderer
10
+ # Renders the home page body: one section per exposed chapter, each linking to its
11
+ # aggregates.
12
+ #
13
+ # @param chapters [Hash{String => Bluebook::Chapter}] the loaded chapters by domain
14
+ # name, in `expose` order
15
+ # @return [String] the HTML page body; only the heading when `chapters` is empty
10
16
  def self.render(chapters)
11
17
  sections = chapters.map { |name, chapter| chapter_section(name, chapter) }
12
18
  <<~HTML
@@ -15,6 +21,12 @@ module Hecks
15
21
  HTML
16
22
  end
17
23
 
24
+ # Renders one chapter's section: its name, its vision as a badge when it declares one,
25
+ # and a link per aggregate showing how many commands and queries it has.
26
+ #
27
+ # @param name [String] the domain name, used as the heading and the first path segment
28
+ # @param chapter [Bluebook::Chapter] the loaded chapter
29
+ # @return [String] HTML for the section's heading and aggregate list
18
30
  def self.chapter_section(name, chapter)
19
31
  items = chapter.aggregates.map do |aggregate|
20
32
  counts = "#{aggregate.commands.size} command#{'s' unless aggregate.commands.size == 1}, " \
@@ -4,35 +4,59 @@ require_relative "field_renderer"
4
4
 
5
5
  module Hecks
6
6
  module Forms
7
- # The two directions between a flat, dotted, ALL-STRINGS web payload
7
+ # The two directions between a flat, dotted, all-strings web payload
8
8
  # (`{"amount.cents"=>"1050", "amount.currency"=>"USD"}`, whether it came
9
9
  # off a POST form body or a GET query string — Rack hands back the same
10
10
  # flat shape for both as long as nothing uses `[]` bracket names) and the
11
11
  # nested, typed hash `Dispatcher#dispatch`/`#query` actually take
12
12
  # (`{amount: {cents: 1050, currency: "USD"}}`).
13
13
  #
14
- # `extract` NEEDS the Field tree, not just the raw params — a numeric
14
+ # `extract` needs the Field tree, not just the raw params — a numeric
15
15
  # leaf's own runtime check (`Value::Coercion#check_numeric_fields`)
16
16
  # requires an actual `Integer`/`Float`, not a String that merely looks
17
17
  # like one (`given.is_a?(expected)`, no coercion attempted there); a web
18
18
  # form can only ever hand back strings, so the cast has to happen here,
19
- # once, using the SAME shape `FieldShape` already resolved for
19
+ # once, using the same shape `FieldShape` already resolved for
20
20
  # rendering the input in the first place — one reading of the IR, not
21
21
  # two that could disagree.
22
22
  module Params
23
- # Every LEAF field carries its own full dotted path regardless of how
23
+ # Casts a flat, all-strings web payload into the nested, typed arguments a command or
24
+ # query takes, guided by the field tree the form was rendered from.
25
+ #
26
+ # Every leaf field carries its own full dotted path regardless of how
24
27
  # deep `FieldShape` nested it to get there — a single-attribute value
25
- # object unwraps to a leaf sitting at the TOP of the fields array with
28
+ # object unwraps to a leaf sitting at the top of the fields array with
26
29
  # a two-segment path (`"reference.value"`), the exact same shape a
27
30
  # `:group`'s own child carries. So this collects every leaf as
28
- # (full path -> value) FIRST, and nests by the path's OWN segments
31
+ # (full path -> value) first, and nests by the path's own segments
29
32
  # last — one nesting rule, blind to how a field arrived at its path.
33
+ #
34
+ # @param fields [Array<Forms::Field>] the resolved field tree the payload was posted
35
+ # against
36
+ # @param raw [Hash{String => String}] the flat web payload, keyed by dotted field path
37
+ # @return [Hash{Symbol => Object}] nested, typed arguments keyed by path segment, ready
38
+ # to splat into a dispatch; a blank optional field is left out
39
+ # @raise [ArgumentError] if a number field's text is not numeric, or two fields collide
40
+ # at one path (see `nest`)
41
+ # @raise [TypeError] if a required number field is missing from `raw`
42
+ # @raise [JSON::ParserError] if a line of a list-of-value-objects field is not JSON
30
43
  def self.extract(fields, raw)
31
44
  pairs = {}
32
45
  fields.each { |field| collect(field, raw, pairs) }
33
46
  nest(pairs)
34
47
  end
35
48
 
49
+ # Gathers one field's typed value, and recursively its children's, into `pairs` under
50
+ # each leaf's full dotted path; a blank optional leaf adds nothing.
51
+ #
52
+ # @param field [Forms::Field] the field to read
53
+ # @param raw [Hash{String => String}] the flat web payload, keyed by dotted field path
54
+ # @param pairs [Hash{String => Object}] accumulator of typed values by dotted path,
55
+ # added to in place
56
+ # @return [void]
57
+ # @raise [ArgumentError] if a number field's text is not numeric
58
+ # @raise [TypeError] if a required number field is missing from `raw`
59
+ # @raise [JSON::ParserError] if a line of a list-of-value-objects field is not JSON
36
60
  def self.collect(field, raw, pairs)
37
61
  case field.kind
38
62
  when :group, :money then field.children.each { |child| collect(child, raw, pairs) }
@@ -41,23 +65,32 @@ module Hecks
41
65
  end
42
66
  end
43
67
 
68
+ # Nests values keyed by dotted path into a Hash keyed by path segment, refusing two
69
+ # paths that cannot share one result.
70
+ #
44
71
  # A path-prefix collision: one field named (say) "price" alongside
45
- # another named "price.cents" implies "price" is BOTH a scalar leaf
72
+ # another named "price.cents" implies "price" is both a scalar leaf
46
73
  # and the parent of a nested group — the two can never coexist in
47
74
  # the same result hash. Depending on which pair `each_with_object`
48
- # reaches first, the naive walk below used to fail in one of two
49
- # ways: a scalar planted first left `acc[segment] ||= {}` seeing a
50
- # truthy non-Hash and reusing IT as `node`, so the next `node[leaf] =
51
- # value` blew up with a raw `TypeError` from calling `String#[]=`
52
- # with a Symbol key; a scalar planted AFTER the nested group instead
53
- # sailed through `node[leaf] = value` and silently clobbered the
54
- # entire nested hash with the scalar, losing every sibling under it
55
- # with no error at all. Both directions are checked explicitly here
56
- # so either order raises the SAME clear `ArgumentError` instead of a
57
- # confusing crash or silent data loss — this is the family of error
58
- # every command/query submission path in app.rb already rescues into
59
- # a 422 (`ArgumentError` sits right alongside the domain refusals in
60
- # every one of those rescue clauses).
75
+ # reaches first, an unguarded walk fails in one of two ways: a scalar
76
+ # planted first leaves `acc[segment] ||= {}` seeing a truthy non-Hash
77
+ # and reusing it as `node`, so the next `node[leaf] = value` blows up
78
+ # with a raw `TypeError` from calling `String#[]=` with a Symbol key;
79
+ # a scalar planted after the nested group instead sails through
80
+ # `node[leaf] = value` and silently clobbers the entire nested hash
81
+ # with the scalar, losing every sibling under it with no error at
82
+ # all. Both directions are checked explicitly here so either order
83
+ # raises the same clear `ArgumentError` instead of a confusing crash
84
+ # or silent data loss — this is the family of error every
85
+ # command/query submission path in app.rb already rescues into a 422
86
+ # (`ArgumentError` sits right alongside the domain refusals in every
87
+ # one of those rescue clauses).
88
+ #
89
+ # @param pairs [Hash{String => Object}] values keyed by dotted path, such as
90
+ # `{"amount.cents" => 1050}`
91
+ # @return [Hash{Symbol => Object}] the nested result, such as `{amount: {cents: 1050}}`
92
+ # @raise [ArgumentError] if one path names a plain value where another names a nested
93
+ # group, in either order
61
94
  def self.nest(pairs)
62
95
  pairs.each_with_object({}) do |(path, value), result|
63
96
  segments = path.to_s.split(".").map(&:to_sym)
@@ -74,6 +107,10 @@ module Hecks
74
107
  end
75
108
  end
76
109
 
110
+ # Builds, without raising, the error `nest` raises for a path-prefix collision.
111
+ #
112
+ # @param path [String] the dotted path at which the collision was found
113
+ # @return [ArgumentError] an error whose message names the conflicting path
77
114
  def self.nesting_collision(path)
78
115
  ArgumentError.new("#{path.inspect} conflicts with another field at the same path — " \
79
116
  "one names it as a plain value and another as a nested group")
@@ -82,11 +119,21 @@ module Hecks
82
119
  SKIP = Object.new.freeze
83
120
  private_constant :SKIP
84
121
 
122
+ # Reads a `:list` field's textarea into an Array, one element per non-blank line.
123
+ #
85
124
  # One line of the textarea per element. A line that itself needs
86
125
  # several fields (a multi-attribute value object as a list element)
87
126
  # is read as JSON on that one line — the honest fallback documented in
88
127
  # docs/command-form-and-query-form-bluebook.md rather than a second
89
128
  # widget this prototype doesn't build yet.
129
+ #
130
+ # @param field [Forms::Field] a `:list` field whose first child describes one element
131
+ # @param raw [Hash{String => String}] the flat web payload, keyed by dotted field path
132
+ # @return [Array<Object>, Object] the elements — cast scalars for a leaf element,
133
+ # symbol-keyed parsed JSON otherwise — or the private `SKIP` sentinel when the field
134
+ # is absent or blank
135
+ # @raise [ArgumentError] if a number element's line is not numeric
136
+ # @raise [JSON::ParserError] if a non-leaf element's line is not JSON
90
137
  def self.extract_list(field, raw)
91
138
  text = raw[field.path]
92
139
  return SKIP if text.nil? || text.strip.empty?
@@ -97,6 +144,16 @@ module Hecks
97
144
  end
98
145
  end
99
146
 
147
+ # Reads one leaf field's typed value out of the payload; a boolean always answers, since
148
+ # an unticked checkbox is itself a value.
149
+ #
150
+ # @param field [Forms::Field] a leaf field
151
+ # @param raw [Hash{String => String}] the flat web payload, keyed by dotted field path
152
+ # @return [Boolean, Integer, Float, String, nil, Object] the cast value; nil when a
153
+ # required non-number field is missing; the private `SKIP` sentinel when an optional
154
+ # field is absent or empty
155
+ # @raise [ArgumentError] if a number field's text is not numeric
156
+ # @raise [TypeError] if a required number field is missing from `raw`
100
157
  def self.extract_leaf(field, raw)
101
158
  return checkbox(raw[field.path]) if field.kind == :boolean
102
159
 
@@ -106,10 +163,24 @@ module Hecks
106
163
  cast_scalar(field, text)
107
164
  end
108
165
 
166
+ # Reads a submitted checkbox value as a Boolean.
167
+ #
168
+ # @param raw_value [String, nil] the submitted text; nil when the field was not posted
169
+ # @return [Boolean] true for `"on"`, `"1"` or `"true"` in any letter case, false for
170
+ # anything else
109
171
  def self.checkbox(raw_value)
110
172
  %w[on 1 true].include?(raw_value.to_s.downcase)
111
173
  end
112
174
 
175
+ # Casts a leaf's submitted text to the Ruby type the runtime's numeric check requires,
176
+ # which accepts an actual `Integer` or `Float` and never a numeric-looking String.
177
+ #
178
+ # @param field [Forms::Field] the leaf the text was submitted for
179
+ # @param text [String, nil] the submitted text
180
+ # @return [Integer, Float, String, nil] an Integer for a `:number` field with `step`
181
+ # `"1"`, a Float for any other `:number` field, and `text` itself for every other kind
182
+ # @raise [ArgumentError] if a number field's text is not numeric
183
+ # @raise [TypeError] if a number field's text is nil
113
184
  def self.cast_scalar(field, text)
114
185
  case field.kind
115
186
  when :number then field.step == "1" ? Integer(text) : Float(text)
@@ -117,13 +188,23 @@ module Hecks
117
188
  end
118
189
  end
119
190
 
191
+ # Flattens held values back into the dotted-path, all-strings pairs a query string
192
+ # carries — the reverse of `extract`.
193
+ #
120
194
  # The other direction — a nested value hash back to the flat dotted
121
195
  # pairs a GET link's query string carries, so a query view's
122
- # "shareable link" and its filter FORM stay two renderings of the
123
- # SAME data rather than two formats that can drift. Reads with
196
+ # "shareable link" and its filter form stay two renderings of the
197
+ # same data rather than two formats that can drift. Reads with
124
198
  # `FieldRenderer.dig` — the identical full-path lookup a re-rendered
125
199
  # input's own value comes from — for the same reason `extract` above
126
200
  # nests by full path rather than by tree shape.
201
+ #
202
+ # @param fields [Array<Forms::Field>] the field tree naming which paths to read
203
+ # @param values [Hash{String, Symbol => Object}] held values, flat or nested; see
204
+ # `FieldRenderer.dig`
205
+ # @param into [Hash{String => String}] accumulator the pairs are added to in place
206
+ # @return [Hash{String => String}] `into`, holding one pair per leaf with a non-nil value
207
+ # and one per list (its elements newline-joined, `""` when it holds nothing)
127
208
  def self.flatten(fields, values, into: {})
128
209
  fields.each do |field|
129
210
  case field.kind
@@ -137,11 +218,19 @@ module Hecks
137
218
  into
138
219
  end
139
220
 
221
+ # Lists every dotted path a field tree submits under, whether or not anything was
222
+ # entered for it.
223
+ #
140
224
  # Every leaf/list path a field tree carries, independent of any
141
225
  # values — what `command_form_renderer.rb`'s inspect panel wants
142
226
  # ("which fields does this command take"), where `flatten` above wants
143
- # "what does THIS submission look like" and returns nothing for a
227
+ # "what does this one submission look like" and returns nothing for a
144
228
  # field nothing was entered for.
229
+ #
230
+ # @param fields [Array<Forms::Field>] the field tree to walk
231
+ # @param into [Array<String>] accumulator the paths are appended to in place
232
+ # @return [Array<String>] `into`, holding each leaf and list path in render order;
233
+ # `:group` and `:money` fields contribute their children's paths, not their own
145
234
  def self.paths(fields, into: [])
146
235
  fields.each do |field|
147
236
  case field.kind
@@ -152,6 +241,14 @@ module Hecks
152
241
  into
153
242
  end
154
243
 
244
+ # Renders held values as a URL query string, percent-encoding each flattened path and
245
+ # value.
246
+ #
247
+ # @param fields [Array<Forms::Field>] the field tree naming which paths to read
248
+ # @param values [Hash{String, Symbol => Object}] held values, flat or nested; see
249
+ # `FieldRenderer.dig`
250
+ # @return [String] `key=value` pairs joined with `&`, without a leading `?`; `""` when
251
+ # `flatten` yields no pairs
155
252
  def self.to_query_string(fields, values)
156
253
  pairs = flatten(fields, values)
157
254
  pairs.map { |key, value| "#{URI.encode_www_form_component(key)}=#{URI.encode_www_form_component(value)}" }.join("&")
@@ -5,13 +5,13 @@ module Hecks
5
5
  # boot. Two spellings the previous inline version got wrong:
6
6
  #
7
7
  # --port=8080 the equals form — the old `ARGV.each_cons(2)` scan
8
- # only ever recognized "--port", "8080" as TWO
8
+ # only ever recognized "--port", "8080" as two
9
9
  # separate argv entries, so this spelling matched
10
10
  # nothing and silently fell through to the default.
11
11
  # -p abc a non-numeric value — the old code did `.to_i` on
12
12
  # whatever followed unconditionally, so a typo
13
13
  # quietly became port 0 (Rackup/WEBrick's actual
14
- # behavior for `Port: 0` is to bind an EPHEMERAL
14
+ # behavior for `Port: 0` is to bind an ephemeral
15
15
  # port — arguably useful on purpose elsewhere, but
16
16
  # never what a mistyped `-p abc` meant to ask for).
17
17
  #
@@ -16,7 +16,7 @@ module Hecks
16
16
  # hiding that behind a form); a row of ready-made links for any
17
17
  # closed-set parameter, since a caller filtering by an enum should not
18
18
  # have to fill in a form to get a link they could just click; a filter
19
- # FORM for everything else, which — being a GET form — produces
19
+ # form for everything else, which — being a GET form — produces
20
20
  # exactly the same kind of link on submit; and the results table once
21
21
  # a request actually supplies parameters.
22
22
  module QueryFormRenderer
@@ -60,7 +60,7 @@ module Hecks
60
60
 
61
61
  # The one enum-shaped parameter (if there is one) rendered as literal,
62
62
  # clickable links — no form to fill in for "show me the suspended
63
- # ones". Capped at the FIRST closed-set field on purpose: a second one
63
+ # ones". Capped at the first closed-set field on purpose: a second one
64
64
  # would mean a cross product of links, which reads as noise rather
65
65
  # than help. A query with more than one is still fully reachable
66
66
  # through the filter form below.
@@ -68,7 +68,7 @@ module Hecks
68
68
  end
69
69
  end
70
70
 
71
- # Every non-creating command, EXCEPT a lifecycle transition that does
71
+ # Every non-creating command, except a lifecycle transition that does
72
72
  # not apply from the record's current state — the same rule
73
73
  # `Rules#admissible_transition` enforces at dispatch, read here so a
74
74
  # link that would only refuse is never offered in the first place.
@@ -78,7 +78,7 @@ module Hecks
78
78
 
79
79
  items = commands.map do |cmd|
80
80
  # L12 — the id is free-form (S3): percent-encoded as the query
81
- # VALUE (a raw `&` here would smuggle a second bogus query
81
+ # value (a raw `&` here would smuggle a second bogus query
82
82
  # parameter), then the assembled href is attribute-escaped as
83
83
  # usual.
84
84
  href = "/#{domain}/#{aggregate.hecks_name}/#{cmd.hecks_name}.html?to=#{Escape.url(id)}"
@@ -5,7 +5,7 @@ module Hecks
5
5
  module Forms
6
6
  # The one shape every renderer in this directory reads a record as —
7
7
  # `id` plus its state hash. `Runtime::Instance` already answers both,
8
- # so `repository.all`'s own records pass straight through; a QUERY's
8
+ # so `repository.all`'s own records pass straight through; a query's
9
9
  # answer does not (`QueryInterpreter#call` flattens `{id:}.merge(state)`
10
10
  # into one hash with no method to call — see
11
11
  # docs/command-form-and-query-form-bluebook.md's note on why), so
@@ -1,11 +1,11 @@
1
1
  module Hecks
2
2
  module Forms
3
- # THE VALUE-OBJECT CLASSIFICATION every consumer of a resolved VO
3
+ # The value-object classification every consumer of a resolved VO
4
4
  # shape needs, spelled once — `field_shape.rb` (a command's own
5
5
  # form), `ui_schema.rb` (embryonaut_console's table/detail-panel
6
6
  # renderer, a separate app, not this repo), and
7
7
  # `adapters/driven/sql_query_builder.rb` (a query's own ORDER BY/
8
- # WHERE compiler) each grew their own copy of "is this VO money-
8
+ # where compiler) each grew their own copy of "is this VO money-
9
9
  # shaped," "does it have exactly one attribute, worth unwrapping,"
10
10
  # and "which member of it is numeric" — the same three questions,
11
11
  # answered identically by definition (a VO's shape doesn't change
@@ -22,7 +22,7 @@ module Hecks
22
22
  value_object.attributes.map { |a| a.name.to_s }.sort == %w[cents currency]
23
23
  end
24
24
 
25
- # A VO with exactly one attribute is a NAME for a scalar, not a
25
+ # A VO with exactly one attribute is a name for a scalar, not a
26
26
  # genuine group (EmailAddress{address}, CustomerNumber{value}) —
27
27
  # [[feedback_name_the_scalar_field]]'s own reasoning, shared here
28
28
  # rather than re-decided per caller. Returns the sole attribute,
data/lib/hecks/forms.rb CHANGED
@@ -2,7 +2,7 @@
2
2
  # `query_form.bluebook` will eventually become — see docs/
3
3
  # command-form-and-query-form-bluebook.md), the IR->HTML renderers, and
4
4
  # the Rack app that content-negotiates between them and a plain JSON
5
- # reading of the same dispatch. NOT required by `require "hecks"`
5
+ # reading of the same dispatch. Not required by `require "hecks"`
6
6
  # itself — a project that never boots this file never pays for `rack`,
7
7
  # the same lazy-dependency discipline the Gemfile's own comment already
8
8
  # holds `pg`/`oauth2`/`aws-sdk-lambda` to.
@@ -19,7 +19,7 @@ module Hecks
19
19
  # `expose` deliberately does and does not cover.
20
20
  module Forms
21
21
  # `expose`'s own declaration — "which chapters does this app expose" —
22
- # kept OUTSIDE the `Hecks.*` collector convention (`Hecks.bluebook`,
22
+ # kept outside the `Hecks.*` collector convention (`Hecks.bluebook`,
23
23
  # `Hecks.hecksagon`, ...) and outside `Runtime::Registry` entirely, on
24
24
  # purpose: a real language word goes through `syntax.bluebook` and
25
25
  # `MetaValidator` (see docs/implemented/guides/extending-hecks.md, "a new word is a
@@ -30,29 +30,49 @@ module Hecks
30
30
  # see docs/command-form-and-query-form-bluebook.md, "why this isn't
31
31
  # syntax.bluebook yet".
32
32
  #
33
- # ONE `expose` GRANTS A WHOLE CHAPTER, not a command or a query
33
+ # One `expose` grants a whole chapter, not a command or a query
34
34
  # individually — the future `command_form.bluebook`/`query_form.bluebook`
35
- # words are PER-DECLARATION (one command, one query, its own form/view,
35
+ # words are per-declaration (one command, one query, its own form/view,
36
36
  # possibly its own overrides), which `expose` doesn't do today and was
37
37
  # never trying to; it's the coarse "turn this chapter's forms on at
38
38
  # all" switch those finer words will eventually sit inside.
39
39
  class Config
40
40
  attr_reader :name, :exposes
41
41
 
42
+ # @param name [String, Symbol] the app name this configuration is registered under;
43
+ # stored as a String
42
44
  def initialize(name)
43
45
  @name = name.to_s
44
46
  @exposes = []
45
47
  end
46
48
 
49
+ # Switches on the forms of one whole chapter for this app; called inside the
50
+ # `Forms.configure` block.
51
+ #
52
+ # @param chapter_name [String, Symbol] name of the chapter (domain) to expose, such as
53
+ # `"Banking"`; stored as a String
54
+ # @return [Array<String>] every chapter name exposed so far, in declaration order
47
55
  def expose(chapter_name) = @exposes << chapter_name.to_s
48
56
  end
49
57
 
58
+ # Declares an app and the chapters it exposes, replacing any earlier configuration
59
+ # registered under the same name.
60
+ #
61
+ # @param name [String, Symbol] the app name `App.for` later looks the configuration up by
62
+ # @yield evaluated with `instance_eval` against the new `Forms::Config`, so a bare
63
+ # `expose "Banking"` inside the block reaches `Config#expose`; optional
64
+ # @return [Forms::Config] the configuration just registered
50
65
  def self.configure(name, &block)
51
66
  config = Config.new(name)
52
67
  config.instance_eval(&block) if block
53
68
  (@configs ||= {})[config.name] = config
54
69
  end
55
70
 
71
+ # Looks up the configuration an earlier `Forms.configure` registered.
72
+ #
73
+ # @param name [String, Symbol] the app name given to `Forms.configure`
74
+ # @return [Forms::Config, nil] the app's configuration, or nil when no app of that name
75
+ # has been configured
56
76
  def self.config(name) = (@configs || {})[name.to_s]
57
77
  end
58
78
  end
data/lib/hecks/fqn.rb CHANGED
@@ -16,7 +16,7 @@ module Hecks
16
16
  new(realm: realm, domain: domain, version: version, aggregate: aggregate, verb: query, kind: :query)
17
17
  end
18
18
 
19
- # ONE ORDER-DEPENDENT PARSE PIPELINE: split -> shape-validate -> dispatch
19
+ # One order-dependent parse pipeline: split -> shape-validate -> dispatch
20
20
  # on segment count -> split domain/version -> classify kind -> cross-
21
21
  # field validate -> construct. Each step consumes locals (segments, verb,
22
22
  # kind) the step before it derived; splitting would mean threading all of
@@ -2,6 +2,15 @@ Hecks.bluebook "Governance" do
2
2
  vision "Organizational authority is explicit, reviewable, and historically explainable."
3
3
  supporting
4
4
 
5
+ # WHAT THIS CHAPTER ANSWERS FOR EVERY DOMAIN THAT ATTACHES IT. The role
6
+ # check at dispatch, boot's refusal of an ungoverned `role`, the
7
+ # authorization adapter and the fuzzer's grant steering all read these
8
+ # three verbs from here, rather than recognising "Governance" by name.
9
+ provides "authorization",
10
+ assignments: "RoleAssignment.AssignmentsForActor",
11
+ grant: "RoleAssignment.Assign",
12
+ transitions: "RoleTransition.Allowed"
13
+
5
14
  aggregate "RoleAssignment" do
6
15
  description "One actor holding one role, over one scope, from one point in time — revoked by setting when it ended, never deleted."
7
16