hecks 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (373) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +72 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +83 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
  7. data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
  9. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  10. data/lib/hecks/adapters/driven/heki.rb +106 -9
  11. data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
  12. data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
  13. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  15. data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
  16. data/lib/hecks/adapters/driven/lambda.rb +82 -35
  17. data/lib/hecks/adapters/driven/local_storage.rb +83 -10
  18. data/lib/hecks/adapters/driven/memory.rb +205 -9
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  24. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  25. data/lib/hecks/adapters/driven/prism.rb +50 -11
  26. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  31. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  32. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  33. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  34. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  35. data/lib/hecks/adapters/driven.rb +6 -4
  36. data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
  37. data/lib/hecks/behaviors/dsl.rb +60 -2
  38. data/lib/hecks/behaviors/expectations.rb +190 -29
  39. data/lib/hecks/behaviors/ir.rb +12 -1
  40. data/lib/hecks/behaviors/rspec.rb +9 -1
  41. data/lib/hecks/behaviors/runner.rb +21 -2
  42. data/lib/hecks/behaviors.rb +9 -1
  43. data/lib/hecks/bluebook/aggregate.rb +43 -13
  44. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
  45. data/lib/hecks/bluebook/assembly/build.rb +26 -5
  46. data/lib/hecks/bluebook/assembly/contract.rb +98 -23
  47. data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
  48. data/lib/hecks/bluebook/assembly/marks.rb +159 -30
  49. data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
  50. data/lib/hecks/bluebook/assembly.rb +32 -14
  51. data/lib/hecks/bluebook/attribute.rb +26 -12
  52. data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
  53. data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
  54. data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
  55. data/lib/hecks/bluebook/behaviour/command.rb +55 -25
  56. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
  57. data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
  58. data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
  59. data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
  60. data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
  61. data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
  62. data/lib/hecks/bluebook/behaviour/query.rb +6 -1
  63. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
  64. data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
  65. data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
  66. data/lib/hecks/bluebook/capabilities.rb +27 -0
  67. data/lib/hecks/bluebook/chapter.rb +51 -9
  68. data/lib/hecks/bluebook/command.rb +62 -17
  69. data/lib/hecks/bluebook/domain_port.rb +34 -9
  70. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  71. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  72. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
  73. data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
  74. data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
  75. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
  76. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
  77. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  78. data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
  79. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  80. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  81. data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
  82. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  83. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
  84. data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
  85. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  86. data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
  87. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  88. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  89. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
  90. data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
  91. data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
  92. data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
  93. data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
  94. data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
  95. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  96. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  97. data/lib/hecks/bluebook/entity.rb +40 -11
  98. data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
  99. data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
  100. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
  101. data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
  102. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
  103. data/lib/hecks/bluebook/expression/resolver.rb +369 -128
  104. data/lib/hecks/bluebook/hexagon.rb +35 -1
  105. data/lib/hecks/bluebook/lifecycle.rb +12 -1
  106. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
  107. data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
  108. data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
  109. data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
  110. data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
  111. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
  112. data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
  113. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
  114. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
  115. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
  116. data/lib/hecks/bluebook/meta_validator.rb +235 -139
  117. data/lib/hecks/bluebook/model_check.rb +434 -104
  118. data/lib/hecks/bluebook/pattern_subset.rb +32 -10
  119. data/lib/hecks/bluebook/policy.rb +15 -13
  120. data/lib/hecks/bluebook/process_manager.rb +27 -14
  121. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  122. data/lib/hecks/bluebook/project_loader.rb +40 -0
  123. data/lib/hecks/bluebook/project_register.rb +50 -6
  124. data/lib/hecks/bluebook/query.rb +31 -4
  125. data/lib/hecks/bluebook/read_model.rb +35 -15
  126. data/lib/hecks/bluebook/reference.rb +26 -13
  127. data/lib/hecks/bluebook/smoke_test.rb +46 -23
  128. data/lib/hecks/bluebook/synthesizer.rb +46 -12
  129. data/lib/hecks/bluebook/translation.rb +34 -5
  130. data/lib/hecks/bluebook/value_object.rb +29 -11
  131. data/lib/hecks/bluebook.rb +5 -6
  132. data/lib/hecks/codemod.rb +138 -50
  133. data/lib/hecks/construct.rb +21 -7
  134. data/lib/hecks/corpus.rb +438 -0
  135. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  136. data/lib/hecks/doc/reference.rb +200 -31
  137. data/lib/hecks/embryonaut_bluebook.rb +38 -15
  138. data/lib/hecks/facade/cli_door.rb +69 -10
  139. data/lib/hecks/facade/cli_runner.rb +105 -24
  140. data/lib/hecks/facade/command_request.rb +23 -0
  141. data/lib/hecks/facade/handle.rb +155 -35
  142. data/lib/hecks/facade/json_door.rb +106 -25
  143. data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
  144. data/lib/hecks/facade/surface/chapter.rb +26 -17
  145. data/lib/hecks/facade/surface.rb +16 -3
  146. data/lib/hecks/facade.rb +15 -4
  147. data/lib/hecks/forms/app.rb +46 -30
  148. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  149. data/lib/hecks/forms/field_renderer.rb +142 -6
  150. data/lib/hecks/forms/field_shape.rb +183 -19
  151. data/lib/hecks/forms/html.rb +51 -7
  152. data/lib/hecks/forms/index_renderer.rb +14 -2
  153. data/lib/hecks/forms/page.rb +14 -0
  154. data/lib/hecks/forms/params.rb +120 -23
  155. data/lib/hecks/forms/port_argument.rb +14 -2
  156. data/lib/hecks/forms/query_form_renderer.rb +65 -2
  157. data/lib/hecks/forms/record_renderer.rb +60 -2
  158. data/lib/hecks/forms/record_table.rb +28 -1
  159. data/lib/hecks/forms/reference_options.rb +24 -0
  160. data/lib/hecks/forms/value_object_shape.rb +13 -3
  161. data/lib/hecks/forms.rb +24 -4
  162. data/lib/hecks/fqn.rb +59 -1
  163. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  164. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  165. data/lib/hecks/framework/oidc.json +15 -0
  166. data/lib/hecks/framework.rb +79 -25
  167. data/lib/hecks/freezer.rb +27 -11
  168. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
  169. data/lib/hecks/fuzzing/combination_miner.rb +178 -0
  170. data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
  171. data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
  172. data/lib/hecks/fuzzing/differential.rb +192 -0
  173. data/lib/hecks/fuzzing/domain_generator.rb +871 -0
  174. data/lib/hecks/fuzzing/era_boundary.rb +68 -19
  175. data/lib/hecks/fuzzing/form_census.rb +121 -20
  176. data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
  177. data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
  178. data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
  179. data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
  180. data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
  181. data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
  182. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
  183. data/lib/hecks/fuzzing/properties/guards.rb +86 -42
  184. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
  185. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
  186. data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
  187. data/lib/hecks/fuzzing/properties/querying.rb +82 -28
  188. data/lib/hecks/fuzzing/properties.rb +84 -51
  189. data/lib/hecks/fuzzing/qa_settings.rb +164 -0
  190. data/lib/hecks/fuzzing/replay.rb +241 -133
  191. data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
  192. data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
  193. data/lib/hecks/fuzzing/self_consistency.rb +315 -137
  194. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
  195. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  196. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  197. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  198. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
  199. data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
  200. data/lib/hecks/fuzzing/shrinker.rb +309 -0
  201. data/lib/hecks/fuzzing/structural_skips.rb +37 -130
  202. data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
  203. data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
  204. data/lib/hecks/fuzzing/value_generator.rb +110 -22
  205. data/lib/hecks/fuzzing.rb +5 -0
  206. data/lib/hecks/grammar/evolve.rb +188 -12
  207. data/lib/hecks/grammar.rb +53 -7
  208. data/lib/hecks/ir.rb +51 -20
  209. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  210. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  211. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  212. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  213. data/lib/hecks/language/oidc.json +5 -0
  214. data/lib/hecks/literal.rb +41 -9
  215. data/lib/hecks/naming.rb +112 -31
  216. data/lib/hecks/ports/access_control.rb +53 -2
  217. data/lib/hecks/ports/agent/answers.rb +83 -6
  218. data/lib/hecks/ports/agent.rb +119 -35
  219. data/lib/hecks/ports/authentication.rb +44 -4
  220. data/lib/hecks/ports/authorization.rb +50 -11
  221. data/lib/hecks/ports/clock.rb +42 -23
  222. data/lib/hecks/ports/extraction.rb +16 -0
  223. data/lib/hecks/ports/identity_assignment.rb +23 -2
  224. data/lib/hecks/ports/identity_generation.rb +17 -3
  225. data/lib/hecks/ports/identity_resolution.rb +17 -1
  226. data/lib/hecks/ports/key_vault.port +6 -0
  227. data/lib/hecks/ports/key_vault.rb +58 -0
  228. data/lib/hecks/ports/loading.rb +4 -0
  229. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  230. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  231. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  232. data/lib/hecks/ports/persistence/execution.rb +4 -0
  233. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  234. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  235. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  236. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  237. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  238. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  239. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  250. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  251. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  252. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
  253. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  254. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  255. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  256. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  257. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  258. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
  259. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  260. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  261. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  262. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
  263. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
  264. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  265. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
  266. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  267. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  268. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  269. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  270. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  271. data/lib/hecks/ports/persistence.rb +36 -1
  272. data/lib/hecks/ports/projection.rb +61 -7
  273. data/lib/hecks/ports/query/in_memory.rb +41 -8
  274. data/lib/hecks/ports/query/ordering.rb +21 -6
  275. data/lib/hecks/ports/query.rb +35 -0
  276. data/lib/hecks/ports.rb +1 -0
  277. data/lib/hecks/projections/bootstrap_table.rb +147 -0
  278. data/lib/hecks/projections/diagrams.rb +315 -79
  279. data/lib/hecks/projections/glossary/html.rb +91 -3
  280. data/lib/hecks/projections/glossary/markdown.rb +50 -3
  281. data/lib/hecks/projections/glossary/mermaid.rb +49 -1
  282. data/lib/hecks/projections/glossary/sections.rb +1 -1
  283. data/lib/hecks/projections/glossary/sentences.rb +110 -5
  284. data/lib/hecks/projections/glossary.rb +173 -31
  285. data/lib/hecks/projections/ir.rb +1 -1
  286. data/lib/hecks/projections/model/deviations.rb +62 -17
  287. data/lib/hecks/projections/model.rb +75 -21
  288. data/lib/hecks/projections/oidc.rb +45 -8
  289. data/lib/hecks/projections/parser_table.rb +53 -12
  290. data/lib/hecks/projections/reference.rb +15 -3
  291. data/lib/hecks/projections/rust_vocabulary.rb +646 -0
  292. data/lib/hecks/projections/shape.rb +10 -2
  293. data/lib/hecks/projections/statements.rb +69 -22
  294. data/lib/hecks/projections/vocabulary.rb +26 -9
  295. data/lib/hecks/projections.rb +5 -3
  296. data/lib/hecks/projector/cli_projector.rb +245 -37
  297. data/lib/hecks/projector/docs_projector.rb +154 -28
  298. data/lib/hecks/projector/exporter.rb +104 -29
  299. data/lib/hecks/projector/ir_projector.rb +7 -1
  300. data/lib/hecks/projector/narrate_projector.rb +144 -23
  301. data/lib/hecks/projector/target.rb +42 -18
  302. data/lib/hecks/projector.rb +86 -18
  303. data/lib/hecks/query_ir.rb +94 -47
  304. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  305. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  306. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  307. data/lib/hecks/query_specification/common/null_policy.rb +61 -15
  308. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  309. data/lib/hecks/query_specification/common/options.rb +25 -0
  310. data/lib/hecks/query_specification/field_path.rb +69 -15
  311. data/lib/hecks/query_specification/hop_path.rb +57 -20
  312. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  313. data/lib/hecks/rendering.rb +9 -3
  314. data/lib/hecks/router/namespace_installer.rb +16 -3
  315. data/lib/hecks/router.rb +56 -1
  316. data/lib/hecks/runtime/aggregate_lock.rb +20 -11
  317. data/lib/hecks/runtime/boot_gates.rb +21 -3
  318. data/lib/hecks/runtime/caller.rb +40 -8
  319. data/lib/hecks/runtime/capability_graph.rb +13 -2
  320. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
  321. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
  322. data/lib/hecks/runtime/command_interpreter.rb +195 -113
  323. data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
  324. data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
  325. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  326. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  327. data/lib/hecks/runtime/command_rules/references.rb +145 -55
  328. data/lib/hecks/runtime/command_rules.rb +3 -0
  329. data/lib/hecks/runtime/dependency_planning.rb +56 -11
  330. data/lib/hecks/runtime/dispatcher.rb +263 -92
  331. data/lib/hecks/runtime/entity_element.rb +304 -67
  332. data/lib/hecks/runtime/entity_interpreter.rb +149 -88
  333. data/lib/hecks/runtime/errors.rb +37 -23
  334. data/lib/hecks/runtime/event.rb +15 -10
  335. data/lib/hecks/runtime/identity.rb +91 -23
  336. data/lib/hecks/runtime/instance.rb +103 -18
  337. data/lib/hecks/runtime/interpreting.rb +24 -16
  338. data/lib/hecks/runtime/invocation.rb +358 -0
  339. data/lib/hecks/runtime/loader.rb +105 -19
  340. data/lib/hecks/runtime/outbox.rb +164 -26
  341. data/lib/hecks/runtime/policy_interpreter.rb +73 -60
  342. data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
  343. data/lib/hecks/runtime/query_interpreter.rb +93 -74
  344. data/lib/hecks/runtime/reaction_invocation.rb +73 -28
  345. data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
  346. data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
  347. data/lib/hecks/runtime/reference_hop.rb +48 -6
  348. data/lib/hecks/runtime/refusal_wording.rb +142 -115
  349. data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
  350. data/lib/hecks/runtime/registry/verification.rb +153 -28
  351. data/lib/hecks/runtime/registry.rb +202 -32
  352. data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
  353. data/lib/hecks/runtime/routing.rb +36 -154
  354. data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
  355. data/lib/hecks/runtime/saga_interpreter.rb +90 -76
  356. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  357. data/lib/hecks/runtime/tenant_check.rb +33 -13
  358. data/lib/hecks/runtime/tenant_scope.rb +23 -5
  359. data/lib/hecks/runtime/value/admission.rb +75 -30
  360. data/lib/hecks/runtime/value/coercion.rb +555 -142
  361. data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
  362. data/lib/hecks/runtime/value.rb +71 -21
  363. data/lib/hecks/runtime.rb +39 -7
  364. data/lib/hecks/storehouse.rb +368 -72
  365. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  366. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  367. data/lib/hecks/version.rb +3 -3
  368. data/lib/hecks/vocabulary.rb +205 -4
  369. data/lib/hecks.rb +91 -11
  370. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
  371. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
  372. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
  373. metadata +28 -2
