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,7 +2,7 @@ require_relative "../runtime/errors"
2
2
 
3
3
  module Hecks
4
4
  module Facade
5
- # THE CLI DOOR — WHERE Facade MEETS A CALLER HOLDING FLAT STRINGS.
5
+ # **The CLI door** — where Facade meets a caller holding flat strings.
6
6
  #
7
7
  # `JsonDoor` beside this one translates for a caller holding parsed JSON:
8
8
  # String keys, already-nested objects, real Integers. A command line has
@@ -12,7 +12,7 @@ module Hecks
12
12
  # So this does the two things that turns into: rebuild the nesting, and
13
13
  # give every leaf the type the chapter declared for it.
14
14
  #
15
- # THE TYPE COMES FROM THE PROJECTION, NEVER FROM THE VALUE. A door that
15
+ # The type comes from the projection, never from the value. A door that
16
16
  # guessed — "99 looks like a number" — would send the Integer 99 for a
17
17
  # version string of "99", and be wrong in a way nothing downstream could
18
18
  # detect, because both are perfectly good arguments. `Projector::CliProjector`
@@ -21,8 +21,24 @@ module Hecks
21
21
  module CliDoor
22
22
  module_function
23
23
 
24
+ # Builds a verb's nested, typed argument Hash out of the `name=value` words on a
25
+ # command line.
26
+ #
24
27
  # `["reference.value=BUG#1", "sequence.value=99"]` against a projected
25
28
  # verb spec -> `{ reference: { value: "BUG#1" }, sequence: { value: 99 } }`
29
+ #
30
+ # @param spec [Hash{Symbol => Object}] one verb's entry from `Projector::CliProjector`;
31
+ # read for `:arguments` and the optional `:legacy_arguments`, each an Array of
32
+ # option Hashes with `:path`, `:type` and, for a list, `:list`
33
+ # @param pairs [Array<String>] the words after the verb, each `path=value`; a path
34
+ # may be the short form of a single-field value object (`reference` for
35
+ # `reference.value`)
36
+ # @return [Hash{Symbol => Object}] the arguments nested by path, each leaf cast to
37
+ # its declared type and each list option collected into an Array; `{}` for no pairs
38
+ # @raise [Runtime::NotFound] if a pair has no `=`, or names a path the verb does
39
+ # not take
40
+ # @raise [Runtime::TypeMismatch] if a value cannot be read as the declared Integer
41
+ # or Float
26
42
  def arguments(spec, pairs)
27
43
  # Legacy options are accepted but not printed in help. This lets an
28
44
  # existing id=... aggregate invocation cross the new receiver boundary
@@ -46,6 +62,12 @@ module Hecks
46
62
  end
47
63
  end
48
64
 
65
+ # Cuts one command-line word at its first `=`, so a value may itself contain `=`.
66
+ #
67
+ # @param pair [String] one word, such as `"reference.value=BUG#1"`
68
+ # @return [Array(String, String)] the path and the value; the value is `""` for
69
+ # `"name="`
70
+ # @raise [Runtime::NotFound] if the word contains no `=`
49
71
  def split(pair)
50
72
  name, value = pair.split("=", 2)
51
73
  raise Runtime::NotFound, "#{pair.inspect} is not name=value" if value.nil?
@@ -53,16 +75,32 @@ module Hecks
53
75
  [name, value]
54
76
  end
55
77
 
56
- # THE SHORT FORM, FOR THE COMMON CASE. Almost every value object in this
78
+ # Expands a bare value-object name into the full path of its only field.
79
+ #
80
+ # The short form, for the common case. Almost every value object in this
57
81
  # corpus has exactly one field, so `reference=BUG#1` is unambiguous and
58
82
  # is what anybody types. Expanded only when precisely one option starts
59
83
  # with that prefix — two would be a guess, and a guess about which field
60
84
  # a caller meant is worse than asking them to say.
