hecks 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (355) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +7 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +10 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
  7. data/lib/hecks/adapters/driven/heki/journal.rb +5 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
  9. data/lib/hecks/adapters/driven/heki.rb +13 -7
  10. data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
  12. data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
  13. data/lib/hecks/adapters/driven/lambda.rb +39 -33
  14. data/lib/hecks/adapters/driven/local_storage.rb +17 -10
  15. data/lib/hecks/adapters/driven/memory.rb +205 -9
  16. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
  17. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  18. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  19. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  20. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  21. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  22. data/lib/hecks/adapters/driven/prism.rb +4 -4
  23. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  24. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  25. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  26. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  27. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  28. data/lib/hecks/adapters/driven.rb +4 -4
  29. data/lib/hecks/adapters/driving/github_webhook.rb +18 -18
  30. data/lib/hecks/behaviors/dsl.rb +2 -2
  31. data/lib/hecks/behaviors/expectations.rb +23 -23
  32. data/lib/hecks/behaviors/ir.rb +1 -1
  33. data/lib/hecks/behaviors/rspec.rb +1 -1
  34. data/lib/hecks/behaviors/runner.rb +2 -2
  35. data/lib/hecks/behaviors.rb +1 -1
  36. data/lib/hecks/bluebook/aggregate.rb +13 -13
  37. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  38. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  39. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  40. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  41. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  42. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  43. data/lib/hecks/bluebook/assembly.rb +9 -9
  44. data/lib/hecks/bluebook/attribute.rb +9 -9
  45. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  46. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  47. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  48. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  49. data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -7
  50. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  51. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  52. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  53. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  54. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  55. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  56. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  57. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  58. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  59. data/lib/hecks/bluebook/capabilities.rb +27 -0
  60. data/lib/hecks/bluebook/chapter.rb +28 -9
  61. data/lib/hecks/bluebook/command.rb +12 -12
  62. data/lib/hecks/bluebook/domain_port.rb +9 -9
  63. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  64. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  65. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  66. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  67. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  68. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  69. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  70. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  71. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  72. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  73. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  74. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  75. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  76. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  77. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  78. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  79. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  80. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  81. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  82. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  83. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  84. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  85. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  86. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  87. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  88. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  89. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  90. data/lib/hecks/bluebook/entity.rb +11 -11
  91. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  92. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  93. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  94. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  95. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  97. data/lib/hecks/bluebook/hexagon.rb +1 -1
  98. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  99. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/judge.rb +101 -87
  101. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  102. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  103. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  104. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  105. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  106. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  107. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  108. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  109. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  110. data/lib/hecks/bluebook/model_check.rb +170 -84
  111. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  112. data/lib/hecks/bluebook/policy.rb +15 -13
  113. data/lib/hecks/bluebook/process_manager.rb +14 -14
  114. data/lib/hecks/bluebook/project_register.rb +6 -6
  115. data/lib/hecks/bluebook/query.rb +4 -4
  116. data/lib/hecks/bluebook/read_model.rb +14 -14
  117. data/lib/hecks/bluebook/reference.rb +8 -8
  118. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  119. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  120. data/lib/hecks/bluebook/translation.rb +4 -4
  121. data/lib/hecks/bluebook/value_object.rb +6 -6
  122. data/lib/hecks/bluebook.rb +2 -2
  123. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  124. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  125. data/lib/hecks/codemod.rb +36 -35
  126. data/lib/hecks/construct.rb +6 -6
  127. data/lib/hecks/corpus.rb +317 -0
  128. data/lib/hecks/deprecation.rb +95 -0
  129. data/lib/hecks/doc/reference.rb +19 -19
  130. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  131. data/lib/hecks/facade/cli_door.rb +69 -10
  132. data/lib/hecks/facade/cli_runner.rb +105 -24
  133. data/lib/hecks/facade/command_request.rb +23 -0
  134. data/lib/hecks/facade/handle.rb +79 -32
  135. data/lib/hecks/facade/json_door.rb +106 -25
  136. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  137. data/lib/hecks/facade/surface/chapter.rb +26 -17
  138. data/lib/hecks/facade/surface.rb +16 -3
  139. data/lib/hecks/facade.rb +15 -4
  140. data/lib/hecks/forms/app.rb +46 -30
  141. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  142. data/lib/hecks/forms/field_renderer.rb +142 -6
  143. data/lib/hecks/forms/field_shape.rb +180 -19
  144. data/lib/hecks/forms/html.rb +51 -7
  145. data/lib/hecks/forms/index_renderer.rb +14 -2
  146. data/lib/hecks/forms/params.rb +120 -23
  147. data/lib/hecks/forms/port_argument.rb +2 -2
  148. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  149. data/lib/hecks/forms/record_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_table.rb +1 -1
  151. data/lib/hecks/forms/value_object_shape.rb +3 -3
  152. data/lib/hecks/forms.rb +24 -4
  153. data/lib/hecks/fqn.rb +1 -1
  154. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  155. data/lib/hecks/framework.rb +48 -17
  156. data/lib/hecks/freezer.rb +11 -11
  157. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  158. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  159. data/lib/hecks/fuzzing/concurrent_dispatch.rb +140 -45
  160. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  161. data/lib/hecks/fuzzing/differential.rb +158 -0
  162. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  163. data/lib/hecks/fuzzing/era_boundary.rb +37 -18
  164. data/lib/hecks/fuzzing/form_census.rb +35 -20
  165. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  166. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  167. data/lib/hecks/fuzzing/isolated_boot.rb +62 -62
  168. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  169. data/lib/hecks/fuzzing/persistence_parity.rb +19 -21
  170. data/lib/hecks/fuzzing/properties/corrections.rb +9 -9
  171. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +154 -49
  172. data/lib/hecks/fuzzing/properties/guards.rb +42 -42
  173. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  174. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +22 -35
  175. data/lib/hecks/fuzzing/properties/outbox.rb +24 -24
  176. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  177. data/lib/hecks/fuzzing/properties.rb +64 -40
  178. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  179. data/lib/hecks/fuzzing/replay.rb +114 -114
  180. data/lib/hecks/fuzzing/rotation_priority.rb +10 -10
  181. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  182. data/lib/hecks/fuzzing/self_consistency.rb +141 -112
  183. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +35 -35
  184. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  185. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  186. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  187. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +58 -31
  188. data/lib/hecks/fuzzing/sequence_generator.rb +111 -23
  189. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  190. data/lib/hecks/fuzzing/structural_skips.rb +23 -130
  191. data/lib/hecks/fuzzing/sweep_depth.rb +4 -4
  192. data/lib/hecks/fuzzing/target_capabilities.rb +49 -18
  193. data/lib/hecks/fuzzing/value_generator.rb +14 -14
  194. data/lib/hecks/fuzzing.rb +5 -0
  195. data/lib/hecks/grammar/evolve.rb +10 -10
  196. data/lib/hecks/grammar.rb +7 -7
  197. data/lib/hecks/ir.rb +13 -13
  198. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  199. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  200. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  201. data/lib/hecks/language/oidc.json +5 -0
  202. data/lib/hecks/literal.rb +9 -9
  203. data/lib/hecks/naming.rb +25 -25
  204. data/lib/hecks/ports/access_control.rb +58 -2
  205. data/lib/hecks/ports/agent/answers.rb +83 -6
  206. data/lib/hecks/ports/agent.rb +119 -35
  207. data/lib/hecks/ports/authentication.rb +44 -4
  208. data/lib/hecks/ports/authorization.rb +53 -11
  209. data/lib/hecks/ports/clock.rb +42 -23
  210. data/lib/hecks/ports/extraction.rb +16 -0
  211. data/lib/hecks/ports/identity_assignment.rb +24 -2
  212. data/lib/hecks/ports/identity_generation.rb +17 -3
  213. data/lib/hecks/ports/identity_resolution.rb +18 -1
  214. data/lib/hecks/ports/loading.rb +4 -0
  215. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  216. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  217. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  218. data/lib/hecks/ports/persistence/execution.rb +4 -0
  219. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  220. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  221. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  222. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  224. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  225. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  226. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  241. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  242. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  247. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  248. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  249. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  252. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  253. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  254. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  255. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  256. data/lib/hecks/ports/persistence.rb +36 -1
  257. data/lib/hecks/ports/projection.rb +61 -7
  258. data/lib/hecks/ports/query/in_memory.rb +3 -3
  259. data/lib/hecks/ports/query/ordering.rb +6 -6
  260. data/lib/hecks/ports/query.rb +35 -0
  261. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  262. data/lib/hecks/projections/diagrams.rb +75 -75
  263. data/lib/hecks/projections/glossary/html.rb +3 -3
  264. data/lib/hecks/projections/glossary/markdown.rb +3 -3
  265. data/lib/hecks/projections/glossary/mermaid.rb +1 -1
  266. data/lib/hecks/projections/glossary/sections.rb +1 -1
  267. data/lib/hecks/projections/glossary/sentences.rb +5 -5
  268. data/lib/hecks/projections/glossary.rb +15 -15
  269. data/lib/hecks/projections/ir.rb +1 -1
  270. data/lib/hecks/projections/model/deviations.rb +18 -17
  271. data/lib/hecks/projections/model.rb +25 -21
  272. data/lib/hecks/projections/oidc.rb +7 -7
  273. data/lib/hecks/projections/parser_table.rb +5 -5
  274. data/lib/hecks/projections/reference.rb +3 -3
  275. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  276. data/lib/hecks/projections/shape.rb +2 -2
  277. data/lib/hecks/projections/statements.rb +11 -11
  278. data/lib/hecks/projections/vocabulary.rb +9 -9
  279. data/lib/hecks/projections.rb +5 -3
  280. data/lib/hecks/projector/cli_projector.rb +29 -29
  281. data/lib/hecks/projector/docs_projector.rb +13 -13
  282. data/lib/hecks/projector/exporter.rb +42 -21
  283. data/lib/hecks/projector/ir_projector.rb +1 -1
  284. data/lib/hecks/projector/narrate_projector.rb +11 -11
  285. data/lib/hecks/projector/target.rb +13 -13
  286. data/lib/hecks/projector.rb +15 -15
  287. data/lib/hecks/query_ir.rb +47 -47
  288. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  289. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  290. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  291. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  292. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  293. data/lib/hecks/query_specification/common/options.rb +25 -0
  294. data/lib/hecks/query_specification/field_path.rb +69 -15
  295. data/lib/hecks/query_specification/hop_path.rb +57 -20
  296. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  297. data/lib/hecks/rendering.rb +3 -3
  298. data/lib/hecks/router/namespace_installer.rb +3 -3
  299. data/lib/hecks/router.rb +1 -1
  300. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  301. data/lib/hecks/runtime/boot_gates.rb +3 -3
  302. data/lib/hecks/runtime/caller.rb +8 -8
  303. data/lib/hecks/runtime/capability_graph.rb +2 -2
  304. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  305. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +42 -63
  306. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  307. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  308. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  309. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  310. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  311. data/lib/hecks/runtime/command_rules/references.rb +35 -35
  312. data/lib/hecks/runtime/command_rules.rb +3 -0
  313. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  314. data/lib/hecks/runtime/dispatcher.rb +283 -90
  315. data/lib/hecks/runtime/entity_element.rb +151 -67
  316. data/lib/hecks/runtime/entity_interpreter.rb +108 -82
  317. data/lib/hecks/runtime/errors.rb +19 -19
  318. data/lib/hecks/runtime/event.rb +6 -6
  319. data/lib/hecks/runtime/identity.rb +22 -22
  320. data/lib/hecks/runtime/instance.rb +39 -14
  321. data/lib/hecks/runtime/interpreting.rb +12 -12
  322. data/lib/hecks/runtime/invocation.rb +276 -0
  323. data/lib/hecks/runtime/loader.rb +14 -14
  324. data/lib/hecks/runtime/outbox.rb +23 -23
  325. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  326. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  327. data/lib/hecks/runtime/query_interpreter.rb +55 -64
  328. data/lib/hecks/runtime/reaction_invocation.rb +20 -20
  329. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  330. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  331. data/lib/hecks/runtime/reference_hop.rb +6 -6
  332. data/lib/hecks/runtime/refusal_wording.rb +92 -115
  333. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  334. data/lib/hecks/runtime/registry/verification.rb +36 -26
  335. data/lib/hecks/runtime/registry.rb +45 -28
  336. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  337. data/lib/hecks/runtime/routing.rb +10 -153
  338. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  339. data/lib/hecks/runtime/saga_interpreter.rb +64 -64
  340. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  341. data/lib/hecks/runtime/tenant_check.rb +9 -9
  342. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  343. data/lib/hecks/runtime/value/admission.rb +75 -30
  344. data/lib/hecks/runtime/value/coercion.rb +318 -127
  345. data/lib/hecks/runtime/value/entity_list_coercion.rb +34 -34
  346. data/lib/hecks/runtime/value.rb +21 -21
  347. data/lib/hecks/runtime.rb +7 -7
  348. data/lib/hecks/storehouse.rb +64 -64
  349. data/lib/hecks/version.rb +3 -3
  350. data/lib/hecks/vocabulary.rb +205 -4
  351. data/lib/hecks.rb +13 -11
  352. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  353. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  354. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  355. metadata +22 -2