@@ -2,18 +2,22 @@ require_relative "../naming"
2
2
 
3
3
  module Hecks
4
4
  module Projector
5
- # A BLUEBOOK, PROJECTED AS ITS OWN COMMAND-LINE SURFACE.
5
+ # A bluebook, projected as its own command-line surface.
6
6
  #
7
7
  # Every verb a domain declares is a subcommand; every argument is an
8
- # option whose TYPE, whose admitted values and whose required-ness are
8
+ # option whose type, whose admitted values and whose required-ness are
9
9
  # already stated in the chapter. A hand-written CLI restates all of it and
10
10
  # then drifts — and the first thing to drift is the help text, which is the
11
11
  # only part anybody reads.
12
12
  #
13
- # WHAT IS PROJECTED, AND WHAT IS NOT. This answers the SURFACE — the verb
14
- # tree, the argument spec, the usage text — and nothing executes here. One
15
- # small generic runner (`bin/run`) boots a domain, asks for this, parses
16
- # against it and dispatches.
13
+ # ## What is projected
14
+ #
15
+ # This answers the surface only — the verb tree, the argument spec, the
16
+ # usage text — and nothing executes here. One small generic runner
17
+ # (`bin/run`) boots a domain, asks for this, parses against it and
18
+ # dispatches.
19
+ #
20
+ # ## Why project rather than generate
17
21
  #
