hecks 0.2.0 → 0.3.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 (408) hide show
  1. checksums.yaml +5 -5
  2. data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +127 -0
  4. data/lib/hecks/adapters/driven/d1.adapter +12 -0
  5. data/lib/hecks/adapters/driven/d1.rb +403 -0
  6. data/lib/hecks/adapters/driven/folder.adapter +3 -0
  7. data/lib/hecks/adapters/driven/folder.rb +199 -0
  8. data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
  9. data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
  10. data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
  11. data/lib/hecks/adapters/driven/governance_authorization.rb +62 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +51 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +38 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +148 -0
  17. data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
  18. data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
  19. data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
  20. data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
  21. data/lib/hecks/adapters/driven/lambda.adapter +4 -0
  22. data/lib/hecks/adapters/driven/lambda.rb +131 -0
  23. data/lib/hecks/adapters/driven/memory.adapter +3 -0
  24. data/lib/hecks/adapters/driven/memory.rb +103 -0
  25. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
  26. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
  27. data/lib/hecks/adapters/driven/postgres/codec.rb +80 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +168 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +391 -0
  31. data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +171 -0
  32. data/lib/hecks/adapters/driven/postgres_era/lineage/field_cache.rb +190 -0
  33. data/lib/hecks/adapters/driven/postgres_era/lineage/head_compiler.rb +429 -0
  34. data/lib/hecks/adapters/driven/postgres_era/lineage/mint_transaction.rb +166 -0
  35. data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +286 -0
  36. data/lib/hecks/adapters/driven/postgres_era/lineage/resumable_backfill.rb +168 -0
  37. data/lib/hecks/adapters/driven/postgres_era/lineage/tail_merge.rb +163 -0
  38. data/lib/hecks/adapters/driven/postgres_era/lineage/transform_installer.rb +114 -0
  39. data/lib/hecks/adapters/driven/postgres_era/lineage.rb +137 -0
  40. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/coverage_check.rb +89 -0
  41. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +82 -0
  42. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  43. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +132 -0
  44. data/lib/hecks/adapters/driven/postgres_era/lineage_manager.rb +74 -0
  45. data/lib/hecks/adapters/driven/postgres_era.adapter +6 -0
  46. data/lib/hecks/adapters/driven/postgres_era.rb +671 -0
  47. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  48. data/lib/hecks/adapters/driven/prism.rb +80 -0
  49. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  50. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  51. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  52. data/lib/hecks/adapters/driven/sqlite/codec.rb +63 -0
  53. data/lib/hecks/adapters/driven/sqlite/projection.rb +105 -0
  54. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +180 -0
  55. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  56. data/lib/hecks/adapters/driven/sqlite.rb +285 -0
  57. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  58. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  59. data/lib/hecks/adapters/driven.rb +36 -0
  60. data/lib/hecks/adapters.rb +6 -0
  61. data/lib/hecks/behaviors/dsl.rb +96 -0
  62. data/lib/hecks/behaviors/expectations.rb +280 -0
  63. data/lib/hecks/behaviors/ir.rb +31 -0
  64. data/lib/hecks/behaviors/rspec.rb +42 -0
  65. data/lib/hecks/behaviors/runner.rb +96 -0
  66. data/lib/hecks/behaviors.rb +25 -0
  67. data/lib/hecks/bluebook/aggregate.rb +108 -0
  68. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  69. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  70. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  71. data/lib/hecks/bluebook/assembly/contracts.rb +409 -0
  72. data/lib/hecks/bluebook/assembly/marks.rb +227 -0
  73. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  74. data/lib/hecks/bluebook/assembly.rb +78 -0
  75. data/lib/hecks/bluebook/attribute.rb +96 -0
  76. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  77. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  78. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  79. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  80. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  81. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  82. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  83. data/lib/hecks/bluebook/behaviour/lifecycle.rb +51 -0
  84. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  85. data/lib/hecks/bluebook/behaviour/process_manager.rb +38 -0
  86. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  87. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  88. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  89. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  90. data/lib/hecks/bluebook/chapter.rb +68 -0
  91. data/lib/hecks/bluebook/command.rb +124 -0
  92. data/lib/hecks/bluebook/domain_port.rb +102 -0
  93. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  94. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +906 -0
  95. data/lib/hecks/bluebook/dsl/attribute_collector.rb +327 -0
  96. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  97. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1018 -0
  98. data/lib/hecks/bluebook/dsl/command_builder.rb +626 -0
  99. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  100. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  101. data/lib/hecks/bluebook/dsl/entity_builder.rb +305 -0
  102. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  103. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  104. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  105. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  106. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  107. data/lib/hecks/bluebook/dsl/policy_builder.rb +120 -0
  108. data/lib/hecks/bluebook/dsl/port_builder.rb +30 -0
  109. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  110. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +207 -0
  111. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  112. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  113. data/lib/hecks/bluebook/dsl/rule_reference.rb +172 -0
  114. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  115. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  116. data/lib/hecks/bluebook/dsl/word_gate.rb +221 -0
  117. data/lib/hecks/bluebook/dsl/world_builder.rb +77 -0
  118. data/lib/hecks/bluebook/dsl.rb +45 -0
  119. data/lib/hecks/bluebook/entity.rb +103 -0
  120. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -0
  121. data/lib/hecks/bluebook/expression/evaluator.rb +261 -0
  122. data/lib/hecks/bluebook/expression/projection.json +170 -0
  123. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  124. data/lib/hecks/bluebook/expression/resolver.rb +630 -0
  125. data/lib/hecks/bluebook/expression.rb +13 -0
  126. data/lib/hecks/bluebook/hexagon.rb +60 -0
  127. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  128. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  129. data/lib/hecks/bluebook/meta_validator/judge.rb +559 -0
  130. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  131. data/lib/hecks/bluebook/meta_validator/port_judge.rb +47 -0
  132. data/lib/hecks/bluebook/meta_validator/readings.rb +350 -0
  133. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +312 -0
  134. data/lib/hecks/bluebook/meta_validator/shapes.rb +246 -0
  135. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  136. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  137. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  138. data/lib/hecks/bluebook/meta_validator.rb +452 -0
  139. data/lib/hecks/bluebook/model_check.rb +318 -0
  140. data/lib/hecks/bluebook/pattern_subset.rb +120 -0
  141. data/lib/hecks/bluebook/policy.rb +41 -0
  142. data/lib/hecks/bluebook/process_manager.rb +86 -0
  143. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  144. data/lib/hecks/bluebook/project_loader.rb +40 -0
  145. data/lib/hecks/bluebook/project_register.rb +82 -0
  146. data/lib/hecks/bluebook/query.rb +61 -0
  147. data/lib/hecks/bluebook/read_model.rb +109 -0
  148. data/lib/hecks/bluebook/reference.rb +74 -0
  149. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  150. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  151. data/lib/hecks/bluebook/translation.rb +92 -0
  152. data/lib/hecks/bluebook/value_object.rb +58 -0
  153. data/lib/hecks/bluebook.rb +74 -0
  154. data/lib/hecks/codemod.rb +342 -0
  155. data/lib/hecks/construct.rb +71 -0
  156. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  157. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  158. data/lib/hecks/deploy/oidc.json +18 -0
  159. data/lib/hecks/doc/reference.rb +389 -0
  160. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  161. data/lib/hecks/facade/cli_door.rb +116 -0
  162. data/lib/hecks/facade/cli_runner.rb +190 -0
  163. data/lib/hecks/facade/command_request.rb +105 -0
  164. data/lib/hecks/facade/handle.rb +173 -0
  165. data/lib/hecks/facade/json_door.rb +154 -0
  166. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  167. data/lib/hecks/facade/surface/chapter.rb +107 -0
  168. data/lib/hecks/facade/surface.rb +48 -0
  169. data/lib/hecks/facade.rb +44 -0
  170. data/lib/hecks/forms/app.rb +300 -0
  171. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  172. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  173. data/lib/hecks/forms/field_renderer.rb +170 -0
  174. data/lib/hecks/forms/field_shape.rb +232 -0
  175. data/lib/hecks/forms/html.rb +53 -0
  176. data/lib/hecks/forms/index_renderer.rb +35 -0
  177. data/lib/hecks/forms/page.rb +157 -0
  178. data/lib/hecks/forms/params.rb +132 -0
  179. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  180. data/lib/hecks/forms/record_renderer.rb +115 -0
  181. data/lib/hecks/forms/record_table.rb +63 -0
  182. data/lib/hecks/forms/reference_options.rb +30 -0
  183. data/lib/hecks/forms/value_object_shape.rb +46 -0
  184. data/lib/hecks/forms.rb +54 -0
  185. data/lib/hecks/fqn.rb +94 -0
  186. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  187. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  188. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  189. data/lib/hecks/framework/bluebook/governance.bluebook +130 -0
  190. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  191. data/lib/hecks/framework/oidc.json +39 -0
  192. data/lib/hecks/framework.rb +90 -0
  193. data/lib/hecks/freezer.rb +67 -0
  194. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  195. data/lib/hecks/fuzzing/isolated_boot.rb +92 -0
  196. data/lib/hecks/fuzzing/properties.rb +1146 -0
  197. data/lib/hecks/fuzzing/replay.rb +635 -0
  198. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  199. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +54 -0
  200. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  201. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +185 -0
  202. data/lib/hecks/fuzzing/sequence_generator.rb +130 -0
  203. data/lib/hecks/fuzzing/value_generator.rb +177 -0
  204. data/lib/hecks/fuzzing.rb +12 -0
  205. data/lib/hecks/grammar/evolve.rb +291 -0
  206. data/lib/hecks/grammar/expression.bluebook +417 -0
  207. data/lib/hecks/grammar/expression_operators.json +1152 -0
  208. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  209. data/lib/hecks/grammar/oidc.json +69 -0
  210. data/lib/hecks/grammar/translation.bluebook +203 -0
  211. data/lib/hecks/grammar.rb +178 -0
  212. data/lib/hecks/ir.rb +126 -0
  213. data/lib/hecks/language/adapter.bluebook +116 -0
  214. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  215. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  216. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  217. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  218. data/lib/hecks/language/bluebook/command.bluebook +437 -0
  219. data/lib/hecks/language/bluebook/entity.bluebook +316 -0
  220. data/lib/hecks/language/bluebook/policy.bluebook +172 -0
  221. data/lib/hecks/language/bluebook/process_manager.bluebook +264 -0
  222. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  223. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  224. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  225. data/lib/hecks/language/bluebook/syntax.bluebook +447 -0
  226. data/lib/hecks/language/bluebook/vocabulary.bluebook +364 -0
  227. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  228. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  229. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  230. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  231. data/lib/hecks/language/oidc.json +313 -0
  232. data/lib/hecks/language/port.bluebook +92 -0
  233. data/lib/hecks/language/translation/translation.bluebook +110 -0
  234. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  235. data/lib/hecks/language/world/wiring.bluebook +62 -0
  236. data/lib/hecks/language/world/world.bluebook +84 -0
  237. data/lib/hecks/literal.rb +125 -0
  238. data/lib/hecks/naming.rb +121 -0
  239. data/lib/hecks/ports/access_control.port +4 -0
  240. data/lib/hecks/ports/access_control.rb +62 -0
  241. data/lib/hecks/ports/agent/answers.rb +104 -0
  242. data/lib/hecks/ports/agent.port +4 -0
  243. data/lib/hecks/ports/agent.rb +167 -0
  244. data/lib/hecks/ports/authentication.port +4 -0
  245. data/lib/hecks/ports/authentication.rb +50 -0
  246. data/lib/hecks/ports/authorization.port +4 -0
  247. data/lib/hecks/ports/authorization.rb +59 -0
  248. data/lib/hecks/ports/clock.port +4 -0
  249. data/lib/hecks/ports/clock.rb +62 -0
  250. data/lib/hecks/ports/extraction.port +4 -0
  251. data/lib/hecks/ports/extraction.rb +37 -0
  252. data/lib/hecks/ports/identity_assignment.port +4 -0
  253. data/lib/hecks/ports/identity_assignment.rb +45 -0
  254. data/lib/hecks/ports/identity_generation.port +4 -0
  255. data/lib/hecks/ports/identity_generation.rb +49 -0
  256. data/lib/hecks/ports/identity_resolution.port +4 -0
  257. data/lib/hecks/ports/identity_resolution.rb +40 -0
  258. data/lib/hecks/ports/loading.port +4 -0
  259. data/lib/hecks/ports/loading.rb +13 -0
  260. data/lib/hecks/ports/persistence/append_only.rb +102 -0
  261. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  262. data/lib/hecks/ports/persistence/execution.rb +18 -0
  263. data/lib/hecks/ports/persistence/lineage.rb +277 -0
  264. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  266. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  267. data/lib/hecks/ports/persistence.port +4 -0
  268. data/lib/hecks/ports/persistence.rb +39 -0
  269. data/lib/hecks/ports/projection.port +4 -0
  270. data/lib/hecks/ports/projection.rb +76 -0
  271. data/lib/hecks/ports/query/in_memory.rb +60 -0
  272. data/lib/hecks/ports/query/ordering.rb +41 -0
  273. data/lib/hecks/ports/query.rb +36 -0
  274. data/lib/hecks/ports.rb +27 -0
  275. data/lib/hecks/projections/diagrams.rb +413 -0
  276. data/lib/hecks/projections/ir.rb +18 -0
  277. data/lib/hecks/projections/model/deviations.rb +98 -0
  278. data/lib/hecks/projections/model.rb +145 -0
  279. data/lib/hecks/projections/oidc.rb +110 -0
  280. data/lib/hecks/projections/parser_table.rb +159 -0
  281. data/lib/hecks/projections/reference.rb +38 -0
  282. data/lib/hecks/projections/shape.rb +31 -0
  283. data/lib/hecks/projections/statements.rb +110 -0
  284. data/lib/hecks/projections/vocabulary.rb +100 -0
  285. data/lib/hecks/projections.rb +32 -0
  286. data/lib/hecks/projector/cli_projector.rb +437 -0
  287. data/lib/hecks/projector/docs_projector.rb +321 -0
  288. data/lib/hecks/projector/exporter.rb +141 -0
  289. data/lib/hecks/projector/ir_projector.rb +18 -0
  290. data/lib/hecks/projector/narrate_projector.rb +243 -0
  291. data/lib/hecks/projector/target.rb +97 -0
  292. data/lib/hecks/projector.rb +186 -0
  293. data/lib/hecks/query_ir.rb +392 -0
  294. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  295. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  296. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  297. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  298. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  299. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  300. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  301. data/lib/hecks/query_specification/common/null_policy.rb +80 -0
  302. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  303. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  304. data/lib/hecks/query_specification/common/options.rb +32 -0
  305. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  306. data/lib/hecks/query_specification/common/specification.rb +10 -0
  307. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  308. data/lib/hecks/query_specification/field_path.rb +89 -0
  309. data/lib/hecks/query_specification/hop_path.rb +130 -0
  310. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  311. data/lib/hecks/query_specification.rb +14 -0
  312. data/lib/hecks/rendering.rb +48 -0
  313. data/lib/hecks/router/namespace_installer.rb +157 -0
  314. data/lib/hecks/router.rb +70 -0
  315. data/lib/hecks/runtime/caller.rb +50 -0
  316. data/lib/hecks/runtime/capability_graph.rb +44 -0
  317. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +124 -0
  318. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +248 -0
  319. data/lib/hecks/runtime/command_interpreter.rb +413 -0
  320. data/lib/hecks/runtime/command_rules/admissibility.rb +307 -0
  321. data/lib/hecks/runtime/command_rules/arithmetic.rb +254 -0
  322. data/lib/hecks/runtime/command_rules/authorization.rb +64 -0
  323. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  324. data/lib/hecks/runtime/command_rules/references.rb +181 -0
  325. data/lib/hecks/runtime/command_rules.rb +28 -0
  326. data/lib/hecks/runtime/dependency_planning.rb +231 -0
  327. data/lib/hecks/runtime/dispatcher.rb +286 -0
  328. data/lib/hecks/runtime/entity_element.rb +253 -0
  329. data/lib/hecks/runtime/entity_interpreter.rb +228 -0
  330. data/lib/hecks/runtime/era_check.rb +136 -0
  331. data/lib/hecks/runtime/era_guard/shape_diff.rb +128 -0
  332. data/lib/hecks/runtime/era_guard.rb +193 -0
  333. data/lib/hecks/runtime/era_tamper.rb +61 -0
  334. data/lib/hecks/runtime/errors.rb +100 -0
  335. data/lib/hecks/runtime/event.rb +51 -0
  336. data/lib/hecks/runtime/identity.rb +128 -0
  337. data/lib/hecks/runtime/instance.rb +120 -0
  338. data/lib/hecks/runtime/interpreting.rb +69 -0
  339. data/lib/hecks/runtime/loader.rb +143 -0
  340. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  341. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  342. data/lib/hecks/runtime/query_interpreter.rb +254 -0
  343. data/lib/hecks/runtime/reaction_invocation.rb +245 -0
  344. data/lib/hecks/runtime/read_model_interpreter.rb +266 -0
  345. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  346. data/lib/hecks/runtime/reference_hop.rb +99 -0
  347. data/lib/hecks/runtime/refusal_wording.rb +124 -0
  348. data/lib/hecks/runtime/registry/saga_persistence.rb +70 -0
  349. data/lib/hecks/runtime/registry/verification.rb +158 -0
  350. data/lib/hecks/runtime/registry.rb +218 -0
  351. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  352. data/lib/hecks/runtime/routing.rb +96 -0
  353. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  354. data/lib/hecks/runtime/saga_interpreter.rb +301 -0
  355. data/lib/hecks/runtime/storage_shape.rb +120 -0
  356. data/lib/hecks/runtime/tenant_check.rb +84 -0
  357. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  358. data/lib/hecks/runtime/value/admission.rb +112 -0
  359. data/lib/hecks/runtime/value/coercion.rb +500 -0
  360. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  361. data/lib/hecks/runtime/value.rb +125 -0
  362. data/lib/hecks/runtime.rb +95 -0
  363. data/lib/hecks/translation/audit/approval_digest.rb +31 -0
  364. data/lib/hecks/translation/audit/layer_one.rb +37 -0
  365. data/lib/hecks/translation/audit/layer_two.rb +77 -0
  366. data/lib/hecks/translation/audit/unfed_report.rb +42 -0
  367. data/lib/hecks/translation/audit.rb +70 -0
  368. data/lib/hecks/translation/reattest.rb +72 -0
  369. data/lib/hecks/translation/rule_compiler.rb +120 -0
  370. data/lib/hecks/translation/scaffold/differ.rb +183 -0
  371. data/lib/hecks/translation/scaffold/renderer.rb +41 -0
  372. data/lib/hecks/translation/scaffold/writer.rb +28 -0
  373. data/lib/hecks/translation/scaffold.rb +30 -0
  374. data/lib/hecks/translation.rb +11 -0
  375. data/lib/hecks/version.rb +16 -0
  376. data/lib/hecks/vocabulary.rb +213 -0
  377. data/lib/hecks.rb +118 -6
  378. metadata +401 -222
  379. data/bin/hecks +0 -7
  380. data/bin/hecks-package +0 -65
  381. data/bin/hecks_console +0 -12
  382. data/bin/hecks_serverless +0 -6
  383. data/lib/cli/build.rb +0 -14
  384. data/lib/cli/command_runner.rb +0 -28
  385. data/lib/cli/console.rb +0 -10
  386. data/lib/cli/generate.rb +0 -37
  387. data/lib/cli/hecks-cli.rb +0 -27
  388. data/lib/cli/test.rb +0 -57
  389. data/lib/console/commands.rb +0 -8
  390. data/lib/console/hecks-console.rb +0 -1
  391. data/lib/packager/README.md +0 -0
  392. data/lib/packager/app_runner.rb +0 -21
  393. data/lib/packager/args.rb +0 -26
  394. data/lib/packager/compatibility/fixnum.rb +0 -6
  395. data/lib/packager/hecks.rb +0 -39
  396. data/lib/packager/query_runner.rb +0 -21
  397. data/lib/packager/resources/Dockerfile +0 -11
  398. data/lib/packager/resources/app_binary +0 -7
  399. data/lib/packager/resources/bundle_config +0 -3
  400. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  401. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  402. data/lib/serverless/Domain +0 -32
  403. data/lib/serverless/cli.rb +0 -75
  404. data/lib/serverless/resources/command_name.js +0 -5
  405. data/lib/serverless/resources/environment.js +0 -7
  406. data/lib/serverless/resources/handler.js.tt +0 -28
  407. data/lib/serverless/resources/run_binary.js +0 -22
  408. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,437 @@