@@ -6,7 +6,7 @@ module Hecks
6
6
  # The DSL reference, projected from the language's own Syntax chapter
7
7
  # — the same Keyword/Argument rows the conformance specs hold equal
8
8
  # to the live builders. Nothing here is described twice: the tables
9
- # come from the declaration, the PROSE is hand-written between
9
+ # come from the declaration, the prose is hand-written between
10
10
  # markers the generator preserves, and the golden spec refuses a
11
11
  # tree where the two have drifted.
12
12
  #
@@ -19,9 +19,9 @@ module Hecks
19
19
  GENERATED_END = "<!-- generated:end -->".freeze
20
20
  TODO_SENTINEL = "<!-- TODO: document this word -->".freeze
21
21
 
22
- # A PAGE'S OWN HAND-WRITTEN OPENING, harvested under a key no word
22
+ # A PAGE'S own hand-written opening, harvested under a key no word
23
23
  # can ever collide with (words are strings off the Syntax chapter;
24
- # this is a Symbol). It exists so a page can boot ONCE — load a real
24
+ # this is a Symbol). It exists so a page can boot once — load a real
25
25
  # corpus chapter, wire its hexagon — and have every word's example
26
26
  # below run against that single boot, the way a guide's opening
27
27
  # `ruby boot` block already does. Without it each word would have to
