hecks 3.14.0 → 4.0.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 (419) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/adapters/fake_processor.rb +34 -0
  3. data/examples/pizzas/adapters/payment_signature.rb +66 -0
  4. data/examples/pizzas/adapters/payment_webhook.rb +87 -0
  5. data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
  6. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  7. data/examples/pizzas/config.ru +22 -0
  8. data/lib/hecks/adapters/driven/prism.rb +0 -2
  9. data/lib/hecks/adapters/driven.rb +0 -2
  10. data/lib/hecks/adapters/driving/json.rb +1 -7
  11. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  12. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  13. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  14. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  15. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  16. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  17. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  18. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  19. data/lib/hecks/bluebook/translation.rb +18 -10
  20. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  21. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  22. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  23. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  24. data/lib/hecks/bluebook.rb +2 -0
  25. data/lib/hecks/chapters.rb +3 -3
  26. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  27. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  28. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  29. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  30. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  31. data/lib/hecks/composer/adapters/jev.rb +66 -0
  32. data/lib/hecks/composer/agent.rb +113 -0
  33. data/lib/hecks/composer/assistant.rb +110 -0
  34. data/lib/hecks/composer/behavior_run.rb +53 -0
  35. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  36. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  37. data/lib/hecks/composer/board.rb +65 -0
  38. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  39. data/lib/hecks/composer/board_edit/source.rb +84 -0
  40. data/lib/hecks/composer/board_edit.rb +144 -0
  41. data/lib/hecks/composer/check.rb +89 -0
  42. data/lib/hecks/composer/domains.rb +81 -0
  43. data/lib/hecks/composer/jobs.rb +134 -0
  44. data/lib/hecks/composer/layout.rb +37 -0
  45. data/lib/hecks/composer/live.rb +150 -0
  46. data/lib/hecks/composer/outline.rb +48 -0
  47. data/lib/hecks/composer/page.html +989 -0
  48. data/lib/hecks/composer/panels.rb +142 -0
  49. data/lib/hecks/composer/parse.rb +83 -0
  50. data/lib/hecks/composer/plain.rb +28 -0
  51. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  52. data/lib/hecks/composer/session.rb +141 -0
  53. data/lib/hecks/composer/toolbox.rb +166 -0
  54. data/lib/hecks/composer/web.rb +156 -0
  55. data/lib/hecks/composer/workbench.rb +51 -0
  56. data/lib/hecks/composer/workspace.rb +81 -0
  57. data/lib/hecks/composer.rb +17 -0
  58. data/lib/hecks/corpus/adapters.rb +22 -0
  59. data/lib/hecks/corpus/rust.rb +1 -1
  60. data/lib/hecks/corpus/worlds.rb +19 -0
  61. data/lib/hecks/corpus.rb +5 -0
  62. data/lib/hecks/forms/app/commands.rb +2 -3
  63. data/lib/hecks/forms/app/queries.rb +2 -3
  64. data/lib/hecks/forms/app/records.rb +17 -5
  65. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  66. data/lib/hecks/forms/field_controls.rb +2 -2
  67. data/lib/hecks/forms/field_renderer.rb +1 -1
  68. data/lib/hecks/forms/field_shape.rb +36 -17
  69. data/lib/hecks/forms/index_renderer.rb +2 -1
  70. data/lib/hecks/forms/page.rb +1 -1
  71. data/lib/hecks/forms/params.rb +0 -13
  72. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  73. data/lib/hecks/forms/record_renderer.rb +11 -7
  74. data/lib/hecks/forms/rule_words.rb +100 -0
  75. data/lib/hecks/forms/value_object_shape.rb +2 -1
  76. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  77. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  78. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  79. data/lib/hecks/gate/stages.yml +25 -0
  80. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  81. data/lib/hecks/grammar/evolve.rb +0 -3
  82. data/lib/hecks/grammar/translation.bluebook +1 -1
  83. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  84. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  85. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  86. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  87. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  88. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  89. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  90. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  91. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  92. data/lib/hecks/hecks/codebase.behaviors +6 -0
  93. data/lib/hecks/hecks/codebase.bluebook +21 -0
  94. data/lib/hecks/hecks/composer.behaviors +68 -0
  95. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  96. data/lib/hecks/hecks/custodian.behaviors +0 -6
  97. data/lib/hecks/hecks/custodian.bluebook +3 -80
  98. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  99. data/lib/hecks/hecks/hecks.world +1 -1
  100. data/lib/hecks/hecks/rehearsal.bluebook +7 -13
  101. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  102. data/lib/hecks/kernel_capabilities.rb +1 -1
  103. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  104. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  105. data/lib/hecks/ports/access_control.rb +2 -2
  106. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  107. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  108. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  109. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  110. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  111. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  112. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  113. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  114. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  115. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  116. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  117. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  118. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  119. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  120. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  121. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  122. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  123. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  124. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  125. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  126. data/lib/hecks/ports.rb +0 -1
  127. data/lib/hecks/projection_files.rb +3 -1
  128. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  129. data/lib/hecks/projections/glossary/entries.rb +10 -4
  130. data/lib/hecks/projections/glossary/groups.rb +42 -0
  131. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  132. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  133. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  134. data/lib/hecks/projections/glossary/html.rb +1 -2
  135. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  136. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  137. data/lib/hecks/projections/glossary/page.css +143 -153
  138. data/lib/hecks/projections/glossary/page.js +63 -3
  139. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  140. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  141. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  142. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  143. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  144. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  145. data/lib/hecks/projections/glossary.rb +2 -1
  146. data/lib/hecks/projections/plain_english.rb +97 -0
  147. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  148. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  149. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  150. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  151. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  152. data/lib/hecks/projections/statements/actions.rb +99 -0
  153. data/lib/hecks/projections/statements/relationships.rb +86 -0
  154. data/lib/hecks/projections/statements.rb +15 -24
  155. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  156. data/lib/hecks/projector/exporter/translations.rb +18 -35
  157. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  158. data/lib/hecks/projector/exporter.rb +5 -1
  159. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  160. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  161. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  162. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  163. data/lib/hecks/runtime/outbox.rb +2 -9
  164. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  165. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  166. data/lib/hecks/runtime/value.rb +13 -0
  167. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  168. data/lib/hecks/rust_build/wasm.rb +0 -12
  169. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  170. data/lib/hecks/rust_format.rb +77 -0
  171. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  172. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  173. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  174. data/lib/hecks/tools/translation_audit.rb +8 -10
  175. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  176. data/lib/hecks/version.rb +1 -1
  177. data/lib/hecks/vocabulary.rb +2 -1
  178. data/lib/hecks.rb +3 -0
  179. data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
  180. data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
  181. data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json} +1 -1
  182. data/rust/Cargo.toml +40 -0
  183. data/rust/build/Cargo.toml +36 -0
  184. data/rust/build/clippy.toml +4 -0
  185. data/rust/build/src/adapters_pass.rs +196 -0
  186. data/rust/build/src/binding_pass.rs +54 -13
  187. data/rust/build/src/build_artifact.rs +23 -5
  188. data/rust/build/src/cargo_sync.rs +69 -26
  189. data/rust/build/src/lineage_pass.rs +101 -87
  190. data/rust/build/src/main.rs +11 -2
  191. data/rust/build/src/pipeline.rs +80 -19
  192. data/rust/build/src/reserved_names.rs +6 -50
  193. data/rust/build/src/resolve.rs +82 -35
  194. data/rust/build/src/rustfmt.rs +100 -0
  195. data/rust/build/src/seams_pass.rs +85 -16
  196. data/rust/build/src/subprocess.rs +30 -6
  197. data/rust/build/src/tmp.rs +5 -2
  198. data/rust/build/src/translation_pass.rs +72 -22
  199. data/rust/build/src/world_pass.rs +109 -0
  200. data/rust/clippy.toml +4 -0
  201. data/rust/codegen/Cargo.toml +36 -0
  202. data/rust/codegen/clippy.toml +4 -0
  203. data/rust/codegen/src/asks.rs +40 -14
  204. data/rust/codegen/src/attr.rs +2 -2
  205. data/rust/codegen/src/bridging.rs +676 -117
  206. data/rust/codegen/src/commands.rs +979 -373
  207. data/rust/codegen/src/constraints.rs +88 -25
  208. data/rust/codegen/src/dependency_planning.rs +128 -34
  209. data/rust/codegen/src/domain_generator.rs +300 -170
  210. data/rust/codegen/src/exemplar.rs +87 -32
  211. data/rust/codegen/src/expr_emitter.rs +54 -6
  212. data/rust/codegen/src/fielded.rs +193 -41
  213. data/rust/codegen/src/hecks_naming.rs +124 -16
  214. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  215. data/rust/codegen/src/json.rs +80 -44
  216. data/rust/codegen/src/json_codec.rs +585 -149
  217. data/rust/codegen/src/literal.rs +104 -138
  218. data/rust/codegen/src/main.rs +145 -52
  219. data/rust/codegen/src/manifest.rs +76 -14
  220. data/rust/codegen/src/mutations.rs +1212 -239
  221. data/rust/codegen/src/naming.rs +111 -60
  222. data/rust/codegen/src/optional_pass.rs +90 -28
  223. data/rust/codegen/src/ports.rs +5 -4
  224. data/rust/codegen/src/prelude.rs +139 -49
  225. data/rust/codegen/src/queries.rs +601 -226
  226. data/rust/codegen/src/reactions.rs +268 -74
  227. data/rust/codegen/src/read_models.rs +606 -205
  228. data/rust/codegen/src/reference_specs.rs +16 -3
  229. data/rust/codegen/src/registry.rs +128 -79
  230. data/rust/codegen/src/reserved_names.rs +6 -50
  231. data/rust/codegen/src/shared.rs +71 -22
  232. data/rust/codegen/src/sidecars.rs +15 -5
  233. data/rust/codegen/src/skip_reason.rs +8 -2
  234. data/rust/codegen/src/types.rs +306 -108
  235. data/rust/host/Cargo.lock +1 -0
  236. data/rust/host/Cargo.toml +40 -4
  237. data/rust/host/HECKS_RELEASE +1 -1
  238. data/rust/host/clippy.toml +4 -0
  239. data/rust/host/src/api.rs +698 -218
  240. data/rust/host/src/auth.rs +491 -214
  241. data/rust/host/src/b64.rs +91 -0
  242. data/rust/host/src/bin/expr_harness.rs +6 -2
  243. data/rust/host/src/bin/lineage_harness.rs +47 -11
  244. data/rust/host/src/bin/mint_harness.rs +70 -37
  245. data/rust/host/src/bin/validate_harness.rs +16 -8
  246. data/rust/host/src/boot.rs +144 -68
  247. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  248. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  249. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  250. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  251. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  252. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  253. data/rust/host/src/checkout.rs +78 -32
  254. data/rust/host/src/clock.rs +8 -0
  255. data/rust/host/src/commerce_ir.rs +333 -95
  256. data/rust/host/src/crypto.rs +33 -0
  257. data/rust/host/src/dispatch.rs +990 -331
  258. data/rust/host/src/expr_json.rs +1021 -273
  259. data/rust/host/src/extension.rs +24 -4
  260. data/rust/host/src/field_hints.rs +11 -7
  261. data/rust/host/src/fuzz_support.rs +85 -13
  262. data/rust/host/src/host.rs +100 -0
  263. data/rust/host/src/http.rs +22 -0
  264. data/rust/host/src/ir.rs +326 -16
  265. data/rust/host/src/journal.rs +277 -176
  266. data/rust/host/src/lambda_client.rs +219 -53
  267. data/rust/host/src/lib.rs +16 -4
  268. data/rust/host/src/log.rs +62 -11
  269. data/rust/host/src/mint.rs +1153 -326
  270. data/rust/host/src/needs.rs +185 -44
  271. data/rust/host/src/payments/keystore.rs +197 -45
  272. data/rust/host/src/payments/roles.rs +39 -7
  273. data/rust/host/src/payments/tests.rs +1427 -282
  274. data/rust/host/src/payments.rs +391 -112
  275. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  276. data/rust/host/src/platform_signin/config.rs +11 -11
  277. data/rust/host/src/platform_signin/cookie.rs +2 -2
  278. data/rust/host/src/platform_signin/extension.rs +2 -1
  279. data/rust/host/src/platform_signin/handler.rs +9 -8
  280. data/rust/host/src/platform_signin/jti.rs +4 -2
  281. data/rust/host/src/platform_signin/jwks.rs +2 -1
  282. data/rust/host/src/platform_signin/jws.rs +2 -37
  283. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  284. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  285. data/rust/host/src/platform_signin/pages.rs +20 -20
  286. data/rust/host/src/platform_signin/session.rs +8 -1
  287. data/rust/host/src/platform_signin/settings.rs +6 -6
  288. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  289. data/rust/host/src/platform_signin/verify.rs +41 -41
  290. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  291. data/rust/host/src/presentation.rs +207 -63
  292. data/rust/host/src/presentation_write.rs +502 -187
  293. data/rust/host/src/query_step.rs +69 -18
  294. data/rust/host/src/rate_limit.rs +541 -142
  295. data/rust/host/src/reference_transform.rs +77 -25
  296. data/rust/host/src/reference_validate.rs +443 -104
  297. data/rust/host/src/resend.rs +190 -55
  298. data/rust/host/src/secrets.rs +116 -21
  299. data/rust/host/src/server.rs +483 -104
  300. data/rust/host/src/storage_shape.rs +88 -21
  301. data/rust/host/src/test_pg.rs +17 -7
  302. data/rust/host/src/ui_schema.rs +495 -207
  303. data/rust/host/src/wasm_runner.rs +21 -6
  304. data/rust/host/src/web/commerce.rs +101 -23
  305. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  306. data/rust/host/src/web/newsletter/template.rs +26 -7
  307. data/rust/host/src/web/newsletter/tests.rs +112 -23
  308. data/rust/host/src/web/newsletter.rs +209 -59
  309. data/rust/host/src/web/newsletter_send.rs +251 -79
  310. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  311. data/rust/host/src/web/registration_receipt.rs +100 -24
  312. data/rust/host/src/web/registrations.rs +1384 -332
  313. data/rust/host/src/web/resend_webhook.rs +138 -80
  314. data/rust/host/src/web.rs +2416 -508
  315. data/rust/lsp/Cargo.toml +36 -0
  316. data/rust/lsp/clippy.toml +4 -0
  317. data/rust/lsp/src/json.rs +29 -22
  318. data/rust/lsp/src/main.rs +81 -31
  319. data/rust/lsp/src/outline.rs +23 -7
  320. data/rust/lsp/src/rpc.rs +17 -4
  321. data/rust/parser/Cargo.toml +36 -0
  322. data/rust/parser/clippy.toml +4 -0
  323. data/rust/parser/src/build/closed_sets.rs +6 -1
  324. data/rust/parser/src/build/identity.rs +4 -3
  325. data/rust/parser/src/build/mod.rs +1 -1
  326. data/rust/parser/src/build/named_pattern.rs +4 -1
  327. data/rust/parser/src/build/naming.rs +2 -80
  328. data/rust/parser/src/build/pattern_subset.rs +6 -8
  329. data/rust/parser/src/build/query_derive.rs +3 -1
  330. data/rust/parser/src/build/query_inference.rs +14 -5
  331. data/rust/parser/src/build/read_model.rs +4 -1
  332. data/rust/parser/src/build/references.rs +4 -3
  333. data/rust/parser/src/canonical.rs +34 -15
  334. data/rust/parser/src/emit.rs +187 -105
  335. data/rust/parser/src/expr/ast_json.rs +134 -42
  336. data/rust/parser/src/expr/evaluator.rs +27 -9
  337. data/rust/parser/src/expr/mod.rs +54 -13
  338. data/rust/parser/src/expr/resolver.rs +299 -92
  339. data/rust/parser/src/ir.rs +35 -3
  340. data/rust/parser/src/keywords.rs +7168 -516
  341. data/rust/parser/src/lex.rs +11 -25
  342. data/rust/parser/src/lib.rs +1 -0
  343. data/rust/parser/src/main.rs +65 -6
  344. data/rust/parser/src/parse/adapter.rs +157 -0
  345. data/rust/parser/src/parse/aggregate.rs +19 -10
  346. data/rust/parser/src/parse/chapter.rs +16 -17
  347. data/rust/parser/src/parse/command.rs +35 -16
  348. data/rust/parser/src/parse/domain_port.rs +4 -7
  349. data/rust/parser/src/parse/entity.rs +21 -11
  350. data/rust/parser/src/parse/hecksagon.rs +10 -4
  351. data/rust/parser/src/parse/lifecycle.rs +9 -5
  352. data/rust/parser/src/parse/mod.rs +22 -91
  353. data/rust/parser/src/parse/needs.rs +21 -12
  354. data/rust/parser/src/parse/policy.rs +2 -2
  355. data/rust/parser/src/parse/process_manager.rs +9 -11
  356. data/rust/parser/src/parse/query.rs +8 -4
  357. data/rust/parser/src/parse/read_model.rs +57 -16
  358. data/rust/parser/src/parse/value_object.rs +7 -2
  359. data/rust/parser/src/parse/world.rs +322 -0
  360. data/rust/parser/src/ruby_value.rs +7 -6
  361. data/rust/src/exemplar/json.rs +4 -4
  362. data/rust/src/exemplar/mutations.rs +18 -0
  363. data/rust/src/exemplar/read_models.rs +1 -10
  364. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  365. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  366. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  367. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  368. data/rust/src/kernel/cli.rs +229 -112
  369. data/rust/src/kernel/dispatch.rs +323 -115
  370. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  371. data/rust/src/kernel/expr.rs +203 -86
  372. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  373. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  374. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  375. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  376. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  377. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  378. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  379. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  380. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  381. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  382. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  383. data/rust/src/kernel/expression_operators/text.rs +120 -27
  384. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  385. data/rust/src/kernel/json.rs +230 -148
  386. data/rust/src/kernel/mod.rs +46 -33
  387. data/rust/src/kernel/named_query.rs +63 -15
  388. data/rust/src/kernel/needs.rs +112 -25
  389. data/rust/src/kernel/orchestrate.rs +980 -581
  390. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  391. data/rust/src/kernel/pattern.rs +107 -34
  392. data/rust/src/kernel/query_comparators.rs +118 -41
  393. data/rust/src/kernel/query_ordering.rs +13 -5
  394. data/rust/src/kernel/read_model.rs +479 -181
  395. data/rust/src/kernel/reference_lookup.rs +122 -26
  396. data/rust/src/kernel/repository.rs +293 -66
  397. data/rust/src/kernel/routing.rs +58 -42
  398. data/rust/src/kernel/ruby_float.rs +139 -0
  399. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  400. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  401. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  402. data/rust/src/kernel/vocab/mod.rs +6 -6
  403. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  404. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  405. data/rust/src/lib.rs +50 -0
  406. data/rust/src/main.rs +4 -1
  407. data/rust/web/Cargo.toml +36 -0
  408. data/rust/web/clippy.toml +4 -0
  409. metadata +82 -15
  410. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  411. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  412. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  413. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  414. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  415. data/lib/hecks/ports/authentication.port +0 -6
  416. data/lib/hecks/ports/authentication.rb +0 -67
  417. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  418. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  419. data/rust/host/src/approval.rs +0 -652