85
+ #
86
+ # @param path [String] the path as typed, such as `"reference"`
87
+ # @param options [Hash{String => Hash}] the verb's options keyed by full path
88
+ # @return [String] the one full path beginning `"#{path}."`, or `path` unchanged when
89
+ # none or several do
61
90
  def expand(path, options)
62
91
  candidates = options.keys.select { |key| key.start_with?("#{path}.") }
63
92
  candidates.length == 1 ? candidates.first : path
64
93
  end
65
94
 
95
+ # Converts one command-line value to the type the chapter declares for its field.
96
+ #
97
+ # @param value [String] the text after the `=`
98
+ # @param type [String] the declared type name; `"Integer"`, `"Float"` and
99
+ # `"Boolean"` are converted, anything else leaves the value a String
100
+ # @return [Integer, Float, Boolean, String] the typed value; a Boolean is true only
101
+ # for `true`, `yes` or `1` in any letter case, and false for every other text
102
+ # @raise [Runtime::TypeMismatch] if the text does not parse as the declared Integer
103
+ # or Float
66
104
  def cast(value, type)
67
105
  case type
68
106
  when "Integer" then Integer(value)
@@ -74,27 +112,36 @@ module Hecks
74
112
  raise Runtime::TypeMismatch, "#{value.inspect} is not #{type} — the chapter declares this field as #{type}"
75
113
  end
76
114
 
77
- # A LIST GROWS RATHER THAN OVERWRITES, and getting this wrong is silent.
115
+ # Adds one element to a list argument, creating the list on first use.
116
+ #
117
+ # A list grows rather than overwrites, and getting this wrong is silent.
78
118
  #
79
- # `tags.value=framework tags.value=model-checker` used to reach `bury`
80
- # twice and store the second one alone — no refusal, no warning, one tag
119
+ # `tags.value=framework tags.value=model-checker` sent through `bury`
120
+ # twice would store the second one alone — no refusal, no warning, one tag
81
121
  # simply gone. That is the failure the interview named first: not the
82
122
  # loud kind, the kind where a value is forgotten and the caller has no
83
123
  # way to notice.
84
124
  #
85
- # A LIST OF ONE IS STILL A LIST. `tags.value=flaky` produces
125
+ # A list of one is still a list. `tags.value=flaky` produces
86
126
  # `[{ value: "flaky" }]`, not `{ value: "flaky" }`, because the chapter
87
127
  # declared a collection and a caller who sent one element did not
88
- # thereby declare a different shape. The old behaviour handed a bare
89
- # object to a `list_of` attribute, and everything downstream that walks
128
+ # thereby declare a different shape. A bare object must never reach a
129
+ # `list_of` attribute: everything downstream that walks
90
130
  # it — a query's `contains`, a projection, the Postgres adapter's own
91
131
  # array handling — is entitled to assume it can iterate.
92
132
  #
93
- # MULTI-FIELD ELEMENTS ARE NOT SUPPORTED HERE, deliberately. A flat
133
+ # Multi-field elements are not supported here, deliberately. A flat
94
134
  # command line has no way to say which `a.x=` goes with which `a.y=`,
95
135
  # and inventing an index syntax would be a language nobody asked for.
96
136
  # Every list in this corpus is a list of single-field value objects; a
97
137
  # richer one is a job for `JsonDoor`, which has real nesting.
138
+ #
139
+ # @param hash [Hash{Symbol => Object}] the arguments built so far; mutated in place
140
+ # @param path [Array<String>] the option's path segments, at least two: the last
141
+ # names the element's field, the one before it names the list
142
+ # @param value [Integer, Float, Boolean, String] the cast value for the new element
143
+ # @return [Hash{Symbol => Object}] `hash`, with `{ leaf => value }` appended to the
144
+ # Array at the list's key
98
145
  def append(hash, path, value)
99
146
  *branches, leaf = path.map(&:to_sym)
100
147
  holder = branches[0..-2].reduce(hash) { |node, key| node[key] ||= {} }