18
22
  # The alternative was generating an executable per domain, which is what
19
23
  # `bin/project_rust` does for a whole runtime and would be the more
@@ -23,16 +27,17 @@ module Hecks
23
27
  # reads. Projecting the surface keeps one dispatcher and a help text that
24
28
  # cannot be stale, because it is computed at the moment it is printed.
25
29
  #
26
- # THE TYPING IS THE POINT. A CLI hands everything over as a String.
27
- # `sequence.value=99` has to become the Integer 99 or the runtime refuses
28
- # it, and the only honest place to learn that is the value object's own
29
- # declared field type. A CLI that guessed — "it looks like a number" —
30
- # would send 99 for a version string of "99" and be wrong in a way nobody
31
- # could see.
30
+ # ## The typing is the point
31
+ #
32
+ # A CLI hands everything over as a String. `sequence.value=99` has to
33
+ # become the Integer 99 or the runtime refuses it, and the only honest
34
+ # place to learn that is the value object's own declared field type. A CLI
35
+ # that guessed — "it looks like a number" — would send 99 for a version
36
+ # string of "99" and be wrong in a way nobody could see.
32
37
  module CliProjector
33
38
  module_function
34
39
 
35
- # TWO NAMESPACES, NOT ONE — `{ verbs:, questions:, usage: }`.
40
+ # Two namespaces, not one — `{ verbs:, questions:, usage: }`.
36
41
  #
37
42
  # A chapter may legally declare a command and a query of one name: the
38
43
  # language namespaces them and `Banking::Account.Open` is both, in the
@@ -45,6 +50,17 @@ module Hecks
45
50
  # shell-safe where a `?` suffix would be eaten by globbing, and it makes