@@ -0,0 +1,88 @@
1
+ require_relative "../../plain_english"
2
+ require_relative "../../statements"
3
+
4
+ module Hecks
5
+ module Projections
6
+ module Glossary
7
+ module Sentences
8
+ # The paragraphs an action's term carries: what it is for and who does it, what it needs,
9
+ # what it checks first, what it changes, what it guarantees after, and what it records.
10
+ module Actions
11
+ # How each kind of change is said, before the field it changes.
12
+ CHANGES = {
13
+ "set" => "sets", "append" => "adds to", "increment" => "raises", "decrement" => "lowers",
14
+ "multiply" => "scales", "clamp" => "limits", "remove" => "removes from",
15
+ "delegate" => "passes on to", "corrects" => "corrects"
16
+ }.freeze
17
+
18
+ module_function
19
+
20
+ # @param facts [Hash] the entry's facts: `:command`, `:holder` and `:value_objects`
21
+ # @param index [Glossary::Index] resolves links to other terms
22
+ # @return [Array<String>] the paragraphs, each present only when it has something to say
23
+ def paragraphs(facts, index)
24
+ command, holder = facts.values_at(:command, :holder)
25
+ [Sentences.command_sentence(command), needs(command, index, facts[:within]),
26
+ checks(holder, command), changes(holder, command), afterwards(command),
27
+ records(command, index)].compact
28
+ end
29
+
30
+ # "Needs a topping (Topping name) and an amount (Topping amount)."
31
+ def needs(command, index, within)
32
+ fields = command.attributes.map { |field| need_phrase(field, index, within) }
33
+ fields.empty? ? nil : "Needs #{Naming.to_sentence_list(fields)}."
34
+ end
35
+
36
+ def need_phrase(field, index, within)
37
+ name = PlainEnglish.noun(field.name)
38
+ name = PlainEnglish.a(name) unless field.list?
39
+ detail = [ValueObjects.type_words(field, index, within), ("optional" if field.optional?)].compact
40
+ "#{name} (#{detail.join(", ")})"
41
+ end
42
+
43
+ # "Allowed only when: the order is available; at most 10 toppings."
44
+ def checks(holder, command)
45
+ rules = Statements::Actions.checks(holder, command, said_by_move: false)
46
+ rules.empty? ? nil : "Allowed only when: #{rules.join("; ")}."
47
+ end
48
+
49
+ # "Sets the customer name and moves the order from available to sold."
50
+ def changes(holder, command)
51
+ parts = command.mutations.group_by { |mutation| CHANGES.fetch(mutation.op.to_s, "changes") }
52
+ .map { |verb, mutations| change_phrase(verb, mutations) }
53
+ parts << move(holder, command)
54
+ parts.compact!
55
+ parts.empty? ? nil : "#{PlainEnglish.upper(Naming.to_sentence_list(parts))}."
56
+ end
57
+
58
+ # "sets the name and the pizza"
59
+ def change_phrase(verb, mutations)
60
+ fields = mutations.map { |mutation| "the #{PlainEnglish.noun(mutation.target.to_s.split(".").last)}" }
61
+ "#{verb} #{Naming.to_sentence_list(fields.uniq)}"
62
+ end
63
+
64
+ # The lifecycle move a command makes, as "moves the order from available to sold".
65
+ def move(holder, command)
66
+ lifecycle = holder.respond_to?(:lifecycle) && holder.lifecycle
67
+ return nil unless lifecycle
68
+
69
+ found = Statements::Actions.moves(lifecycle).find { |name, _move| name == command.hecks_name }
70
+ found && Statements::Actions.move_phrase(found.last, "the #{PlainEnglish.noun(holder.hecks_name)}")
71
+ end
72
+
73
+ # "Afterwards, always: the balance fell by exactly the amount."
74
+ def afterwards(command)
75
+ rules = command.ensures.map { |rule| Statements::Actions.text(rule) }
76
+ rules.empty? ? nil : "Afterwards, always: #{rules.join("; ")}."
77
+ end
78
+
79
+ # "Records Topping added."
80
+ def records(command, index)
81
+ events = command.emits.map { |event| index.link(:event, event.to_s.split(/::|\./).last) }
82
+ events.empty? ? nil : "Records #{Naming.to_sentence_list(events)}."
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,77 @@
1
+ require_relative "../../plain_english"
2
+
3
+ module Hecks
4
+ module Projections
5
+ module Glossary
6
+ module Sentences
7
+ # Speaks a dotted field path the way a reader names it. A one-field value object is
8
+ # named by its own name, so `pizza.price_cents.cents` is "price" and
9
+ # `customer_name.value` is "customer name".
10
+ class Paths
11
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity] the construct the path starts on
12
+ # @param value_objects [Array<Bluebook::ValueObject>] the value objects types resolve to
13
+ def initialize(holder, value_objects)
14
+ @holder = holder
15
+ @by_name = value_objects.to_h { |value_object| [value_object.hecks_name, value_object] }
16
+ end
17
+
18
+ # @param path [String, Symbol] a dotted field path
19
+ # @return [Boolean] whether it is the holder's lifecycle field
20
+ def lifecycle?(path)
21
+ lifecycle = @holder.respond_to?(:lifecycle) && @holder.lifecycle
22
+ lifecycle ? lifecycle.field.to_s == path.to_s : false
23
+ end
24
+
25
+ # @param path [String, Symbol] a dotted field path
26
+ # @return [String] how a reader names the field
27
+ def label(path)
28
+ hops = path.to_s.split("/")
29
+ return hops.map { |hop| PlainEnglish.noun(hop) }.join("'s ") if hops.size > 1
30
+
31
+ walk(path).first
32
+ end
33
+
34
+ # @param path [String, Symbol] a dotted field path
35
+ # @return [String, nil] the type the path ends on, such as "String", when it resolves
36
+ def type(path) = walk(path).last
37
+
38
+ private
39
+
40
+ # Follows each segment through the attributes and value objects it names. Inside a
41
+ # one-field value object the label already said is kept.
42
+ def walk(path)
43
+ segments = path.to_s.split(".")
44
+ state = { label: PlainEnglish.noun(segments.last), type: nil, attributes: @holder.attributes, named: false }
45
+ segments.each do |segment|
46
+ field = state[:attributes].find { |attribute| attribute.name.to_s == segment }
47
+ return [state[:label], nil] unless field
48
+
49
+ state = step(field, state)
50
+ end
51
+ [state[:label], leaf_type(state)]
52
+ end
53
+
54
+ # The primitive a path ends on: a value object compares by its one number, when it has
55
+ # exactly one, as the query runtime does.
56
+ def leaf_type(state)
57
+ attributes = state[:attributes]
58
+ return attributes.first.type.to_s if state[:named]
59
+
60
+ numbers = attributes.select { |attribute| %w[Integer Float].include?(attribute.type.to_s) }
61
+ numbers.one? ? numbers.first.type.to_s : state[:type]
62
+ end
63
+
64
+ # The state after one segment: its label, type, and where the next segment is looked up.
65
+ def step(field, state)
66
+ value_object = @by_name[field.type.to_s]
67
+ label = state[:named] ? state[:label] : PlainEnglish.noun(field.name)
68
+ single = value_object && value_object.attributes.size == 1
69
+ label = PlainEnglish.noun(value_object.hecks_name) if single
70
+ { label: label, type: field.type.to_s, attributes: value_object ? value_object.attributes : [],
71
+ named: single || false }
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,90 @@
1
+ require_relative "../../plain_english"
2
+ require_relative "paths"
3
+
4
+ module Hecks
5
+ module Projections
6
+ module Glossary
7
+ module Sentences
8
+ # The sentence a list's term carries, read from its filters, ordering and limit:
9
+ # "Orders that are available, sorted by pizza name, A to Z."
10
+ module Queries
11
+ # How each comparison is said between a field and its value.
12
+ OPERATORS = {
13
+ "eq" => "is", "ne" => "is not", "lt" => "is less than", "lte" => "is at most",
14
+ "gt" => "is more than", "gte" => "is at least", "in" => "is", "contains" => "includes"
15
+ }.freeze
16
+
17
+ module_function
18
+
19
+ # @param query [Bluebook::Query] the query
20
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity] the construct it lists
21
+ # @param value_objects [Array<Bluebook::ValueObject>] the aggregate's value objects
22
+ # @return [Array<String>] the author's description, when there is one, then the
23
+ # derived sentence
24
+ def paragraphs(query, holder, value_objects)
25
+ paths = Paths.new(holder, value_objects)
26
+ [query.description, sentence(query, holder, paths), limit(query)].compact
27
+ end
28
+
29
+ def sentence(query, holder, paths)
30
+ things = PlainEnglish.plural(PlainEnglish.noun(holder.hecks_name))
31
+ filters = query.wheres.map { |clause| filter(clause, paths) }
32
+ subject = filters.empty? ? "All #{things}" : "#{PlainEnglish.upper(things)} #{filters.join(" and ")}"
33
+ "#{subject}#{order(query, things, paths)}."
34
+ end
35
+
36
+ # "that are available" on the lifecycle field, otherwise "whose price is less than ...".
37
+ def filter(clause, paths)
38
+ operator = clause.op.to_s
39
+ value = value_words(clause.value, operator)
40
+ return "that are #{"not " if operator == "ne"}#{value}" if paths.lifecycle?(clause.field)
41
+ return "that have none #{value}" if operator == "none_in_state"
42
+
43
+ "whose #{paths.label(clause.field)} #{OPERATORS.fetch(operator, operator.tr("_", " "))} #{value}"
44
+ end
45
+
46
+ # A parameter is "the ceiling you give"; a list of states is "open or frozen".
47
+ def value_words(value, operator)
48
+ return "the #{PlainEnglish.noun(value)} you give" if value.is_a?(Symbol)
49
+ return PlainEnglish.states(value.to_s.split(",").map(&:strip)) if operator == "in"
50
+
51
+ PlainEnglish.state(value)
52
+ end
53
+
54
+ # ", sorted by price, lowest first", and where unset values go when that is declared.
55
+ def order(query, things, paths)
56
+ order_by = query.order_by
57
+ return "" unless order_by
58
+
59
+ label = paths.label(order_by.field)
60
+ words = direction(paths.type(order_by.field), order_by.direction.to_s == "desc")
61
+ ", sorted by #{label}#{words}#{nulls(query, things, label)}"
62
+ end
63
+
64
+ def direction(type, descending)
65
+ case type
66
+ when "String" then descending ? ", Z to A" : ", A to Z"
67
+ when "Integer", "Float" then descending ? ", highest first" : ", lowest first"
68
+ else descending ? ", in reverse order" : ""
69
+ end
70
+ end
71
+
72
+ def nulls(query, things, label)
73
+ mode = query.null_semantics&.mode.to_s
74
+ return "" unless %w[first last].include?(mode)
75
+
76
+ ", with #{things} that have no #{label} #{mode}"
77
+ end
78
+
79
+ # "Shows at most 50." when the list is capped.
80
+ def limit(query)
81
+ value = query.limit&.value
82
+ return nil unless value
83
+
84
+ value.is_a?(Symbol) ? "Shows at most the number you give." : "Shows at most #{value}."
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -1,6 +1,7 @@
1
1
  require_relative "../../../naming"