@@ -33,7 +33,7 @@ module Hecks
33
33
 
34
34
  def generated_begin(word) = "<!-- generated:begin word=#{word} -->"
35
35
 
36
- # Keyed by REGION rather than by word — the same marker convention,
36
+ # Keyed by region rather than by word — the same marker convention,
37
37
  # used for the parts of a page that are not about one word: a
38
38
  # page's generated lede here, README's generated indexes below.
39
39
  def region_begin(id) = "<!-- generated:begin id=#{id} -->"
@@ -56,7 +56,7 @@ module Hecks
56
56
  # same shape `rows` used to produce — nothing below this needed to
57
57
  # change.
58
58
  #
59
- # NO SEPARATE `@keywords ||=` HERE ANYMORE. This module used to
59
+ # No separate `@keywords ||=` here anymore. This module used to
60
60
  # memoize its own copy on top of `SyntaxBoot.call`'s own memo — a
61
61
  # double cache with no way to invalidate either half, and a real
62
62
  # bug: whichever call in the whole process happened to land first
@@ -86,9 +86,9 @@ module Hecks
86
86
  end.merge("index.md" => render_index)
87
87
  end
88
88
 
89
- # A WORD ADMITTING TWO FORMS HAS TWO ROWS — syntax.bluebook's own
89
+ # A WORD admitting two forms has two rows — syntax.bluebook's own
90
90
  # stated rule, and `identified_by` (a block, or a bare argument and