@@ -104,6 +151,13 @@ module Hecks
104
151
  hash
105
152
  end
106
153
 
154
+ # Sets one value at a nested path, creating each intermediate Hash on the way and
155
+ # overwriting whatever the leaf already held.
156
+ #
157
+ # @param hash [Hash{Symbol => Object}] the arguments built so far; mutated in place
158
+ # @param path [Array<String>] the option's path segments, outermost first
159
+ # @param value [Integer, Float, Boolean, String] the cast value to store at the leaf
160
+ # @return [Hash{Symbol => Object}] `hash`, with the value set under Symbol keys
107
161
  def bury(hash, path, value)
108
162
  *branches, leaf = path.map(&:to_sym)
109
163
  target = branches.reduce(hash) { |node, key| node[key] ||= {} }
@@ -111,6 +165,11 @@ module Hecks
111
165
  hash
112
166
  end
113
167
 
168
+ # Words the refusal for an argument the verb does not take, listing what it does.
169
+ #
170
+ # @param path [String] the path as the caller typed it
171
+ # @param known [Array<String>] every path the verb accepts, in any order
172
+ # @return [String] a one-line message naming `path` and the sorted accepted paths
114
173
  def unknown(path, known)
115
174
  "no argument #{path.inspect} — this verb takes #{known.sort.join(', ')}"
116
175
  end
@@ -7,23 +7,44 @@ require_relative "../ports/clock"
7
7
 
8
8
  module Hecks
9
9
  module Facade
10
- # THE RUNNER BEHIND A PROJECTED CLI.
10
+ # The runner behind a projected CLI.
11
11
  #
12
- # `Projector::CliProjector` answers what a domain's command line LOOKS
12
+ # `Projector::CliProjector` answers what a domain's command line looks
13
13
  # like; this is the twenty lines that parse against it and dispatch. It
14
14
  # lives in `lib/` rather than in a `bin/` because more than one front door
15
15
  # wants it — `bin/run` for whichever domain you are standing in, `bin/qc`
16
16
  # pinned to the QA ledger — and a second copy of the parse-and-dispatch
17
17
  # would be the exact duplication the projection exists to avoid.
18
18
  #
19
- # NO IO. It answers `[text, status]` and never prints or exits, so a spec
19
+ # No IO. It answers `[text, status]` and never prints or exits, so a spec
20
20
  # can call it without capturing streams or trapping SystemExit. The `bin/`
21
21
  # scripts do the printing, the same division `Router` and `JsonDoor`
22
22
  # already keep against HTTP.
23
23
  module CliRunner
24
24
  module_function
25
25
 
26
+ # Runs one command line against a booted domain: resolves the verb or question,
27
+ # parses its `name=value` arguments, dispatches or queries, and answers with the text
28
+ # to print.
29
+ #
26
30
  # `[text, status]` — status 0 answered, 1 refused or misused.
31
+ #
32
+ # Only the first bluebook in the registry (the booted domain's own) is projected
33
+ # into verbs. `--help`, `-h`, `help` or an empty `argv` answers the usage text; a
34
+ # verb followed by `--help` answers that verb's help.
35
+ #
36
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted domain,
37
+ # as `Hecks.boot` returns it
38
+ # @param argv [Array<String>] the words after the program name: an optional `ask`,
39
+ # the verb or question name (bare or aggregate-qualified), then `path=value` pairs
40
+ # @param program [String] how the caller was invoked, echoed in usage and hints
41
+ # @return [Array(String, Integer)] the text to print and the exit status: 0 with
42
+ # usage, help, or pretty-printed JSON of the outcome; 1 with a message when the
43
+ # verb is unknown, an argument is wrong, a record is missing, or the domain refuses
44
+ # @raise [Runtime::WiringError] if a repository, or the clock adapter a `now`
45
+ # argument needs, cannot be resolved
46
+ # @raise [Runtime::StaleWrite] if concurrent writers beat the command through every
47
+ # retry
27
48
  def call(runtime:, argv:, program: "bin/run")