2
2
  require_relative "../../statements"
3
3
  require_relative "../sensitivity"
4
+ require_relative "../../../bluebook/named_pattern"
4
5
 
5
6
  module Hecks
6
7
  module Projections
@@ -14,6 +15,13 @@ module Hecks
14
15
  "Boolean" => "yes or no", "TrueClass" => "yes or no", "FalseClass" => "yes or no"
15
16
  }.freeze
16
17
 
18
+ # What each named pattern asks of a value, in words.
19
+ PATTERN_WORDS = {
20
+ present: "must not be blank", not_empty: "must not be empty", no_whitespace: "must have no spaces",
21
+ snake_name: "must be lowercase words joined by underscores", git_sha: "must be a commit id",
22
+ digits: "must be digits only"
23
+ }.freeze
24
+
17
25
  module_function
18
26
 
19
27
  def paragraphs(value_object, index, within, sensitive = {})
@@ -28,7 +36,7 @@ module Hecks
28
36
  attributes = value_object.attributes
29
37
  return "A marker with no details of its own." if attributes.empty?
30
38
 
31
- return "#{Sentences.upper_first(type_words(attributes.first, index, within))}." if bare_value?(attributes)
39
+ return bare_sentence(attributes.first, index, within) if bare_value?(attributes)
32
40
 