1
+ require_relative "../naming"
2
+
3
+ module Hecks
4
+ module Projector
5
+ # A BLUEBOOK, PROJECTED AS ITS OWN COMMAND-LINE SURFACE.
6
+ #
7
+ # Every verb a domain declares is a subcommand; every argument is an
8
+ # option whose TYPE, whose admitted values and whose required-ness are
9
+ # already stated in the chapter. A hand-written CLI restates all of it and
10
+ # then drifts — and the first thing to drift is the help text, which is the
11
+ # only part anybody reads.
12
+ #
13
+ # WHAT IS PROJECTED, AND WHAT IS NOT. This answers the SURFACE — the verb
14
+ # tree, the argument spec, the usage text — and nothing executes here. One
15
+ # small generic runner (`bin/run`) boots a domain, asks for this, parses
16
+ # against it and dispatches.
17
+ #
18
+ # The alternative was generating an executable per domain, which is what
19
+ # `bin/project_rust` does for a whole runtime and would be the more
20
+ # spectacular version of this. It was not taken: a generated program is a
21
+ # second copy of the dispatch logic, and it needs regenerating on every
22
+ # bluebook edit — a second tax beside the era gate, paid for a file nobody
23
+ # reads. Projecting the surface keeps one dispatcher and a help text that
24
+ # cannot be stale, because it is computed at the moment it is printed.
25
+ #
26
+ # THE TYPING IS THE POINT. A CLI hands everything over as a String.
27
+ # `sequence.value=99` has to become the Integer 99 or the runtime refuses
28
+ # it, and the only honest place to learn that is the value object's own
29
+ # declared field type. A CLI that guessed — "it looks like a number" —
30
+ # would send 99 for a version string of "99" and be wrong in a way nobody
31
+ # could see.
32
+ module CliProjector
33
+ module_function
34
+
35
+ # TWO NAMESPACES, NOT ONE — `{ verbs:, questions:, usage: }`.
36
+ #
37
+ # A chapter may legally declare a command and a query of one name: the
38
+ # language namespaces them and `Banking::Account.Open` is both, in the
39
+ # corpus, today. A single flat list of subcommands has to pick one, and
40
+ # picking silently is how `Ticket.Filed` sat undetected in this
41
+ # repository for a day.
42
+ #
43
+ # So a question is asked with `ask`: `bin/run ask account.open`. It is
44
+ # this codebase own word — `Query::AskOption`, "the ask" — it is
45
+ # shell-safe where a `?` suffix would be eaten by globbing, and it makes
46
+ # the collision impossible rather than detected. It also reads as what it
47
+ # is: everything under `ask` changes nothing.
48
+ def call(bluebook:, options: {})
49
+ verbs = {}
50
+ questions = {}
51
+
52
+ bluebook.aggregates.each do |aggregate|
53
+ aggregate.commands.each { |c| claim(verbs, name_for(aggregate, c), command_spec(bluebook, aggregate, nil, c)) }
54
+ aggregate.queries.each { |q| claim(questions, name_for(aggregate, q), query_spec(bluebook, aggregate, nil, q)) }
55
+
56
+ aggregate.entities.each do |entity|
57
+ entity.commands.each { |c|
58
+ claim(verbs, name_for(aggregate, c, entity), command_spec(bluebook, aggregate, entity, c))
59
+ }
60
+ entity.queries.each { |q|
61
+ claim(questions, name_for(aggregate, q, entity), query_spec(bluebook, aggregate, entity, q))
62
+ }
63
+ end
64
+
65
+ # A PORT IS A VERB TOO, and leaving it off the map was a real gap
66
+ # rather than a tasteful omission. The runtime has always dispatched
67
+ # a port operation by exactly the same name as a command — the
68
+ # projection simply never listed one, so `run_specs` and `file`
69
+ # answered "no such verb" while working perfectly through Ruby.
70
+ #
71
+ # It matters most for the caller with no other door. An agent that
72
+ # may not shell out reaches this domain ONLY through the projected
73
+ # CLI, and a port it cannot see is a capability it does not have.
74
+ aggregate.ports.each do |port|
75
+ port.operations.each { |o| claim(verbs, name_for(aggregate, o), port_spec(bluebook, aggregate, port, o)) }
76
+ end
77
+ end
78
+
79
+ # A REPORT IS A QUESTION TOO, and leaving it off was the same gap the
80
+ # ports had: `Dispatcher#query` has always answered `Domain.ReportName`,
81
+ # the projection simply never listed one — so the composed reads worked
82
+ # from Ruby and did not exist for anybody whose only door is the command
83
+ # line.
84
+ #
85
+ # It matters most for exactly what a report is FOR. Every other question
86
+ # here answers with rows and leaves the arithmetic to the reader; a
87
+ # `group_by` report is the one that counts. An agent that cannot reach
88
+ # it can list bugs all day and never answer "how are we doing".
89
+ #
90
+ # ONE DOT, NOT TWO — a report belongs to the chapter rather than to any
91
+ # aggregate (that is what rootless means), so it is addressed
92
+ # `QualityControl.BugsByStatus` where a query is
93
+ # `QualityControl::Bug.Queue`. `Dispatcher#query` splits on precisely
94
+ # that difference.
95
+ bluebook.read_models.each do |model|
96
+ claim(questions, Naming.snake(model.hecks_name), report_spec(bluebook, model))
97
+ end
98
+
99
+ # THE SHORT SPELLING, WHERE IT CANNOT BE AMBIGUOUS. `pizzas
100
+ # create_pizza` rather than `pizzas order.create_pizza` — the
101
+ # aggregate is worth typing only when two of them declare the same
102
+ # verb, and in a one-aggregate domain it never is. Both spellings are
103
+ # always accepted; `names` maps every accepted one to its canonical
104
+ # key, and the display name is the shortest that is unambiguous.
105
+ shorten(verbs)
106
+ shorten(questions)
107
+
108
+ { verbs: verbs, questions: questions,
109
+ names: { command: aliases(verbs), question: aliases(questions) },
110
+ usage: usage(bluebook, verbs, questions, options) }
111
+ end
112
+
113
+ # A last segment is claimed only if exactly one verb ends in it. Two
114
+ # aggregates declaring `Close` keep `customer.close` and `account.close`,
115
+ # which is the honest answer — a CLI that picked one would be choosing
116
+ # for the caller.
117
+ def shorten(specs)
118
+ tails = specs.keys.group_by { |name| name.split(".").last }
119
+ specs.each do |name, spec|
120
+ tail = name.split(".").last
121
+ spec[:short] = tails[tail].length == 1 ? tail : name
122
+ end
123
+ end
124
+
125
+ def aliases(specs)
126
+ specs.each_with_object({}) do |(name, spec), map|
127
+ map[name] = name
128
+ map[spec[:short]] = name
129
+ end
130
+ end
131
+
132
+ # A NAME IS CLAIMED ONCE. A command and a query of one name are legal in
133
+ # a chapter — the language namespaces them — and ambiguous as
134
+ # subcommands. Refused here rather than silently resolving to whichever
135
+ # was walked first, which is how `Ticket.Filed` (a command) and
136
+ # `Ticket.Filed` (a query) sat undetected until something flattened them.
137
+ def claim(verbs, name, spec)
138
+ if verbs.key?(name)
139
+ raise Bluebook::DSL::Malformed,
140
+ "two verbs project to the command-line name #{name.inspect}: " \
141
+ "#{verbs[name][:verb]} and #{spec[:verb]} — rename one"
142
+ end
143
+
144
+ verbs[name] = spec
145
+ end
146
+
147
+ def name_for(aggregate, verb, entity = nil)
148
+ parts = [Naming.snake(aggregate.hecks_name)]
149
+ parts << Naming.snake(entity.hecks_name) if entity
150
+ parts << Naming.snake(verb.hecks_name)
151
+ parts.join(".")
152
+ end
153
+
154
+ def fqn(bluebook, aggregate, verb, entity = nil)
155
+ [bluebook.name, "::", aggregate.hecks_name, ".",
156
+ entity ? "#{entity.hecks_name}." : "", verb.hecks_name].join
157
+ end
158
+
159
+ # ── one verb ──────────────────────────────────────────────────────
160
+
161
+ def command_spec(bluebook, aggregate, entity, command)
162
+ holder = entity || aggregate
163
+ arguments = command.attributes.flat_map { |a| options_for(a, holder, aggregate) }
164
+ receiver = if entity
165
+ :entity
166
+ else
167
+ (command.creates? ? nil : :aggregate)
168
+ end
169
+ legacy_arguments = []
170
+
171
+ # THE RECEIVER IS NOT A COMMAND ARGUMENT. An aggregate command names
172
+ # its record through to; an entity command needs both the aggregate
173
+ # record and the entity element within it. Keeping those paths in the
174
+ # projected option list makes the human-facing request complete while
175
+ # CommandRequest can remove them before it builds with: from the
176
+ # command's declared facts.
177
+ #
178
+ # Existing aggregate scripts may still spell the receiver id=... .
179
+ # That alias is deliberately hidden from help and recorded separately
180
+ # as legacy_arguments; new help and examples teach only to=... .
181
+ if entity
182
+ arguments = [
183
+ { path: "to.aggregate", type: "String", required: true,
184
+ note: "id of the #{aggregate.hecks_name} holding the #{entity.hecks_name}" },
185
+ { path: "to.entity", type: "String", required: true,
186
+ note: "id of the #{entity.hecks_name} to act on" }
187
+ ] + arguments
188
+ elsif receiver == :aggregate
189
+ arguments = [{ path: "to", type: "String", required: true,
190
+ note: "id of the #{aggregate.hecks_name} to act on" }] + arguments
191
+ legacy_arguments = [{ path: "id", type: "String", required: true }]
192
+ end
193
+
194
+ { verb: fqn(bluebook, aggregate, command, entity), kind: :command,
195
+ summary: command.goal, role: command.role, creates: command.creates?,
196
+ receiver: receiver, legacy_receiver: (receiver == :aggregate ? :id : nil),
197
+ legacy_arguments: legacy_arguments,
198
+ refusals: refusals(command, holder), arguments: arguments }
199
+ end
200
+
201
+ # A PORT OPERATION READS AS A VERB BUT REPORTS AS A BOUNDARY.
202
+ #
203
+ # `creates: false` because it makes no record, and `refusals: []`
204
+ # because it has none in the sense every other verb means: a command's
205
+ # refusals are sentences the chapter will say back to you, and an
206
+ # outbound operation's failure is somebody else's sentence, unknowable
207
+ # from here.
208
+ #
209
+ # THE SUMMARY NAMES BOTH ENDINGS, which is the one thing a caller most
210
+ # needs and cannot infer. `run_specs` looks like it either works or
211
+ # errors; what it actually does is answer `SpecsCompleted` even when the
212
+ # suite is red, and refuse only when rspec could not run. Somebody
213
+ # reading `--help` should not have to open the hecksagon to find that
214
+ # out.
215
+ def port_spec(bluebook, aggregate, port, operation)
216
+ arguments = receiver_options(:aggregate, aggregate, nil) +
217
+ operation.attributes.flat_map { |a| options_for(a, aggregate, aggregate) }
218
+
219
+ # THE WIRE NAME CARRIES THE PORT, THE TYPED NAME DOES NOT.
220
+ #
221
+ # `Dispatcher#dispatch` splits a verb into head and sub and looks the
222
+ # head up as a port, so a port operation is addressed
223
+ # `Aggregate.Port.Operation` — three parts where a command has two.
224
+ # But nobody should have to type `sweep.toolchain.run_specs`: which
225
+ # port a verb goes out through is wiring, and the caller's business is
226
+ # what they want done. So the projection spells the verb in full and
227
+ # names it short, which is the same split `shorten` already makes.
228
+ { verb: [fqn(bluebook, aggregate, operation).sub(/\.[^.]+\z/, ""), port.name, operation.hecks_name].join("."),
229
+ kind: :command, creates: false, receiver: :aggregate, refusals: [],
230
+ role: operation.outbound? ? "#{aggregate.hecks_name} asking #{port.name}" : "#{port.name} telling #{aggregate.hecks_name}",
231
+ summary: port_summary(port, operation), arguments: arguments }
232
+ end
233
+
234
+ def port_summary(port, operation)
235
+ return "#{port.name} reports it; emits #{operation.emits.join(', ')}" unless operation.outbound?
236
+
237
+ "Ask #{port.name} — answers #{operation.answers}, refuses #{operation.refuses}"
238
+ end
239
+
240
+ # A ROOTLESS REPORT TAKES NOTHING; a rooted one takes the id of the
241
+ # record it is a view of, under the name the model gave that reference.
242
+ def report_spec(bluebook, model)
243
+ arguments =
244
+ if model.reference_target
245
+ [{ path: model.reference_name.to_s, type: "String", required: true,
246
+ note: "id of the #{model.reference_target} this is a view of" }]
247
+ else
248
+ []
249
+ end
250
+
251
+ { verb: "#{bluebook.name}.#{model.hecks_name}", kind: :query,
252
+ summary: model.description, arguments: arguments }
253
+ end
254
+
255
+ def query_spec(bluebook, aggregate, entity, query)
256
+ arguments = Array(query.to_h[:attributes]).flat_map do |declared|
257
+ attribute = query.attributes.find { |a| a.name.to_s == declared[:name].to_s }
258
+ attribute ? options_for(attribute, entity || aggregate, aggregate) : []
259
+ end
260
+
261
+ { verb: fqn(bluebook, aggregate, query, entity), kind: :query,
262
+ summary: query.description, arguments: arguments }
263
+ end
264
+
265
+ # ── one argument, flattened ───────────────────────────────────────
266
+
267
+ # A VALUE OBJECT BECOMES ONE OPTION PER FIELD, dotted. `commit` typed
268
+ # `CommitRef` is `--commit.value`, because that is the shape the runtime
269
+ # wants and a flat `--commit` would have to guess which field it meant.
270
+ # Single-field value objects — almost all of them — read fine either way,
271
+ # and the runner accepts the short form for exactly those.
272
+ # RECURSIVE, AND IT HAS TO BE. A value object may hold another one —
273
+ # pizzas' `Pizza` holds a `Price` and a `Size` — so stopping after one
274
+ # level produced `pizza.price_cents=1500` and sent the STRING "1500"
275
+ # where `{ cents: 1500 }` belonged.
276
+ #
277
+ # The runtime took it. `qa/FINDINGS.md` #2 is exactly that gap —
278
+ # `Value::Coercion.build` does not validate nested value objects — so a
279
+ # one-level CLI is not merely inconvenient, it is a machine for writing
280
+ # malformed records into a real store, which is what it did on its first
281
+ # run against the pizzas database.
282
+ def options_for(attribute, holder, aggregate, prefix = nil, optional = nil)
283
+ path = [prefix, attribute.name].compact.join(".")
284
+ optional = optional || attribute.optional?
285
+ return [reference_option(attribute)] if attribute.reference?
286
+
287
+ value_object = value_object_for(attribute, holder, aggregate)
288
+ return [scalar_option(path, attribute, optional)] unless value_object
289
+
290
+ # A LIST SAYS SO, ALL THE WAY DOWN TO ITS LEAVES.
291
+ #
292
+ # Without this a `list_of(Tag)` projected exactly like a single Tag:
293
+ # one option, `tags.value`, indistinguishable from a scalar. So the
294
+ # help said to pass one, `CliDoor#bury` overwrote the leaf each time,
295
+ # and passing two tags stored the second and lost the first WITHOUT
296
+ # SAYING ANYTHING. A missing argument is refused loudly; a forgotten
297
+ # one is not, which makes it the more expensive of the two by far.
298
+ #
299
+ # The flag is carried on the leaf rather than kept beside the
300
+ # attribute because the leaf is all `CliDoor` ever sees — it is handed
301
+ # a path and a spec, and reuniting them with the attribute that
302
+ # produced them would be a lookup that exists only to answer this.
303
+ fields = value_object.attributes.flat_map do |field|
304
+ nested = value_object_for(field, value_object, aggregate)
305
+ next options_for(field, value_object, aggregate, path, optional) if nested
306
+
307
+ scalar_option("#{path}.#{field.name}", field, optional || field.optional?,
308
+ enum: closed_members(value_object, field))
309
+ end
310
+
311
+ return fields unless attribute.list?
312
+
313
+ fields.map { |option| option.merge(list: true, note: [option[:note], "repeatable"].compact.join("; ")) }
314
+ end
315
+
316
+ def reference_option(attribute)
317
+ { path: attribute.name.to_s, type: "String", required: !attribute.optional?,
318
+ note: "id of a #{attribute.type.target_name}" }
319
+ end
320
+
321
+ def scalar_option(path, field, optional, enum: [])
322
+ option = { path: path, type: field.type.to_s, required: !optional }
323
+ option[:enum] = enum unless enum.empty?
324
+ option[:pattern] = field.pattern if field.respond_to?(:pattern) && field.pattern
325
+ option[:default] = field.default if field.respond_to?(:default) && !field.default.nil?
326
+ option
327
+ end
328
+
329
+ def closed_members(value_object, field)
330
+ return [] unless value_object.closed_set?
331
+
332
+ value_object.members.filter_map { |member| member[field.name] }.uniq
333
+ end
334
+
335
+ def value_object_for(attribute, holder, aggregate)
336
+ [holder, aggregate].compact.each do |scope|
337
+ next unless scope.respond_to?(:value_objects)
338
+
339
+ found = scope.value_objects.find { |v| v.hecks_name == attribute.type.to_s }
340
+ return found if found
341
+ end
342
+ nil
343
+ end
344
+
345
+ # Every way this verb can say no, in the chapter's own words — printed
346
+ # by `--help` before the caller spends a dispatch finding out.
347
+ def refusals(command, holder)
348
+ out = []
349
+ lifecycle = holder.lifecycle
350
+ froms = lifecycle && lifecycle.transitions.filter_map do |name, transition|
351
+ Array(transition.from) if name.to_s == command.hecks_name
352
+ end.flatten.uniq
353
+ out << "#{lifecycle.field} is not #{froms.join(' or ')}" if froms && !froms.empty?
354
+ out += command.attributes.select(&:reference?).map { |r| "no #{r.type.target_name} has that #{r.name}" }
355
+ out + command.givens.map(&:description)
356
+ end
357
+
358
+ # ── the help ──────────────────────────────────────────────────────
359
+
360
+ def usage(bluebook, verbs, questions, options)
361
+ program = options[:program] || "bin/run"
362
+ only = options[:verb]
363
+
364
+ # WHICH NAMESPACE, when both hold the name. `options[:ask]` says so;
365
+ # without it a `--help` for a question would print the command that
366
+ # shares its name, which banking has and which is how this was found.
367
+ if only
368
+ pool = options[:ask] ? questions : verbs
369
+ key = aliases(pool)[only] || (options[:ask] ? nil : aliases(questions)[only])
370
+ spec = pool[key] || questions[key]
371
+ return verb_help(program, spec[:short], spec, ask: options[:ask]) if spec
372
+ end
373
+
374
+ width = (verbs.values + questions.values).map { |spec| spec[:short].length }.max.to_i
375
+ out = ["#{bluebook.name} — #{bluebook.vision}", "",
376
+ " #{program} <verb> [name=value …] do something",
377
+ " #{program} ask <question> [name=value …] read something", ""]
378
+
379
+ out << "verbs:"
380
+ verbs.each_value { |spec| out << " #{spec[:short].ljust(width)} #{spec[:summary]}" }
381
+ out << ""
382
+ out << "questions (nothing here changes anything):"
383
+ questions.each_value { |spec| out << " #{spec[:short].ljust(width)} #{first_sentence(spec[:summary])}" }
384
+ out << ""
385
+ out << " #{program} <verb> --help what one verb wants, and every way it refuses"
386
+ out << " a verb can always be spelled in full — #{example_qualified(verbs)}"
387
+ out.join("\n")
388
+ end
389
+
390
+ def example_qualified(verbs)
391
+ name, spec = verbs.find { |key, value| key != value[:short] } || verbs.first
392
+ name ? "#{spec[:short]} is also #{name}" : ""
393
+ end
394
+
395
+ # A QUERY's `description` is written as a paragraph — it argues for why
396
+ # the list is worth reading. A verb table wants the first sentence of
397
+ # that argument; `--help` still prints the whole thing.
398
+ def first_sentence(text)
399
+ text.to_s.split(/(?<=\.)\s/).first.to_s
400
+ end
401
+
402
+ def verb_help(program, name, spec, ask: false)
403
+ out = ["#{name} — #{spec[:summary]}", ""]
404
+ out << "dispatches #{spec[:verb]}" if spec[:kind] == :command
405
+ out << "reads #{spec[:verb]}" if spec[:kind] == :query
406
+ out << "issued by #{spec[:role]}" if spec[:role]
407
+ out << ""
408
+ invocation = ask ? "#{program} ask #{name}" : "#{program} #{name}"
409
+ out << " #{invocation}#{spec[:arguments].map { |a| " #{a[:path]}=…" }.join}"
410
+ out << ""
411
+
412
+ unless spec[:arguments].empty?
413
+ width = spec[:arguments].map { |a| a[:path].length }.max
414
+ spec[:arguments].each do |argument|
415
+ notes = []
416
+ notes << argument[:type]
417
+ notes << "one of #{argument[:enum].join(', ')}" if argument[:enum]
418
+ notes << "matches #{argument[:pattern]}" if argument[:pattern]
419
+ notes << "defaults to #{argument[:default].inspect}" unless argument[:default].nil?
420
+ notes << argument[:note] if argument[:note]
421
+ notes << "optional" unless argument[:required]
422
+ out << " #{argument[:path].ljust(width)} #{notes.join('; ')}"
423
+ end
424
+ out << ""
425
+ end
426
+
427
+ unless Array(spec[:refusals]).empty?
428
+ out << "refused when:"
429
+ spec[:refusals].each { |refusal| out << " #{refusal}" }
430
+ out << ""
431
+ end
432
+
433
+ out.join("\n")
434
+ end
435
+ end
436
+ end
437
+ end