28
49
  bluebook = runtime.registry.bluebooks.values.first
29
50
  cli = Projector.call(:cli, bluebook: bluebook, options: { program: program })
@@ -31,14 +52,14 @@ module Hecks
31
52
  name = argv.first
32
53
  return [cli[:usage], 0] if name.nil? || %w[--help -h help].include?(name)
33
54
 
34
- # `ask` PUTS A QUESTION IN ITS OWN NAMESPACE — a chapter may declare a
55
+ # `ask` puts a question in its own namespace — a chapter may declare a
35
56
  # command and a query of one name, and banking does.
36
57
  asking = name == "ask"
37
58
  argv = argv[1..] if asking
38
59
  name = argv.first
39
60
  return [cli[:usage], 1] if name.nil?
40
61
 
41
- # RESOLVED THROUGH THE ALIAS MAP, so `pizzas create_pizza` and
62
+ # Resolved through the alias map, so `pizzas create_pizza` and
42
63
  # `pizzas order.create_pizza` reach the same verb — the aggregate is
43
64
  # worth typing only when two of them declare the same word.
44
65
  pool = asking ? cli[:questions] : cli[:verbs]
@@ -56,6 +77,26 @@ module Hecks
56
77
  dispatch(runtime, spec, name, rest, program, asking)
57
78
  end
58
79
 
80
+ # Parses one resolved verb's arguments, runs it as a query or a command, and turns
81
+ # the outcome, or the domain's refusal, into printable text.
82
+ #
83
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted domain
84
+ # @param spec [Hash{Symbol => Object}] the verb's entry from `Projector::CliProjector`;
85
+ # read for `:kind`, `:verb`, `:arguments`, `:receiver` and `:legacy_receiver`
86
+ # @param name [String] the verb as the caller typed it, echoed in the `--help` hint
87
+ # @param rest [Array<String>] the `path=value` words after the verb
88
+ # @param program [String] how the caller was invoked, echoed in the `--help` hint
89
+ # @param asking [Boolean] whether the verb was reached through `ask`, which only
90
+ # changes the wording of the hint
91
+ # @return [Array(String, Integer)] status 0 with pretty-printed JSON: the materialized
92
+ # rows for a query; `id`, `state` and event names for a command; `id` and events
93
+ # with payloads for a port operation. Status 1 with the message of a
94
+ # `Runtime::DOMAIN_REFUSALS` error, followed by a `--help` hint when it is a
95
+ # `Runtime::NotFound` or `Runtime::TypeMismatch`
96
+ # @raise [Runtime::WiringError] if a repository, or the clock adapter a `now`
97
+ # argument needs, cannot be resolved
98
+ # @raise [Runtime::StaleWrite] if concurrent writers beat the command through every
99
+ # retry
59
100
  def dispatch(runtime, spec, name, rest, program, asking)
60
101
  args = stamp_time(runtime, spec, CliDoor.arguments(spec, rest))
61
102
 
@@ -64,34 +105,36 @@ module Hecks
64
105
  return [JSON.pretty_generate(rows.map { |row| JsonDoor.materialize(row) }), 0]
65
106
  end
66
107
 
67
- # THE ANSWER IS SCOPED TO WHAT WAS ASKED. `bin/run`'s step-list form
108
+ # The answer is scoped to what was asked. `bin/run`'s step-list form
68
109
  # reports the whole store because a corpus run is judged on all of it;
69
110
  # somebody who issued one verb wants that verb's outcome, and against a
70
111
  # Postgres-backed domain the full dump is every record there has been.
71
112
  request = CommandRequest.normalize(args, receiver: spec[:receiver],
72
113
  legacy_receiver: spec[:legacy_receiver])
73
- handle = runtime.dispatch(spec[:verb], **request)
114
+ handle = runtime.dispatch_flat(spec[:verb], request)
74
115
  return [JSON.pretty_generate(answered(handle)), 0] if handle.state.nil?