91
- # none) is the case that made it real again. One SECTION per word all
91
+ # none) is the case that made it real again. One section per word all
92
92
  # the same: the prose is the word's rather than the form's, and the
93
93
  # argument rows join by (word, context) and so already cover every
94
94
  # form. Grouped rather than rendered per row, or a reader would meet
@@ -128,7 +128,7 @@ module Hecks
128
128
  inside.empty? ? "Words available in the #{context} body." : "Words available inside #{inside}."
129
129
  end
130
130
 
131
- # One SPELLING per form, everything else off the first row — the
131
+ # One spelling per form, everything else off the first row — the
132
132
  # columns that differ between two forms of one word are `body` (which
133
133
  # is what the spelling shows) and nothing else.
134
134
  def render_word(forms, prose)
@@ -212,7 +212,7 @@ module Hecks
212
212
  # preamble is invented — the older shape reads back unchanged.
213
213
  # A single-pass line-scanning state machine (current/collecting/
214
214
  # buffer/in_fence) — each branch mutates shared local state that
215
- # carries into the NEXT iteration, so splitting per branch would
215
+ # carries into the next iteration, so splitting per branch would
216
216
  # mean passing all four back and forth by reference every line.
217
217
  # rubocop:disable-next Metrics/PerceivedComplexity
218
218
  def harvest(text)
@@ -221,7 +221,7 @@ module Hecks
221
221
  collecting = false