46
51
  # the collision impossible rather than detected. It also reads as what it
47
52
  # is: everything under `ask` changes nothing.
53
+ #
54
+ # @param bluebook [Bluebook::Chapter] the booted domain to project
55
+ # @param options [Hash{Symbol => Object}] `:program` (String, defaults to
56
+ # `"bin/run"`) is echoed into the usage text; `:verb` (String) and `:ask`
57
+ # (Boolean) select one verb's `--help` text instead of the full usage
58
+ # @return [Hash{Symbol => Object}] `:verbs` and `:questions` map each projected
59
+ # name to its spec hash; `:names` maps `:command`/`:question` to an alias table
60
+ # (short and full spelling both keying the full name); `:usage` is the
61
+ # pre-rendered help text
62
+ # @raise [Bluebook::DSL::Malformed] if two verbs project to the same command-line
63
+ # name
48
64
  def call(bluebook:, options: {})
49
65
  verbs = {}
50
66
  questions = {}
@@ -62,32 +78,32 @@ module Hecks
62
78
  end
63
79
  end
64
80
 
65
- # A PORT IS A VERB TOO, and leaving it off the map was a real gap
81
+ # A port is a verb too, and leaving it off the map was a real gap
66
82
  # rather than a tasteful omission. The runtime has always dispatched
67
83
  # a port operation by exactly the same name as a command — the
68
84
  # projection simply never listed one, so `run_specs` and `file`
69
85
  # answered "no such verb" while working perfectly through Ruby.
70
86
  #
71
87
  # It matters most for the caller with no other door. An agent that
72
- # may not shell out reaches this domain ONLY through the projected
88
+ # may not shell out reaches this domain only through the projected
73
89
  # CLI, and a port it cannot see is a capability it does not have.
74
90
  aggregate.ports.each do |port|
75
91
  port.operations.each { |o| claim(verbs, name_for(aggregate, o), port_spec(bluebook, aggregate, port, o)) }
76
92
  end
77
93
  end
78
94
 
79
- # A REPORT IS A QUESTION TOO, and leaving it off was the same gap the
95
+ # A report is a question too, and leaving it off was the same gap the
80
96
  # ports had: `Dispatcher#query` has always answered `Domain.ReportName`,
81
97
  # the projection simply never listed one — so the composed reads worked
82
98
  # from Ruby and did not exist for anybody whose only door is the command
83
99
  # line.
84
100
  #
85
- # It matters most for exactly what a report is FOR. Every other question
101
+ # It matters most for exactly what a report is for. Every other question
86
102
  # here answers with rows and leaves the arithmetic to the reader; a
87
103
  # `group_by` report is the one that counts. An agent that cannot reach
88
104
  # it can list bugs all day and never answer "how are we doing".
89
105
  #
90
- # ONE DOT, NOT TWO — a report belongs to the chapter rather than to any
106
+ # **One dot, not two** — a report belongs to the chapter rather than to any
91
107
  # aggregate (that is what rootless means), so it is addressed
92
108
  # `QualityControl.BugsByStatus` where a query is
93
109
  # `QualityControl::Bug.Queue`. `Dispatcher#query` splits on precisely
@@ -96,7 +112,7 @@ module Hecks
96
112
  claim(questions, Naming.snake(model.hecks_name), report_spec(bluebook, model))
97
113
  end
98
114
 
99
- # THE SHORT SPELLING, WHERE IT CANNOT BE AMBIGUOUS. `pizzas
115
+ # The short spelling, where it cannot be ambiguous. `pizzas
100
116
  # create_pizza` rather than `pizzas order.create_pizza` — the
101
117
  # aggregate is worth typing only when two of them declare the same
102
118
  # verb, and in a one-aggregate domain it never is. Both spellings are
@@ -114,6 +130,10 @@ module Hecks
114
130
  # aggregates declaring `Close` keep `customer.close` and `account.close`,
115
131
  # which is the honest answer — a CLI that picked one would be choosing
116
132
  # for the caller.
133
+ #
134
+ # @param specs [Hash{String => Hash}] the verb or question map being projected,
135
+ # mutated in place: each spec hash gains a `:short` key
136
+ # @return [void]
117
137
  def shorten(specs)
118
138
  tails = specs.keys.group_by { |name| name.split(".").last }
119
139
  specs.each do |name, spec|
@@ -122,6 +142,12 @@ module Hecks
122
142
  end
123
143
  end
124
144
 
145
+ # Builds the alias table `CliRunner` resolves a typed name against.
146
+ #
147
+ # @param specs [Hash{String => Hash}] a verb or question map whose specs already
148
+ # carry `:short` (set by `shorten`)
149
+ # @return [Hash{String => String}] every accepted spelling — the full name and
150
+ # its `:short` form — mapped to the full name
125
151
  def aliases(specs)
126
152
  specs.each_with_object({}) do |(name, spec), map|
127
153
  map[name] = name
@@ -129,11 +155,18 @@ module Hecks
129
155
  end
130
156
  end
131
157
 
132
- # A NAME IS CLAIMED ONCE. A command and a query of one name are legal in
158
+ # A name is claimed once. A command and a query of one name are legal in
133
159
  # a chapter — the language namespaces them — and ambiguous as
134
160
  # subcommands. Refused here rather than silently resolving to whichever
135
161
  # was walked first, which is how `Ticket.Filed` (a command) and
136
162
  # `Ticket.Filed` (a query) sat undetected until something flattened them.
163
+ #
164
+ # @param verbs [Hash{String => Hash}] the verb or question map being built,
165
+ # mutated in place
166
+ # @param name [String] the command-line name this spec claims
167
+ # @param spec [Hash{Symbol => Object}] the spec hash to store under `name`
168
+ # @return [void]
169
+ # @raise [Bluebook::DSL::Malformed] if `name` is already claimed by another verb
137
170
  def claim(verbs, name, spec)
138
171
  if verbs.key?(name)
139
172
  raise Bluebook::DSL::Malformed,
@@ -144,6 +177,15 @@ module Hecks
144
177
  verbs[name] = spec
145
178
  end