75
116
 
76
117
  [JSON.pretty_generate(id: handle.id,
77
118
  state: JsonDoor.materialize(handle.state),
78
119
  events: handle.events.map(&:name)), 0]
79
120
  rescue Runtime::NotFound, Runtime::TypeMismatch => e
80
- # A BAD ARGUMENT AND A MISSING RECORD BOTH LAND HERE, and both want the
121
+ # A bad argument and a missing record both land here, and both want the
81
122
  # same next step: read what the verb actually takes.
82
123
  ["#{e.message}\n\n #{program} #{'ask ' if asking}#{name} --help", 1]
83
124
  rescue *Runtime::DOMAIN_REFUSALS => e
84
- # THE REFUSAL IS THE PRODUCT — the chapter's own sentence, verbatim.
125
+ # **The refusal is the product** — the chapter's own sentence, verbatim.
85
126
  [e.message, 1]
86
127
  end
87
128
 
88
- # THE CLOCK, FILLED IN AT THE DOOR.
129
+ # Supplies the current time for a verb that takes a `now` argument the caller left out.
130
+ #
131
+ # The clock, filled in at the door.
89
132
  #
90
133
  # A staleness rule needs the time, and the sublanguage cannot ask for it
91
134
  # — a `given` that read the clock would judge the same record differently
92
135
  # on two runs, and every replay, audit and fuzz oracle here assumes it
93
- # does not. So `now` stays an ARGUMENT the predicate merely reads, and
94
- # the question becomes who types it. Before this, the caller did:
136
+ # does not. So `now` stays an argument the predicate merely reads, and
137
+ # the question becomes who types it. Left to the caller, that is:
95
138
  #
96
139
  # qa/quality_control target.claim id=QC held_by.value=me \
97
140
  # now.value=$(date +%s) window.value=900
@@ -99,20 +142,20 @@ module Hecks
99
142
  # which is a shell incantation in front of every claim, and one an agent
100
143
  # gets wrong by pasting a stale number.
101
144
  #
102
- # AT THE DOOR, NOT IN THE RUNTIME, and the distinction is load-bearing.
145
+ # At the door, not in the runtime, and the distinction is load-bearing.
103
146
  # `Ports::IdentityGeneration` reasons the same question through for a
104
147
  # minted uuid and lands on "the value is baked into the caller's args at
105
- # the first live dispatch". A clock consulted INSIDE the interpreter
148
+ # the first live dispatch". A clock consulted inside the interpreter
106
149
  # would not have that property — a recorded corpus step replayed
107
150
  # tomorrow would quietly get tomorrow's time, and the fuzzer's oracle and
108
151
  # the adapter-agreement gate both compare runs of exactly that shape. Here
109
152
  # it fills only what a person or an agent is typing, and `runtime.dispatch`
110
153
  # is left alone.
111
154
  #
112
- # AN EXPLICIT VALUE ALWAYS WINS, so a spec or a caller reproducing a
155
+ # An explicit value always wins, so a spec or a caller reproducing a
113
156
  # moment says so and is believed. This only supplies what was omitted.
114
157
  #
115
- # BY NAME, WHICH IS THE ONE UNCOMFORTABLE PART. `now` is a plausible
158
+ # By name, which is the one uncomfortable part. `now` is a plausible
116
159
  # domain word and nothing declares that it means the clock. It is
117
160
  # tolerable because this is a convenience layer rather than semantics —
118
161
  # the verb's own help says the argument exists, dispatch is unchanged,
@@ -120,6 +163,17 @@ module Hecks
120
163
  # declaration in the chapter (`attribute :now, Instant, from: :clock`),
121
164
  # which is a language change: DSL, IR, the self-hosted grammar and its
122
165
  # goldens. Worth doing; not worth smuggling in here.