33
41
  fields = attributes.map { |field| field_phrase(field, index, within, sensitive[field.name.to_s]) }
34
42
  "Made up of #{Naming.to_sentence_list(fields)}."
@@ -36,18 +44,37 @@ module Hecks
36
44
 
37
45
  def bare_value?(attributes) = attributes.size == 1 && attributes.first.name.to_s == "value"
38
46
 
39
- # "amount (a whole number)", or "medications (text, PHI)" when marked sensitive.
47
+ # "Text that must not be blank.", or just "A whole number." when nothing constrains it.
48
+ def bare_sentence(field, index, within)
49
+ rule = constraint(field)
50
+ "#{Sentences.upper_first(type_words(field, index, within))}#{" that #{rule}" if rule}."
51
+ end
52
+
53
+ # "amount (a whole number)", "name (text, must not be blank)", or, when marked
54
+ # sensitive, "medications (text, PHI)".
40
55
  def field_phrase(field, index, within, marking = nil)
41
- detail = [type_words(field, index, within), (Sensitivity.tag(marking) if marking)].compact.join(", ")
56
+ detail = [type_words(field, index, within), constraint(field), ("optional" if field.optional?),
57
+ (Sensitivity.tag(marking) if marking)].compact.join(", ")
42
58
  "#{Naming.words(field.name).downcase} (#{detail})"