146
179
 
180
+ # Builds the dotted command-line name for one verb: `aggregate[.entity].verb`,
181
+ # snake-cased.
182
+ #
183
+ # @param aggregate [Bluebook::Aggregate] the verb's owning aggregate
184
+ # @param verb [Bluebook::Command, Bluebook::Query, Bluebook::PortOperation]
185
+ # the command, query or port operation being named
186
+ # @param entity [Bluebook::Entity, nil] the entity the verb is declared on, or `nil`
187
+ # for an aggregate-level verb
188
+ # @return [String] the dotted, snake-cased command-line name
147
189
  def name_for(aggregate, verb, entity = nil)
148
190
  parts = [Naming.snake(aggregate.hecks_name)]
149
191
  parts << Naming.snake(entity.hecks_name) if entity
@@ -151,12 +193,22 @@ module Hecks
151
193
  parts.join(".")
152
194
  end
153
195
 
196
+ # Builds the verb's fully-qualified language name, `Bluebook::Aggregate[.Entity].Verb`,
197
+ # for display in help text.
198
+ #
199
+ # @param bluebook [Bluebook::Chapter] the verb's owning chapter
200
+ # @param aggregate [Bluebook::Aggregate] the verb's owning aggregate
201
+ # @param verb [Bluebook::Command, Bluebook::Query, Bluebook::PortOperation]
202
+ # the command, query or port operation being named
203
+ # @param entity [Bluebook::Entity, nil] the entity the verb is declared on, or `nil`
204
+ # for an aggregate-level verb
205
+ # @return [String] the fully-qualified name, as the language spells it
154
206
  def fqn(bluebook, aggregate, verb, entity = nil)
155
207
  [bluebook.name, "::", aggregate.hecks_name, ".",
156
208
  entity ? "#{entity.hecks_name}." : "", verb.hecks_name].join
157
209
  end
158
210
 
159
- # THE ARGUMENTS A RECEIVER ADDS, BEFORE ANY VERB-SPECIFIC ONE. Shared by
211
+ # The arguments a receiver adds, before any verb-specific one. Shared by
160
212
  # `command_spec` and `port_spec` — a port operation always addresses an
161
213
  # aggregate record (`port_spec` passes `receiver: :aggregate`, never
162
214
  # `:entity` or `nil`, because a port is declared on an aggregate, never
@@ -166,6 +218,14 @@ module Hecks
166
218
  #
167
219
  # `nil` — a creating command — takes none: there is no existing record
168
220
  # yet for `to=` to name.
221
+ #
222
+ # @param receiver [Symbol, nil] `:entity`, `:aggregate`, or `nil` for a creating
223
+ # command that names no existing record
224
+ # @param aggregate [Bluebook::Aggregate] the aggregate the record belongs to
225
+ # @param entity [Bluebook::Entity, nil] the entity to name, required when
226
+ # `receiver` is `:entity`
227
+ # @return [Array<Hash{Symbol => Object}>] the `to`/`to.aggregate`+`to.entity` option
228
+ # specs this receiver needs, or `[]` for `nil`
169
229
  def receiver_options(receiver, aggregate, entity)
170
230
  case receiver
171
231
  when :entity
@@ -185,6 +245,18 @@ module Hecks
185
245
 
186
246
  # ── one verb ──────────────────────────────────────────────────────
187
247
 
248
+ # Projects one command into the spec `verb_help` and `CliRunner` both read.
249
+ #
250
+ # @param bluebook [Bluebook::Chapter] the command's owning chapter
251
+ # @param aggregate [Bluebook::Aggregate] the command's owning aggregate
252
+ # @param entity [Bluebook::Entity, nil] the entity the command is declared on, or
253
+ # `nil` for an aggregate-level command
254
+ # @param command [Bluebook::Command] the command being projected
255
+ # @return [Hash{Symbol => Object}] `:verb` (fully-qualified name), `:kind` (`:command`),
256
+ # `:summary`, `:role` (String, nil if the command declares none), `:role_gated`
257
+ # (Boolean), `:creates` (Boolean), `:receiver` (`:entity`, `:aggregate` or `nil`),
258
+ # `:legacy_receiver` (`:id` or `nil`), `:legacy_arguments`, `:refusals`
259
+ # (see `refusals`) and `:arguments` (see `options_for`)
188
260
  def command_spec(bluebook, aggregate, entity, command)
189
261
  holder = entity || aggregate
190
262
  arguments = command.attributes.flat_map { |a| options_for(a, holder, aggregate) }
@@ -194,7 +266,7 @@ module Hecks
194
266
  (command.creates? ? nil : :aggregate)
195
267
  end
196
268
 
197
- # THE RECEIVER IS NOT A COMMAND ARGUMENT. An aggregate command names
269
+ # The receiver is not a command argument. An aggregate command names
198
270
  # its record through to; an entity command needs both the aggregate
199
271
  # record and the entity element within it. Keeping those paths in the
200
272
  # projected option list makes the human-facing request complete while
@@ -215,7 +287,7 @@ module Hecks
215
287
  refusals: refusals(command, holder), arguments: arguments }
216
288
  end
217
289
 
218
- # A PORT OPERATION READS AS A VERB BUT REPORTS AS A BOUNDARY.
290
+ # A port operation reads as a verb but reports as a boundary.
219
291
  #
220
292
  # `creates: false` because it makes no record, and `refusals: []`
221
293
  # because it has none in the sense every other verb means: a command's
@@ -223,17 +295,27 @@ module Hecks
223
295
  # outbound operation's failure is somebody else's sentence, unknowable
224
296
  # from here.
225
297
  #
226
- # THE SUMMARY NAMES BOTH ENDINGS, which is the one thing a caller most
298
+ # The summary names both endings, which is the one thing a caller most
227
299
  # needs and cannot infer. `run_specs` looks like it either works or
228
300
  # errors; what it actually does is answer `SpecsCompleted` even when the
229
301
  # suite is red, and refuse only when rspec could not run. Somebody
230
302
  # reading `--help` should not have to open the hecksagon to find that
231
303
  # out.