166
+ #
167
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted domain,
168
+ # whose registry names the clock adapter
169
+ # @param spec [Hash{Symbol => Object}] the verb's projected entry; only an option whose
170
+ # `:path` starts `"now."` makes this method act
171
+ # @param args [Hash{Symbol => Object}] the parsed arguments; not mutated
172
+ # @return [Hash{Symbol => Object}] `args` itself when the verb takes no `now` or the
173
+ # caller gave one; otherwise a copy with `now: { value: seconds }`, the clock port's
174
+ # current time in whole seconds
175
+ # @raise [Runtime::WiringError] if the time is needed and zero or several adapters
176
+ # implement the clock port
123
177
  def stamp_time(runtime, spec, args)
124
178
  return args unless spec[:arguments].any? { |argument| argument[:path].start_with?("now.") }
125
179
  return args if args.key?(:now)
@@ -127,12 +181,15 @@ module Hecks
127
181
  args.merge(now: { value: Ports::Clock.now(runtime.registry) })
128
182
  end
129
183
 
130
- # A PORT OPERATION HAS NO STATE, AND ITS PAYLOAD IS THE ENTIRE POINT.
184
+ # Shapes a port operation's outcome for printing: the record asked about, and each
185
+ # event with its full payload.
186
+ #
187
+ # A port operation has no state, and its payload is the entire point.
131
188
  #
132
189
  # A command answers with the record it changed, so naming the events is
133
190
  # enough — the interesting part is in `state`. A port operation changes
134
191
  # no record: it asked something outside and came back with what was
135
- # said, and that lives ONLY in the event payload. Reporting names alone
192
+ # said, and that lives only in the event payload. Reporting names alone
136
193
  # would print `SpecsCompleted` and drop the spec output on the floor.
137
194
  #
138
195
  # This is what makes a projected CLI usable as somebody's only door. An
@@ -140,13 +197,19 @@ module Hecks
140
197
  # it asks the port and reads the answer, and if the answer is a bare
141
198
  # event name then the door leads nowhere and it needs a shell after all.
142
199
  #
143
- # BOTH ENDINGS COME BACK THE SAME WAY, and the status stays 0 for both.
200
+ # Both endings come back the same way, and the status stays 0 for both.
144
201
  # A refusal here is not a misuse — `IssueStillOpen` and `SuiteFailed`
145
202
  # are answers the caller asked for, correctly delivered. Exit 1 is for
146
203
  # "you typed something wrong", and conflating the two would have a
147
204
  # scripted agent treat a healthy no as a broken call.
205
+ #
206
+ # @param handle [Runtime::Dispatcher::Result, Runtime::RemoteDispatcher::Result] the
207
+ # dispatch result, which for a port operation has a nil `state`
208
+ # @return [Hash{Symbol => Object}] `:id`, the result's id or else the first event's
209
+ # (nil when there is neither), and `:events`, an Array of `{ name:, payload: }`
210
+ # Hashes with each payload materialized to plain data
148
211
  def answered(handle)
149
- # THE ID COMES OFF THE EVENT, because a port operation hydrates no
212
+ # The ID comes off the event, because a port operation hydrates no
150
213
  # instance and the handle's own `id` is nil by design. The event knows
151
214
  # which record was asked about — it was stamped with it — and printing
152
215
  # `null` beside a payload that plainly says `SW-TOOL` would read as a
@@ -157,15 +220,27 @@ module Hecks
157
220
  end }
158
221
  end
159
222
 
160
- # A NEAR MISS IS WORTH MORE THAN A LIST. Somebody who typed
223
+ # Words the answer to a verb or question that does not exist, suggesting up to five
224
+ # names that start the same way.
225
+ #
226
+ # A near miss is worth more than a list. Somebody who typed
161
227
  # `bug.discovr` wants one line, not eighty-seven of them.
162
228
  #