43
59
  end
44
60
 
61
+ # A plain type in words, a reference as a link to what it points at, otherwise a link
62
+ # to the value's own term.
45
63
  def type_words(field, index, within)
46
- type = field.type.to_s
47
- inner = TYPE_WORDS[type] || index.link(:value_object, type, within: within)
64
+ inner = if field.reference? then index.link(:aggregate, field.type.target_name)
65
+ else TYPE_WORDS[field.type.to_s] || index.link(:value_object, field.type.to_s, within: within)
66
+ end
48
67
  field.list? ? "a list of #{inner}" : inner
49
68
  end
50
69
 
70
+ # What a field's pattern asks of its value, or nil when it has none.
71
+ def constraint(field)
72
+ return nil unless field.pattern
73
+
74
+ name = Bluebook::NamedPattern::TABLE.key(field.pattern)
75
+ name ? PATTERN_WORDS[name] : "must follow a set format"
76
+ end
77
+
51
78
  # A multi-field row leads with its first field and keeps the rest beside it,
52
79
  # so no row loses what makes it distinct.
53
80
  def closed_set_sentence(members)
@@ -2,6 +2,8 @@ require_relative "../../naming"
2
2
  require_relative "../statements"
3
3
  require_relative "sensitivity"
4
4
  require_relative "sentences/value_objects"