304
+ #
305
+ # @param bluebook [Bluebook::Chapter] the port's owning chapter
306
+ # @param aggregate [Bluebook::Aggregate] the aggregate the port is declared on
307
+ # @param port [Bluebook::DomainPort] the port the operation is declared on
308
+ # @param operation [Bluebook::PortOperation] the operation being projected
309
+ # @return [Hash{Symbol => Object}] `:verb` (the `Aggregate.port.operation` name),
310
+ # `:kind` (`:command`), `:creates` (`false`), `:receiver` (`:aggregate`),
311
+ # `:refusals` (`[]`), `:role` (String, who calls whom through the port),
312
+ # `:role_gated` (`false`), `:summary` (see `port_summary`) and `:arguments`
313
+ # (see `options_for`)
232
314
  def port_spec(bluebook, aggregate, port, operation)
233
315
  arguments = receiver_options(:aggregate, aggregate, nil) +
234
316
  operation.attributes.flat_map { |a| options_for(a, aggregate, aggregate) }
235
317
 
236
- # THE WIRE NAME CARRIES THE PORT, THE TYPED NAME DOES NOT.
318
+ # The wire name carries the port, the typed name does not.
237
319
  #
238
320
  # `Dispatcher#dispatch` splits a verb into head and sub and looks the
239
321
  # head up as a port, so a port operation is addressed
@@ -244,7 +326,7 @@ module Hecks
244
326
  # names it short, which is the same split `shorten` already makes.
245
327
  { verb: [fqn(bluebook, aggregate, operation).sub(/\.[^.]+\z/, ""), port.name, operation.hecks_name].join("."),
246
328
  kind: :command, creates: false, receiver: :aggregate, refusals: [],
247
- # `role:` HERE IS DESCRIPTIVE TEXT, NOT AN AUTHORIZATION GATE —
329
+ # `role:` here is descriptive text, not an authorization gate —
248
330
  # who calls whom through the port, for `--help`/`verb_help`'s
249
331
  # "issued by" line. A port operation never reaches
250
332
  # `CommandRules::Authorization#refuse_role_mismatch` (only
@@ -260,14 +342,26 @@ module Hecks
260
342
  summary: port_summary(port, operation), arguments: arguments }
261
343
  end
262
344
 
345
+ # Names both endings a port operation can have: what an inbound `tells` records,
346
+ # or what an outbound `asks` came back with and what it said instead.
347
+ #
348
+ # @param port [Bluebook::DomainPort] the operation's owning port
349
+ # @param operation [Bluebook::PortOperation] the operation being summarized
350
+ # @return [String] one line, worded for `--help`
263
351
  def port_summary(port, operation)
264
352
  return "#{port.name} reports it; emits #{operation.emits.join(', ')}" unless operation.outbound?
265
353
 
266
354
  "Ask #{port.name} — answers #{operation.answers}, refuses #{operation.refuses}"
267
355
  end
268
356
 
269
- # A ROOTLESS REPORT TAKES NOTHING; a rooted one takes the id of the
357
+ # A rootless report takes nothing; a rooted one takes the id of the
270
358
  # record it is a view of, under the name the model gave that reference.
359
+ #
360
+ # @param bluebook [Bluebook::Chapter] the report's owning chapter
361
+ # @param model [Bluebook::ReadModel] the report being projected
362
+ # @return [Hash{Symbol => Object}] `:verb` (`Chapter.ReportName`), `:kind`
363
+ # (`:query`), `:summary` and `:arguments` (`[]`, or one required id option
364
+ # named after the model's `reference_name` when it declares a `reference_target`)
271
365
  def report_spec(bluebook, model)
272
366
  arguments =
273
367
  if model.reference_target
@@ -281,6 +375,15 @@ module Hecks
281
375
  summary: model.description, arguments: arguments }
282
376
  end
283
377
 
378
+ # Projects one query into the spec `verb_help` and `CliRunner` both read.
379
+ #
380
+ # @param bluebook [Bluebook::Chapter] the query's owning chapter
381
+ # @param aggregate [Bluebook::Aggregate] the query's owning aggregate
382
+ # @param entity [Bluebook::Entity, nil] the entity the query is declared on, or
383
+ # `nil` for an aggregate-level query
384
+ # @param query [Bluebook::Query] the query being projected
385
+ # @return [Hash{Symbol => Object}] `:verb` (fully-qualified name), `:kind`
386
+ # (`:query`), `:summary` and `:arguments` (see `options_for`)
284
387
  def query_spec(bluebook, aggregate, entity, query)
285
388
  arguments = Array(query.to_h[:attributes]).flat_map do |declared|
286
389
  attribute = query.attributes.find { |a| a.name.to_s == declared[:name].to_s }
@@ -293,14 +396,14 @@ module Hecks
293
396
 
294
397
  # ── one argument, flattened ───────────────────────────────────────
295
398
 
296
- # A VALUE OBJECT BECOMES ONE OPTION PER FIELD, dotted. `commit` typed
399
+ # A value object becomes one option per field, dotted. `commit` typed
297
400
  # `CommitRef` is `--commit.value`, because that is the shape the runtime
298
401
  # wants and a flat `--commit` would have to guess which field it meant.
299
402
  # Single-field value objects — almost all of them — read fine either way,
300
403
  # and the runner accepts the short form for exactly those.
301
- # RECURSIVE, AND IT HAS TO BE. A value object may hold another one —
404
+ # Recursive, and it has to be. A value object may hold another one —
302
405
  # pizzas' `Pizza` holds a `Price` and a `Size` — so stopping after one
303
- # level produced `pizza.price_cents=1500` and sent the STRING "1500"
406
+ # level produced `pizza.price_cents=1500` and sent the string "1500"
304
407
  # where `{ cents: 1500 }` belonged.
305
408
  #
306
409
  # The runtime took it. `qa/FINDINGS.md` #2 is exactly that gap —
@@ -308,6 +411,18 @@ module Hecks
308
411
  # one-level CLI is not merely inconvenient, it is a machine for writing
309
412
  # malformed records into a real store, which is what it did on its first
310
413
  # run against the pizzas database.