163
- # RANKED BY SHARED PREFIX, not by substring. Substring was the first
164
- # attempt and it finds nothing for the commonest typo of all — a dropped
229
+ # Ranked by shared prefix, not by substring. Substring matching finds
230
+ # nothing for the commonest typo of all — a dropped
165
231
  # letter, `order.create_piza`, which is a substring of nothing. Prefix
166
232
  # length survives an error anywhere after it, which is where errors are.
233
+ #
234
+ # @param cli [Hash{Symbol => Object}] the `Projector::CliProjector` projection; read
235
+ # for `cli[:names]`, the alias maps under `:command` and `:question`
236
+ # @param name [String] what the caller typed
237
+ # @param asking [Boolean] whether it was typed after `ask`, which picks the question
238
+ # names and the wording
239
+ # @param program [String] how the caller was invoked, echoed in the closing hint
240
+ # @return [String] a multi-line message; candidates must share a prefix of at least
241
+ # half of `name`'s length, and never fewer than three characters
167
242
  def unknown(cli, name, asking, program)
168
- # BOTH SPELLINGS ARE CANDIDATES. A caller who typed the qualified
243
+ # **Both spellings are candidates**. A caller who typed the qualified
169
244
  # form with a typo — `order.create_piza` — shares no prefix with the
170
245
  # short name `create_pizza`, so pooling only one of them suggests
171
246
  # nothing for half the mistakes anybody makes.
@@ -181,6 +256,12 @@ module Hecks
181
256
  lines.join("\n")
182
257
  end
183
258
 
259
+ # Counts how many leading characters two names have in common.
260
+ #
261
+ # @param one [String] a candidate name
262
+ # @param other [String] the name to compare it with
263
+ # @return [Integer] the length of the common prefix, from 0 up to the shorter
264
+ # name's length
184
265
  def shared_prefix(one, other)
185
266
  length = [one.length, other.length].min
186
267
  (0...length).find { |index| one[index] != other[index] } || length
@@ -16,6 +16,29 @@ module Hecks
16
16
  module CommandRequest
17
17
  module_function
18
18
 
19
+ # Splits one external request into routing (`to:`) and facts (`with:`), accepting
20
+ # either the explicit envelope or a flat Hash, and refuses a receiver that does
21
+ # not fit the kind of command being called.
22
+ #
23
+ # Keys are symbolized at every depth first, so a parsed JSON body and a Ruby Hash
24
+ # are treated alike. In a flat Hash everything but `to` (or the legacy receiver
25
+ # keys) is a fact.
26
+ #
27
+ # @param input [Hash, Object] the request, with String or Symbol keys; anything
28
+ # that is not a Hash is refused
29
+ # @param receiver [Symbol, nil] the kind of receiver the command takes:
30
+ # `:aggregate` (an identity), `:entity` (a Hash of `aggregate:` and `entity:`
31
+ # identities), or `nil` for a command that takes none, such as a creating command
32
+ # @param legacy_receiver [Symbol, String, Hash{Symbol => Symbol, String}, nil] where
33
+ # a flat request without `to` may carry its receiver instead: one key name (such
34
+ # as `:id`) for an `:aggregate` receiver, or `{ aggregate: key, entity: key }` for
35
+ # an `:entity` receiver; `nil` accepts no legacy spelling
36
+ # @return [Hash{Symbol => Object}] `{ with: facts }`, plus `to:` holding the route
37
+ # whenever `receiver` is not `nil`; ready to pass to `Dispatcher#dispatch_flat`
38
+ # @raise [Runtime::TypeMismatch] if `input` or its `with:` is not a Hash, if an
39
+ # explicit envelope carries keys other than `to:` and `with:`, or if the route
40
+ # is missing, blank, malformed, or given to a command that takes no receiver
41
+ # @raise [ArgumentError] if `receiver` is not `nil`, `:aggregate` or `:entity`
19
42
  def normalize(input, receiver:, legacy_receiver: nil)
20
43
  request = symbolize(input)
21
44
  raise Runtime::TypeMismatch, "a command request must be a hash" unless request.is_a?(Hash)