5
+ require_relative "sentences/actions"
6
+ require_relative "sentences/queries"
5
7
 
6
8
  module Hecks
7
9
  module Projections
@@ -12,8 +14,6 @@ module Hecks
12
14
  module Sentences
13
15
  # The one sentence each kind of term other than an entity or value object carries.
14
16
  SINGLES = {
15
- command: ->(facts, _index) { command_sentence(facts[:command]) },
16
- query: ->(facts, _index) { facts[:query].description },
17
17
  event: ->(facts, index) { event_sentence(facts, index) },
18
18
  policy: ->(facts, index) { policy_sentence(facts[:policy], index) },
19
19
  saga: ->(facts, index) { saga_sentence(facts[:saga], index) },
@@ -28,6 +28,8 @@ module Hecks
28
28
  case entry.kind
29
29
  when :entity then holder_paragraphs(facts[:entity])
30
30
  when :value_object then value_object_paragraphs(entry, index)
31
+ when :command then Actions.paragraphs(facts, index)
32
+ when :query then Queries.paragraphs(facts[:query], facts[:holder], facts[:value_objects])
31
33
  else [SINGLES.fetch(entry.kind).call(facts, index)]
32
34
  end.compact
33
35
  end
@@ -67,7 +69,7 @@ module Hecks
67
69
  # A cross-domain trigger (`across "Compliance"`) is spoken as words with the
68
70
  # domain named, since there is nothing here to link to.
69
71
  def policy_sentence(policy, index)
70
- sentence = "When #{index.link(:event, bare(policy.on_event))} happens, #{asked_phrase(policy, index)}"
72
+ sentence = "When #{trigger_phrase(bare(policy.on_event), index)}, #{asked_phrase(policy, index)}"
71
73
  if policy.for_each
72
74
  query_holder, query = split_trigger(policy.for_each)
73
75
  sentence += ", once for each row of #{index.link(:query, query, within: query_holder)}"
@@ -75,6 +77,14 @@ module Hecks
75
77
  "#{sentence}."
76
78
  end
77
79
 
80
+ # "Topping added happens", or, for an event nothing here records, its name in quotes
81
+ # and where it comes from.
82
+ def trigger_phrase(event, index)
83
+ return "#{index.link(:event, event)} happens" if index[:event, event]
84
+
85
+ "\"#{Naming.words(event)}\" arrives from outside"
86
+ end
87
+
78
88
  # What the policy asks of whom, linked unless the trigger crosses into another domain.
79
89
  def asked_phrase(policy, index)
80
90
  holder, command = split_trigger(policy.trigger_command)
@@ -14,7 +14,10 @@ module Hecks
14
14
  take.call(Markdown.title(bluebook))
15
15
  sections.each do |section|
16
16
  section.slug = take.call(section.title)
17
- section.terms.each { |entry| entry.slug = take.call(entry.headword) }
17
+ Groups.walk(section).each do |kind, item|
18
+ slug = take.call(kind == :group ? item : item.headword)
19
+ item.slug = slug unless kind == :group
20
+ end
18
21
  end
19
22
  end
20
23
 
@@ -2,6 +2,7 @@ require_relative "../projector"
2
2
  require_relative "../naming"
3
3
  require_relative "statements"
4
4
  require_relative "glossary/sections"
5
+ require_relative "glossary/groups"
5
6
  require_relative "glossary/sensitivity"
6
7
  require_relative "glossary/sentences"
7
8
  require_relative "glossary/mermaid"
@@ -77,7 +78,7 @@ module Hecks
77
78
  def with_headwords(entries, section_name)
78
79
  entries.each { |entry| entry.headword = Naming.words(entry.name) }
79
80
  entries.group_by(&:headword).each_value { |group| qualify_repeats(group, section_name) }
80
- entries.sort_by { |entry| [entry.headword.downcase, entry.kind.to_s] }
81
+ entries.sort_by { |entry| [Groups.rank(entry), entry.headword.downcase, entry.kind.to_s] }
81
82
  end
82
83
 
83
84
  def qualify_repeats(group, section_name)
@@ -0,0 +1,97 @@
1
+ require_relative "../naming"
2
+
3
+ module Hecks
4
+ module Projections
5
+ # The few pieces of English grammar the statements and the glossary share: how a construct
6
+ # name is spoken as a noun, and how a command is spoken as something being done.
7
+ #
8
+ # PlainEnglish.doing("AddTopping", "Order") # => "adding a topping"
9
+ # PlainEnglish.doing("Purchase", "Order") # => "purchasing an order"
10
+ module PlainEnglish
11
+ # Nouns that take no article: "accruing interest", never "accruing an interest".
12
+ UNCOUNTED = %w[interest money cash information data feedback equipment evidence].freeze
13
+
14
+ # Two-syllable verbs stressed on the last syllable, which double their final consonant.
15
+ DOUBLED = %w[submit commit admit permit omit refer prefer transfer occur control patrol compel
16
+ expel regret equip rebut forget begin cancel].freeze
17
+
18
+ # One-word verbs that act on their holder through a preposition: "withdrawing from a
19
+ # card", never "withdrawing a card".
20
+ THROUGH = { "withdraw" => "from", "deposit" => "into" }.freeze
21
+
22
+ # Verbs written as one word whose "-ing" form splits back into a verb and a particle.
23
+ PHRASAL = {
24
+ "chargeback" => "charging back", "rollback" => "rolling back", "backup" => "backing up",
25
+ "setup" => "setting up", "signup" => "signing up", "checkout" => "checking out",
26
+ "login" => "logging in", "logout" => "logging out", "payout" => "paying out"
27
+ }.freeze
28
+
29
+ module_function
30
+
31
+ # A construct name as a lowercase noun phrase; acronyms keep their capitals.
32
+ #
33
+ # @param name [String, Symbol] a construct or field name, such as `ATMCard` or `daily_limit`
34
+ # @return [String] the spoken noun, such as "ATM card" or "daily limit"
35
+ def noun(name)
36
+ Naming.words(name).split.map { |word| word.match?(/\A[A-Z]{2,}\z/) ? word : word.downcase }.join(" ")
37
+ end
38
+
39
+ # @param phrase [String] a spoken noun phrase
40
+ # @return [String] the phrase with its indefinite article, such as "an order"
41
+ def a(phrase) = "#{Naming.a_or_an(phrase)} #{phrase}"
42
+
43
+ # @param phrase [String] a spoken noun phrase
44
+ # @return [String] the phrase with its last word pluralised, such as "ATM cards"
45
+ def plural(phrase)
46
+ *head, last = phrase.split
47
+ [*head, Naming.plural(last)].join(" ")
48
+ end
49
+
50
+ # A state name as it is said: `charged_back` -> "charged back".
51
+ def state(name) = name.to_s.tr("_", " ")
52
+
53
+ # @param states [Array<String>] state names
54
+ # @return [String] the spoken states joined with "or"
55
+ def states(states) = Naming.to_sentence_list(states.map { |name| state(name) }, conj: "or")
56
+
57
+ # @param text [String] a sentence fragment
58
+ # @return [String] the fragment with its first letter capitalised
59
+ def upper(text) = text.sub(/\A[[:lower:]]/, &:upcase)
60
+
61
+ # A command as the act of doing it. A one-word command takes its holder as the object;
62
+ # a command named as a past state ("Debited") reads as marking the holder so.
63
+ #
64
+ # @param command_name [String] the command's name, such as `AddTopping`
65
+ # @param holder_name [String] the aggregate or entity the command is declared on
66
+ # @return [String] the lowercase phrase, such as "adding a topping"
67
+ def doing(command_name, holder_name)
68
+ verb, *rest = noun(command_name).split
69
+ holder = a(noun(holder_name))
70
+ return "marking #{holder} #{[verb, *rest].join(" ")}" if verb.end_with?("ed")
71
+ return [gerund(verb), THROUGH[verb], holder].compact.join(" ") if rest.empty?
72
+
73
+ "#{gerund(verb)} #{object(rest.join(" "))}"
74
+ end
75
+
76
+ # @param phrase [String] the words after a command's verb
77
+ # @return [String] the phrase with an article, unless it names something uncounted
78
+ def object(phrase) = UNCOUNTED.include?(phrase.split.last) ? phrase : a(phrase)
79
+
80
+ # The "-ing" form of a plain verb.
81
+ #
82
+ # @param verb [String] a lowercase verb, such as "purchase" or "log"
83
+ # @return [String] its "-ing" form, such as "purchasing" or "logging"
84
+ def gerund(verb)
85
+ return PHRASAL[verb] if PHRASAL.key?(verb)
86
+ return "#{verb[0..-3]}ying" if verb.end_with?("ie")
87
+ return "#{verb[0..-2]}ing" if verb.match?(/[^aeiouy]e\z/) || verb.end_with?("ue")
88
+ return "#{verb}#{verb[-1]}ing" if doubles?(verb)
89
+
90
+ "#{verb}ing"
91
+ end
92
+
93
+ # One vowel then one plain consonant ("log", "set") doubles; so do the stressed `DOUBLED`.
94
+ def doubles?(verb) = DOUBLED.include?(verb) || verb.match?(/\A[^aeiou]*[aeiou][bdgklmnprt]\z/)
95
+ end
96
+ end
97
+ end
@@ -163,8 +163,8 @@ module Hecks
163
163
  def local_name(argument) = "#{argument}_text"
164
164
 
165
165
  # @param argument [String] an argument's name
166
- # @return [String] the Rust field, raw-escaped when the name is a keyword
167
- def rust_field(argument) = RustVocabulary::RUST_KEYWORDS.include?(argument) ? "r##{argument}" : argument
166
+ # @return [String] the Rust field, escaped when the name is a keyword
167
+ def rust_field(argument) = RustVocabulary.field_name(argument)
168
168
  end
169
169
  end
170
170
  end
@@ -10,7 +10,7 @@ module Hecks
10
10
  " /// order given. A template is read, never evaluated. Private: call",
11
11
  " /// sites go through a site's typed `<Variant>Args::render_args`,",
12
12
  " /// which formats and supplies every declared argument.",
13
- " fn render(&self, values: &[(&str, &str)]) -> String {",
13
+ " fn render(self, values: &[(&str, &str)]) -> String {",
14
14
  " let mut text = self.template().to_string();",
15
15
  " for (key, value) in values {",
16
16
  " text = text.replace(&format!(\"{{{key}}}\"), value);",
@@ -28,40 +28,38 @@ module Hecks
28
28
  def impl_lines(spec, variants, rows)
29
29
  enum = spec[:enum]
30
30
  ["impl #{enum} {",
31
- constants(enum, variants, spec[:kind]),
32
- rows.first.keys.map { |field| accessor(enum, field, variants, rows) },
33
- from_name(enum, spec[:kind]),
34
- extras(enum, variants, spec[:kind]),
31
+ constants(variants, spec[:kind]),
32
+ rows.first.keys.map { |field| accessor(field, variants, rows) },
33
+ from_name(spec[:kind]),
34
+ extras(variants, spec[:kind]),
35
35
  "}",
36
36
  ""]
37
37
  end
38
38
 
39
- # @param enum [String] the enum's name
40
39
  # @param variants [Array<String>] the variant names
41
40
  # @param kind [Symbol] `:order`, `:templates` or `:set`
42
41
  # @return [Array<String>] the `ORDER` or `ALL` constant, as Rust lines
43
- def constants(enum, variants, kind)
44
- list = variants.map { |variant| " #{enum}::#{variant}," }
42
+ def constants(variants, kind)
43
+ list = variants.map { |variant| " Self::#{variant}," }
45
44
  if kind == :order
46
45
  [" /// The declared step order, first to last.",
47
- " pub const ORDER: [#{enum}; #{variants.size}] = [", list, " ];", ""]
46
+ " pub const ORDER: [Self; #{variants.size}] = [", list, " ];", ""]
48
47
  else