414
+ #
415
+ # @param attribute [Bluebook::Attribute] the field being projected
416
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity, Class] the construct
417
+ # `attribute` is declared on; a `Bluebook::ValueObject` subclass on a recursive call
418
+ # @param aggregate [Bluebook::Aggregate] the top-level aggregate, kept across recursion
419
+ # so a nested value object can still be found
420
+ # @param prefix [String, nil] the dotted path built so far, `nil` at the top level
421
+ # @param optional [Boolean, nil] whether an enclosing field already makes this one
422
+ # optional; `nil` defers to `attribute.optional?`
423
+ # @return [Array<Hash{Symbol => Object}>] one option spec per leaf field, each with
424
+ # `:path`, `:type`, `:required`, and optionally `:note`, `:enum`, `:pattern`,
425
+ # `:default`, `:list`
311
426
  def options_for(attribute, holder, aggregate, prefix = nil, optional = nil)
312
427
  path = [prefix, attribute.name].compact.join(".")
313
428
  optional ||= attribute.optional?
@@ -316,13 +431,13 @@ module Hecks
316
431
  value_object = value_object_for(attribute, holder, aggregate)
317
432
  return [scalar_option(path, attribute, optional)] unless value_object
318
433
 
319
- # A LIST SAYS SO, ALL THE WAY DOWN TO ITS LEAVES.
434
+ # A list says so, all the way down to its leaves.
320
435
  #
321
436
  # Without this a `list_of(Tag)` projected exactly like a single Tag:
322
437
  # one option, `tags.value`, indistinguishable from a scalar. So the
323
438
  # help said to pass one, `CliDoor#bury` overwrote the leaf each time,
324
- # and passing two tags stored the second and lost the first WITHOUT
325
- # SAYING ANYTHING. A missing argument is refused loudly; a forgotten
439
+ # and passing two tags stored the second and lost the first without
440
+ # saying anything. A missing argument is refused loudly; a forgotten
326
441
  # one is not, which makes it the more expensive of the two by far.
327
442
  #
328
443
  # The flag is carried on the leaf rather than kept beside the
@@ -342,11 +457,27 @@ module Hecks
342
457
  fields.map { |option| option.merge(list: true, note: [option[:note], "repeatable"].compact.join("; ")) }
343
458
  end
344
459
 
460
+ # Projects a `reference_to` attribute as the id of the record it points at.
461
+ #
462
+ # @param attribute [Bluebook::Attribute] a `reference?` attribute
463
+ # @return [Hash{Symbol => Object}] `:path`, `:type` (`"String"`), `:required` and
464
+ # `:note`
345
465
  def reference_option(attribute)
346
466
  { path: attribute.name.to_s, type: "String", required: !attribute.optional?,
347
467
  note: "id of a #{attribute.type.target_name}" }
348
468
  end
349
469
 
470
+ # Projects one leaf field — a plain attribute, or one field of a flattened
471
+ # value object.
472
+ #
473
+ # @param path [String] the dotted option path this field is reached at
474
+ # @param field [Bluebook::Attribute] the field being projected
475
+ # @param optional [Boolean] whether this option may be omitted
476
+ # @param enum [Array<Object>] the closed set of values this field admits, `[]` for
477
+ # an open field
478
+ # @return [Hash{Symbol => Object}] `:path`, `:type`, `:required`, plus `:enum` when
479
+ # `enum` is non-empty, `:pattern` when `field` declares one, and `:default` when
480
+ # `field` declares a non-nil one
350
481
  def scalar_option(path, field, optional, enum: [])
351
482
  option = { path: path, type: field.type.to_s, required: !optional }
352
483
  option[:enum] = enum unless enum.empty?
@@ -355,12 +486,30 @@ module Hecks
355
486
  option
356
487
  end
357
488
 
489
+ # The values a `one_of` value object's field is closed to, for the option's
490
+ # `:enum`.
491
+ #
492
+ # @param value_object [Class] a `Bluebook::ValueObject` subclass
493
+ # @param field [Bluebook::Attribute] the value object's field being projected
494
+ # @return [Array<Object>] the distinct values every declared member gives this
495
+ # field, or `[]` when the value object declares no closed set
358
496
  def closed_members(value_object, field)
359
497
  return [] unless value_object.closed_set?
360
498
 
361
499
  value_object.members.filter_map { |member| member[field.name] }.uniq
362
500
  end
363
501
 
502
+ # Finds the value object type an attribute declares, if any — the fork between
503
+ # `scalar_option` (one option) and recursing into `options_for` (one option per
504
+ # field).
505
+ #
506
+ # @param attribute [Bluebook::Attribute] the field whose type is being resolved
507
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity, Class, nil] the construct
508
+ # `attribute` is declared on, searched first
509
+ # @param aggregate [Bluebook::Aggregate, nil] the top-level aggregate, searched when
510
+ # `holder` does not declare the value object itself
511
+ # @return [Class, nil] the matching `Bluebook::ValueObject` subclass, or `nil` if
512
+ # `attribute`'s type names no value object in scope
364
513
  def value_object_for(attribute, holder, aggregate)
365
514
  [holder, aggregate].compact.each do |scope|
366
515
  next unless scope.respond_to?(:value_objects)
@@ -373,6 +522,13 @@ module Hecks
373
522
 
374
523
  # Every way this verb can say no, in the chapter's own words — printed
375
524
  # by `--help` before the caller spends a dispatch finding out.
525
+ #
526
+ # @param command [Bluebook::Command] the command whose refusals are being projected
527
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity] the aggregate or entity the
528
+ # command's lifecycle guard, if any, is checked against
529
+ # @return [Array<String>] one sentence per way the command can refuse: a lifecycle
530
+ # state mismatch, a missing referenced record per `reference_to` attribute, and
531
+ # each declared `given`'s description, in that order
376
532
  def refusals(command, holder)
377
533
  out = []
378
534
  lifecycle = holder.lifecycle
@@ -386,11 +542,21 @@ module Hecks
386
542
 
387
543
  # ── the help ──────────────────────────────────────────────────────
388
544
 