222
222
  buffer = []
223
223
 
224
- # A HEADING INSIDE A FENCE IS NOT A HEADING. `## something` is an
224
+ # A heading inside a fence is not a heading. `## something` is an
225
225
  # ordinary Ruby comment, and now that every word's section carries
226
226
  # runnable code, one written at the left margin would otherwise
227
227
  # end that section mid-example and orphan the rest of it under a
@@ -293,11 +293,11 @@ module Hecks
293
293
  lines.join("\n")
294
294
  end
295
295
 
296
- # The opening comment PARAGRAPH, not just the first line — a table
296
+ # The opening comment paragraph, not just the first line — a table
297
297
  # cell that trails off mid-clause reads worse than one that runs a
298
298
  # little long and says "...". A code-bearing comment (`field.name`,
299
299
  # `pattern:`) makes naive sentence-splitting on "." or ":" cut in
300
- # the wrong place, so this truncates on LENGTH alone.
300
+ # the wrong place, so this truncates on length alone.
301
301
  def tool_summary(path)
302
302
  comment_lines = []
303
303
  started = false
@@ -317,13 +317,13 @@ module Hecks
317
317
  text.length > 140 ? "#{text[0, 137]}..." : text
318
318
  end
319
319
 
320
- # ONE REAL, COMMITTED FILE, READ FRESH — not re-derived from a boot
320
+ # One real, committed file, read fresh — not re-derived from a boot
321
321
  # (this module never requires `hecks/projections/diagrams`, and
322
322
  # shouldn't just to draw one example). `docs/generated/diagrams/`
323
323
  # is already held to the declaration by `spec/diagrams_spec.rb`'s
324
324
  # own drift check; this just quotes its own output, so the two
325
325
  # can't independently drift from each other either — a stale
326
- # Order_lifecycle.mmd fails THAT spec long before this one runs.
326
+ # Order_lifecycle.mmd fails that spec long before this one runs.
327
327
  def diagram_showcase(root)
328
328
  lifecycle = File.read(File.join(root, "docs/generated/diagrams/pizzas/Order_lifecycle.mmd")).strip
329
329
  <<~MARKDOWN.strip
@@ -363,7 +363,7 @@ module Hecks
363
363
  File.write(path, render_readme(root, File.read(path)))
364
364
  end
365
365
 
366
- # An example A READER CAN SEE and the harness will actually run.
366
+ # An example a reader can see and the harness will actually run.
367
367
  # `ruby skip` is display-only by the doctest harness's own rule, and
368
368
  # a hidden `<!-- doctest:boot -->` block is setup rather than an
369
369
  # example — a word whose only "example" is invisible or inert is a
@@ -373,7 +373,7 @@ module Hecks
373
373
 
374
374
  def exemplified?(prose) = prose.to_s.match?(EXAMPLE_FENCE)
375
375
 
376
- # EVERY LIVE WORD, PAIRED WITH ITS PROSE. Both coverage gates ask a
376
+ # Every live WORD, paired with its prose. Both coverage gates ask a
377
377
  # question about this same walk and differ only in what they ask of
378
378
  # the prose, so they share it rather than each re-deriving the page
379
379
  # set — the two are meant to move together, and one drifting past
@@ -394,18 +394,18 @@ module Hecks
394
394
 
395
395
  def name_of(word, context) = "#{word} (#{context})"
396
396
 
397
- # The coverage gate's question: every LIVE word with no prose yet.
397
+ # The coverage gate's question: every live word with no prose yet.
398
398
  def undocumented(directory)
399
399
  live_words(directory).reject { |_word, _context, prose| prose }
400
400
  .map { |word, context, _| name_of(word, context) }
401
401
  end
402
402
 
403
- # The SECOND coverage gate: prose is a declaration, and a
403
+ # The second coverage gate: prose is a declaration, and a
404
404
  # declaration nothing runs cannot disagree with anything. A word
405
405
  # documented only in sentences can go stale — or describe a word
406
406
  # the runtime never wired at all, which this repository has already
407
407
  # shipped twice (`read_model`'s where/order_by/limit/offset, and
408
- # `role`/`goal` on a command). An example that RUNS is the only
408
+ # `role`/`goal` on a command). An example that runs is the only
409
409
  # documentation that can go red.
410
410
  def unexemplified(directory)
411
411
  live_words(directory).reject { |_word, _context, prose| exemplified?(prose) }
@@ -1,12 +1,12 @@
1
1
  module Hecks