49
48
  [" /// Every row, in declared order.",
50
- " pub const ALL: &'static [#{enum}] = &[", list, " ];", ""]
49
+ " pub const ALL: &'static [Self] = &[", list, " ];", ""]
51
50
  end
52
51
  end
53
52
 
54
- # @param enum [String] the enum's name
55
53
  # @param field [String] the row field the accessor reads
56
54
  # @param variants [Array<String>] the variant names
57
55
  # @param rows [Array<Hash>] the table's rows
58
56
  # @return [Array] the accessor, as Rust lines
59
- def accessor(enum, field, variants, rows)
57
+ def accessor(field, variants, rows)
60
58
  arms = variants.zip(rows).map do |variant, row|
61
- " #{enum}::#{variant} => #{RustVocabulary.rust_string(row.fetch(field))},"
59
+ " Self::#{variant} => #{RustVocabulary.rust_string(row.fetch(field))},"
62
60
  end
63
61
  [" /// The row's declared `#{field}`.",
64
- " pub fn #{RustVocabulary.accessor_name(field)}(&self) -> &'static str {",
62
+ " pub const fn #{RustVocabulary.accessor_name(field)}(&self) -> &'static str {",
65
63
  " match self {",
66
64
  arms,
67
65
  " }",
@@ -71,38 +69,35 @@ module Hecks
71
69
 
72
70
  # Built on the enum's own list and name accessor, not a string match, so no wildcard arm.
73
71
  #
74
- # @param enum [String] the enum's name
75
72
  # @param kind [Symbol] `:order`, `:templates` or `:set`
76
73
  # @return [Array<String>] `from_name`, as Rust lines
77
- def from_name(enum, kind)
74
+ def from_name(kind)
78
75
  list = kind == :order ? "ORDER" : "ALL"
79
76
  field = kind == :templates ? "site" : RustVocabulary.name_field(kind)
80
77
  [" /// The row whose `#{field}` is `name`, if any.",
81
- " pub fn from_name(name: &str) -> Option<#{enum}> {",
82
- " #{enum}::#{list}.iter().copied().find(|row| row.#{RustVocabulary.accessor_name(field)}() == name)",
78
+ " pub fn from_name(name: &str) -> Option<Self> {",
79
+ " Self::#{list}.iter().copied().find(|row| row.#{RustVocabulary.accessor_name(field)}() == name)",
83
80
  " }",
84
81
  ""]
85
82
  end
86
83
 
87
- # @param enum [String] the enum's name
88
84
  # @param variants [Array<String>] the variant names
89
85
  # @param kind [Symbol] `:order`, `:templates` or `:set`
90
86
  # @return [Array<String>] the methods only this kind of table has, as Rust lines
91
- def extras(enum, variants, kind)
87
+ def extras(variants, kind)
92
88
  case kind
93
- when :order then position_fn(enum, variants)
89
+ when :order then position_fn(variants)
94
90
  when :templates then RENDER_FN
95
91
  else []
96
92
  end
97
93
  end
98
94
 
99
- # @param enum [String] the enum's name
100
95
  # @param variants [Array<String>] the variant names
101
96
  # @return [Array] `position`, as Rust lines
102
- def position_fn(enum, variants)
103
- arms = variants.each_with_index.map { |variant, index| " #{enum}::#{variant} => #{index}," }
97
+ def position_fn(variants)
98
+ arms = variants.each_with_index.map { |variant, index| " Self::#{variant} => #{index}," }
104
99
  [" /// This step's index in `ORDER`.",
105
- " pub fn position(&self) -> usize {",
100
+ " pub const fn position(&self) -> usize {",
106
101
  " match self {",
107
102
  arms,
108
103
  " }",
@@ -59,11 +59,11 @@ module Hecks
59
59
 
60
60
  # @return [Array<String>] the test that every row is found by its own name
61
61
  def name_test(enum, kind)
62
- list = kind == :order ? "ORDER" : "ALL"
62
+ list = kind == :order ? "&#{enum}::ORDER" : "#{enum}::ALL"
63
63
  name = RustVocabulary.accessor_name(RustVocabulary.name_field(kind))
64
64
  [" #[test]",
65
65
  " fn every_row_is_found_by_its_own_name() {",
66
- " for row in #{enum}::#{list}.iter() {",
66
+ " for row in #{list} {",
67
67
  " assert_eq!(#{enum}::from_name(row.#{name}()), Some(*row));",
68
68
  " }",
69
69
  " }",