545
+ # Renders the full verb/question table, or one verb's `--help` text when
546
+ # `options[:verb]` names one.
547
+ #
548
+ # @param bluebook [Bluebook::Chapter] the projected chapter, for its name and vision
549
+ # line
550
+ # @param verbs [Hash{String => Hash}] the projected, `shorten`ed command map
551
+ # @param questions [Hash{String => Hash}] the projected, `shorten`ed query map
552
+ # @param options [Hash{Symbol => Object}] `:program` (String), `:verb` (String, nil)
553
+ # and `:ask` (Boolean) — see `call`
554
+ # @return [String] the rendered help text
389
555
  def usage(bluebook, verbs, questions, options)
390
556
  program = options[:program] || "bin/run"
391
557
  only = options[:verb]
392
558
 
393
- # WHICH NAMESPACE, when both hold the name. `options[:ask]` says so;
559
+ # Which namespace, when both hold the name. `options[:ask]` says so;
394
560
  # without it a `--help` for a question would print the command that
395
561
  # shares its name, which banking has and which is how this was found.
396
562
  if only
@@ -416,24 +582,40 @@ module Hecks
416
582
  out.join("\n")
417
583
  end
418
584
 
585
+ # Picks one verb whose full spelling differs from its short one, to show the
586
+ # caller both forms exist.
587
+ #
588
+ # @param verbs [Hash{String => Hash}] the projected, `shorten`ed command map
589
+ # @return [String] `"<short> is also <full>"`, or `""` if every verb's short form
590
+ # is its full name (a one-aggregate domain, where no name ever collides)
419
591
  def example_qualified(verbs)
420
592
  name, spec = verbs.find { |key, value| key != value[:short] } || verbs.first
421
593
  name ? "#{spec[:short]} is also #{name}" : ""
422
594
  end
423
595
 
424
- # A QUERY's `description` is written as a paragraph — it argues for why
596
+ # A query's `description` is written as a paragraph — it argues for why
425
597
  # the list is worth reading. A verb table wants the first sentence of
426
598
  # that argument; `--help` still prints the whole thing.
599
+ #
600
+ # @param text [String, nil] the full description, or `nil`
601
+ # @return [String] the text up to and including its first `.`, or `""` for `nil`
427
602
  def first_sentence(text)
428
603
  text.to_s.split(/(?<=\.)\s/).first.to_s
429
604
  end
430
605
 
431
- # FOUR TEXT BLOCKS, IN FIXED DISPLAY ORDER — meta (name/kind/role),
606
+ # Four text blocks, in fixed display order — meta (name/kind/role),
432
607
  # invocation, arguments, refusals. Each block is independent of the
433
- # others' content (only the OUTPUT ORDER is fixed, and stays fixed
608
+ # others' content (only the output order is fixed, and stays fixed
434
609
  # below), so each is its own method returning the lines it
435
610
  # contributes — `[]` when it contributes none — concatenated in the
436
611
  # same order the original inline version built them in.
612
+ #
613
+ # @param program [String] how the caller was invoked, echoed in the invocation line
614
+ # @param name [String] the verb's short display name
615
+ # @param spec [Hash{Symbol => Object}] the verb or question's projected spec (see
616
+ # `command_spec`, `port_spec`, `report_spec` or `query_spec`)
617
+ # @param ask [Boolean] whether this is a question, reached through `ask`
618
+ # @return [String] the verb's full `--help` text
437
619
  def verb_help(program, name, spec, ask: false)
438
620
  out = verb_help_meta_lines(name, spec)
439
621
  out.concat(verb_help_invocation_lines(program, name, spec, ask))
@@ -442,6 +624,12 @@ module Hecks
442
624
  out.join("\n")
443
625
  end
444
626
 
627
+ # Renders the name/summary heading, plus the dispatched or read verb and issuing
628
+ # role when the spec declares them.
629
+ #
630
+ # @param name [String] the verb's short display name
631
+ # @param spec [Hash{Symbol => Object}] the verb or question's projected spec
632
+ # @return [Array<String>] the meta block's lines
445
633
  def verb_help_meta_lines(name, spec)
446
634
  out = ["#{name} — #{spec[:summary]}", ""]
447
635
  out << "dispatches #{spec[:verb]}" if spec[:kind] == :command
@@ -450,11 +638,26 @@ module Hecks
450
638
  out
451
639
  end
452
640
 
641
+ # Renders the one-line example invocation, with every argument's path as a
642
+ # placeholder.
643
+ #
644
+ # @param program [String] how the caller was invoked
645
+ # @param name [String] the verb's short display name
646
+ # @param spec [Hash{Symbol => Object}] the verb or question's projected spec, read
647
+ # for `:arguments`
648
+ # @param ask [Boolean] whether to show the `ask` form of the invocation
649
+ # @return [Array<String>] the invocation block's lines
453
650
  def verb_help_invocation_lines(program, name, spec, ask)
454
651
  invocation = ask ? "#{program} ask #{name}" : "#{program} #{name}"
455
652
  ["", " #{invocation}#{spec[:arguments].map { |a| " #{a[:path]}=…" }.join}", ""]
456
653
  end
457
654
 
655
+ # Renders one line per declared argument: its type, admitted values, pattern,
656
+ # default and whether it is optional.
657
+ #
658
+ # @param spec [Hash{Symbol => Object}] the verb or question's projected spec, read
659
+ # for `:arguments`
660
+ # @return [Array<String>] the argument block's lines, `[]` if the verb takes none
458
661
  def verb_help_argument_lines(spec)
459
662
  return [] if spec[:arguments].empty?
460
663
 
@@ -472,6 +675,11 @@ module Hecks
472
675
  lines << ""
473
676
  end
474
677
 
678
+ # Renders the "refused when:" block, one line per way the verb can say no.
679
+ #
680
+ # @param spec [Hash{Symbol => Object}] the verb or question's projected spec, read
681
+ # for `:refusals`
682
+ # @return [Array<String>] the refusal block's lines, `[]` if the verb declares none
475
683
  def verb_help_refusal_lines(spec)
476
684
  return [] if Array(spec[:refusals]).empty?
477
685