2
- # A VENDORED, EXTERNAL BLUEBOOK — same shape as Framework (framework.rb),
2
+ # **A vendored, external bluebook** — same shape as Framework (framework.rb),
3
3
  # for members that don't ship inside hecks's own lib/ at all: a
4
4
  # separate, independently-versioned package
5
5
  # (github.com/chrisyoung/embryonaut_bluebooks) that a consuming project
6
- # vendors into its OWN checkout, the same way a project already vendors
6
+ # vendors into its own checkout, the same way a project already vendors
7
7
  # hecks itself (bin/vendor_hecks, vendor/hecks/).
8
8
  #
9
- # RECOVERED, NOT REBUILT — this module and its `uses_embryonaut_bluebook`
9
+ # **Recovered, not rebuilt** — this module and its `uses_embryonaut_bluebook`
10
10
  # DSL word (hecksagon_builder.rb) were built on a prior commit of this
11
11
  # repo (933d1dd), vendored out to a real consumer (lifeadelics/domain,
12
12
  # for embryonaut_bluebooks/payments), and then lost from this repo's own
@@ -17,19 +17,19 @@ module Hecks
17
17
  # conventions rather than copied wholesale, since the two trees had
18
18
  # otherwise diverged for weeks in both directions.
19
19
  #
20
- # RESOLVED FROM THE CONSUMING REGISTRY'S OWN ROOT, not this gem's
20
+ # Resolved from the consuming registry's own root, not this gem's
21
21
  # __dir__ — Framework::ROOT can be a fixed, `__dir__`-relative constant
22
22
  # because framework members ship inside this gem; an embryonaut bluebook
23
- # ships inside the CONSUMER's own checkout instead, at
23
+ # ships inside the consumer's own checkout instead, at
24
24
  # `<registry.root>/vendor/embryonaut_bluebooks/<name>/bluebook/`. There
25
25
  # is no fixed answer until a registry (and its root) actually exists, so
26
26
  # this resolves lazily, per call — the same reason `uses_framework`
27
27
  # itself only runs at hecksagon-build time, when a real registry is
28
28
  # current.
29
29
  #
30
- # EVERY `.bluebook` FILE IN THE PACKAGE, SORTED — not just one. Unlike a
30
+ # Every `.bluebook` file in the package, sorted — not just one. Unlike a
31
31
  # framework member (one file, named by its own stem), a vendored package
32
- # can span several bluebook files that reopen the SAME `Hecks.bluebook`
32
+ # can span several bluebook files that reopen the same `Hecks.bluebook`
33
33
  # (embryonaut_bluebooks/payments/bluebook/{payment,payments,policies}
34
34
  # .bluebook all reopen "Payments"). Load order matters — policies
35
35
  # .bluebook names `Payment::Succeed` and needs the aggregate already
@@ -37,16 +37,16 @@ module Hecks
37
37
  # payment < payments < policies, the same reason that package's own
38
38
  # files are named to fall in that order in the first place.
39
39
  #
40
- # ONLY THE BLUEBOOK FILES — same restriction Framework draws, same
41
- # reason: a `.hecksagon`/`.port`/`.adapter` is a WIRING decision
40
+ # **Only the bluebook files** — same restriction Framework draws, same
41
+ # reason: a `.hecksagon`/`.port`/`.adapter` is a wiring decision
42
42
  # (persistence, which processor adapter is bound) that belongs to
43
43
  # whoever is deploying, never baked into the vendored package itself.
44
44
  # embryonaut_bluebooks/payments ships its own mock `.hecksagon` for its
45
- # own spec suite; a consumer declares its OWN separate
45
+ # own spec suite; a consumer declares its own separate
46
46
  # `Hecks.hecksagon "Payments" do ... end` to bind real storage/adapters
47
47
  # — see Framework's own comment for the fuller reasoning, identical here.
48
48
  #
49
- # IDEMPOTENT THE SAME WAY Framework.load! IS — checked against the
49
+ # Idempotent the same way Framework.load! Is — checked against the
50
50
  # bluebook this package actually declares (`Naming.pascal("payments")`
51
51
  # => "Payments"), not a separate ledger. A vendored package's directory
52
52
  # name and its declared `Hecks.bluebook` name are the one convention
@@ -2,7 +2,7 @@ require_relative "../runtime/errors"
2
2
 
3
3
  module Hecks
4
4
  module Facade
5
- # THE CLI DOORWHERE 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