hecks 3.13.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 (440) 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.bluebook +4 -4
  7. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  8. data/examples/pizzas/config.ru +22 -0
  9. data/lib/hecks/adapters/driven/prism.rb +0 -2
  10. data/lib/hecks/adapters/driven.rb +0 -2
  11. data/lib/hecks/adapters/driving/json.rb +1 -7
  12. data/lib/hecks/bluebook/dsl/attribute_collector.rb +15 -5
  13. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  14. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  15. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  16. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  17. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  18. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  19. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  20. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  21. data/lib/hecks/bluebook/named_pattern.rb +33 -0
  22. data/lib/hecks/bluebook/translation.rb +18 -10
  23. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  24. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  25. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  26. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  27. data/lib/hecks/bluebook.rb +3 -0
  28. data/lib/hecks/chapters.rb +3 -3
  29. data/lib/hecks/cli/domain_stub.rb +1 -1
  30. data/lib/hecks/cli/interview_aggregate.rb +4 -4
  31. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  32. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  33. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  34. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  35. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  36. data/lib/hecks/composer/adapters/jev.rb +66 -0
  37. data/lib/hecks/composer/agent.rb +113 -0
  38. data/lib/hecks/composer/assistant.rb +110 -0
  39. data/lib/hecks/composer/behavior_run.rb +53 -0
  40. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  41. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  42. data/lib/hecks/composer/board.rb +65 -0
  43. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  44. data/lib/hecks/composer/board_edit/source.rb +84 -0
  45. data/lib/hecks/composer/board_edit.rb +144 -0
  46. data/lib/hecks/composer/check.rb +89 -0
  47. data/lib/hecks/composer/domains.rb +81 -0
  48. data/lib/hecks/composer/jobs.rb +134 -0
  49. data/lib/hecks/composer/layout.rb +37 -0
  50. data/lib/hecks/composer/live.rb +150 -0
  51. data/lib/hecks/composer/outline.rb +48 -0
  52. data/lib/hecks/composer/page.html +989 -0
  53. data/lib/hecks/composer/panels.rb +142 -0
  54. data/lib/hecks/composer/parse.rb +83 -0
  55. data/lib/hecks/composer/plain.rb +28 -0
  56. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  57. data/lib/hecks/composer/session.rb +141 -0
  58. data/lib/hecks/composer/toolbox.rb +166 -0
  59. data/lib/hecks/composer/web.rb +156 -0
  60. data/lib/hecks/composer/workbench.rb +51 -0
  61. data/lib/hecks/composer/workspace.rb +81 -0
  62. data/lib/hecks/composer.rb +17 -0
  63. data/lib/hecks/corpus/adapters.rb +22 -0
  64. data/lib/hecks/corpus/rust.rb +1 -1
  65. data/lib/hecks/corpus/worlds.rb +19 -0
  66. data/lib/hecks/corpus.rb +5 -0
  67. data/lib/hecks/deploy/bluebook/deploy.bluebook +34 -34
  68. data/lib/hecks/forms/app/commands.rb +2 -3
  69. data/lib/hecks/forms/app/queries.rb +2 -3
  70. data/lib/hecks/forms/app/records.rb +17 -5
  71. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  72. data/lib/hecks/forms/field_controls.rb +2 -2
  73. data/lib/hecks/forms/field_renderer.rb +1 -1
  74. data/lib/hecks/forms/field_shape.rb +36 -17
  75. data/lib/hecks/forms/index_renderer.rb +2 -1
  76. data/lib/hecks/forms/page.rb +1 -1
  77. data/lib/hecks/forms/params.rb +0 -13
  78. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  79. data/lib/hecks/forms/record_renderer.rb +11 -7
  80. data/lib/hecks/forms/rule_words.rb +100 -0
  81. data/lib/hecks/forms/value_object_shape.rb +2 -1
  82. data/lib/hecks/framework/bluebook/change.bluebook +2 -2
  83. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  84. data/lib/hecks/fuzzing/domain_generator/block_rendering.rb +1 -1
  85. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  86. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  87. data/lib/hecks/gate/stages.yml +25 -0
  88. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  89. data/lib/hecks/grammar/evolve.rb +0 -3
  90. data/lib/hecks/grammar/translation.bluebook +1 -1
  91. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  92. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  93. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  94. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  95. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  96. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  97. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  98. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  99. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  100. data/lib/hecks/hecks/codebase.behaviors +6 -0
  101. data/lib/hecks/hecks/codebase.bluebook +58 -37
  102. data/lib/hecks/hecks/composer.behaviors +68 -0
  103. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  104. data/lib/hecks/hecks/custodian.behaviors +0 -6
  105. data/lib/hecks/hecks/custodian.bluebook +15 -92
  106. data/lib/hecks/hecks/hecks.bluebook +2 -2
  107. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  108. data/lib/hecks/hecks/hecks.world +1 -1
  109. data/lib/hecks/hecks/rehearsal.bluebook +10 -16
  110. data/lib/hecks/hecks/tooling.bluebook +22 -22
  111. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  112. data/lib/hecks/kernel_capabilities.rb +1 -1
  113. data/lib/hecks/language/bluebook/aggregate.bluebook +1 -0
  114. data/lib/hecks/language/bluebook/command.bluebook +1 -0
  115. data/lib/hecks/language/bluebook/entity.bluebook +1 -0
  116. data/lib/hecks/language/bluebook/query.bluebook +1 -0
  117. data/lib/hecks/language/bluebook/shape.bluebook +8 -7
  118. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  119. data/lib/hecks/language/hecksagon/port_operation.bluebook +1 -0
  120. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  121. data/lib/hecks/ports/access_control.rb +2 -2
  122. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  123. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  124. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  125. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  126. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  127. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  128. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  129. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  130. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  131. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  132. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  133. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  134. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  135. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  136. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  137. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  138. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  139. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  140. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  141. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  142. data/lib/hecks/ports.rb +0 -1
  143. data/lib/hecks/projection_files.rb +3 -1
  144. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  145. data/lib/hecks/projections/glossary/entries.rb +10 -4
  146. data/lib/hecks/projections/glossary/groups.rb +42 -0
  147. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  148. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  149. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  150. data/lib/hecks/projections/glossary/html.rb +1 -2
  151. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  152. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  153. data/lib/hecks/projections/glossary/page.css +143 -153
  154. data/lib/hecks/projections/glossary/page.js +63 -3
  155. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  156. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  157. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  158. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  159. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  160. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  161. data/lib/hecks/projections/glossary.rb +2 -1
  162. data/lib/hecks/projections/plain_english.rb +97 -0
  163. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  164. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  165. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  166. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  167. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  168. data/lib/hecks/projections/statements/actions.rb +99 -0
  169. data/lib/hecks/projections/statements/relationships.rb +86 -0
  170. data/lib/hecks/projections/statements.rb +15 -24
  171. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  172. data/lib/hecks/projector/exporter/translations.rb +18 -35
  173. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  174. data/lib/hecks/projector/exporter.rb +5 -1
  175. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  176. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  177. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  178. data/lib/hecks/quality_control/quality_control.bluebook +51 -51
  179. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  180. data/lib/hecks/runtime/outbox.rb +2 -9
  181. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  182. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  183. data/lib/hecks/runtime/value.rb +13 -0
  184. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  185. data/lib/hecks/rust_build/wasm.rb +0 -12
  186. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  187. data/lib/hecks/rust_format.rb +77 -0
  188. data/lib/hecks/site/bluebook/site.bluebook +5 -5
  189. data/lib/hecks/sme/bluebook/sme.bluebook +12 -12
  190. data/lib/hecks/tickets/bluebook/tickets.bluebook +3 -3
  191. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  192. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  193. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  194. data/lib/hecks/tools/translation_audit.rb +8 -10
  195. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  196. data/lib/hecks/version.rb +1 -1
  197. data/lib/hecks/vocabulary.rb +2 -1
  198. data/lib/hecks.rb +3 -0
  199. data/prebuilt/{58208399bada10b0db99928c48e38789288fc58f7b71a72b21bfa386680e32ed.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
  200. data/prebuilt/{2b59737fbdbd5f44edda0fddce2611f45d76087b3a7c782d1f1a17ea58c77948.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
  201. data/prebuilt/verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json +1 -0
  202. data/rust/Cargo.toml +40 -0
  203. data/rust/build/Cargo.toml +36 -0
  204. data/rust/build/clippy.toml +4 -0
  205. data/rust/build/src/adapters_pass.rs +196 -0
  206. data/rust/build/src/binding_pass.rs +54 -13
  207. data/rust/build/src/build_artifact.rs +23 -5
  208. data/rust/build/src/cargo_sync.rs +69 -26
  209. data/rust/build/src/lineage_pass.rs +101 -87
  210. data/rust/build/src/main.rs +11 -2
  211. data/rust/build/src/pipeline.rs +80 -19
  212. data/rust/build/src/reserved_names.rs +6 -50
  213. data/rust/build/src/resolve.rs +82 -35
  214. data/rust/build/src/rustfmt.rs +100 -0
  215. data/rust/build/src/seams_pass.rs +85 -16
  216. data/rust/build/src/subprocess.rs +30 -6
  217. data/rust/build/src/tmp.rs +5 -2
  218. data/rust/build/src/translation_pass.rs +72 -22
  219. data/rust/build/src/world_pass.rs +109 -0
  220. data/rust/clippy.toml +4 -0
  221. data/rust/codegen/Cargo.toml +36 -0
  222. data/rust/codegen/clippy.toml +4 -0
  223. data/rust/codegen/src/asks.rs +40 -14
  224. data/rust/codegen/src/attr.rs +2 -2
  225. data/rust/codegen/src/bridging.rs +676 -117
  226. data/rust/codegen/src/commands.rs +979 -373
  227. data/rust/codegen/src/constraints.rs +88 -25
  228. data/rust/codegen/src/dependency_planning.rs +128 -34
  229. data/rust/codegen/src/domain_generator.rs +300 -170
  230. data/rust/codegen/src/exemplar.rs +87 -32
  231. data/rust/codegen/src/expr_emitter.rs +54 -6
  232. data/rust/codegen/src/fielded.rs +193 -41
  233. data/rust/codegen/src/hecks_naming.rs +124 -16
  234. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  235. data/rust/codegen/src/json.rs +80 -44
  236. data/rust/codegen/src/json_codec.rs +585 -149
  237. data/rust/codegen/src/literal.rs +104 -138
  238. data/rust/codegen/src/main.rs +145 -52
  239. data/rust/codegen/src/manifest.rs +76 -14
  240. data/rust/codegen/src/mutations.rs +1212 -239
  241. data/rust/codegen/src/naming.rs +111 -60
  242. data/rust/codegen/src/optional_pass.rs +90 -28
  243. data/rust/codegen/src/ports.rs +5 -4
  244. data/rust/codegen/src/prelude.rs +139 -49
  245. data/rust/codegen/src/queries.rs +601 -226
  246. data/rust/codegen/src/reactions.rs +268 -74
  247. data/rust/codegen/src/read_models.rs +606 -205
  248. data/rust/codegen/src/reference_specs.rs +16 -3
  249. data/rust/codegen/src/registry.rs +128 -79
  250. data/rust/codegen/src/reserved_names.rs +6 -50
  251. data/rust/codegen/src/shared.rs +71 -22
  252. data/rust/codegen/src/sidecars.rs +15 -5
  253. data/rust/codegen/src/skip_reason.rs +8 -2
  254. data/rust/codegen/src/types.rs +306 -108
  255. data/rust/host/Cargo.lock +1 -0
  256. data/rust/host/Cargo.toml +40 -4
  257. data/rust/host/HECKS_RELEASE +1 -1
  258. data/rust/host/clippy.toml +4 -0
  259. data/rust/host/src/api.rs +698 -218
  260. data/rust/host/src/auth.rs +491 -214
  261. data/rust/host/src/b64.rs +91 -0
  262. data/rust/host/src/bin/expr_harness.rs +6 -2
  263. data/rust/host/src/bin/lineage_harness.rs +47 -11
  264. data/rust/host/src/bin/mint_harness.rs +70 -37
  265. data/rust/host/src/bin/validate_harness.rs +16 -8
  266. data/rust/host/src/boot.rs +144 -68
  267. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  268. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  269. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  270. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  271. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  272. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  273. data/rust/host/src/checkout.rs +78 -32
  274. data/rust/host/src/clock.rs +8 -0
  275. data/rust/host/src/commerce_ir.rs +333 -95
  276. data/rust/host/src/crypto.rs +33 -0
  277. data/rust/host/src/dispatch.rs +990 -331
  278. data/rust/host/src/expr_json.rs +1021 -273
  279. data/rust/host/src/extension.rs +24 -4
  280. data/rust/host/src/field_hints.rs +11 -7
  281. data/rust/host/src/fuzz_support.rs +85 -13
  282. data/rust/host/src/host.rs +100 -0
  283. data/rust/host/src/http.rs +22 -0
  284. data/rust/host/src/ir.rs +326 -16
  285. data/rust/host/src/journal.rs +277 -176
  286. data/rust/host/src/lambda_client.rs +219 -53
  287. data/rust/host/src/lib.rs +16 -4
  288. data/rust/host/src/log.rs +62 -11
  289. data/rust/host/src/mint.rs +1153 -326
  290. data/rust/host/src/needs.rs +185 -44
  291. data/rust/host/src/payments/keystore.rs +197 -45
  292. data/rust/host/src/payments/roles.rs +191 -0
  293. data/rust/host/src/payments/tests.rs +1445 -275
  294. data/rust/host/src/payments.rs +409 -121
  295. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  296. data/rust/host/src/platform_signin/config.rs +11 -11
  297. data/rust/host/src/platform_signin/cookie.rs +2 -2
  298. data/rust/host/src/platform_signin/extension.rs +2 -1
  299. data/rust/host/src/platform_signin/handler.rs +9 -8
  300. data/rust/host/src/platform_signin/jti.rs +4 -2
  301. data/rust/host/src/platform_signin/jwks.rs +2 -1
  302. data/rust/host/src/platform_signin/jws.rs +2 -37
  303. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  304. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  305. data/rust/host/src/platform_signin/pages.rs +20 -20
  306. data/rust/host/src/platform_signin/session.rs +8 -1
  307. data/rust/host/src/platform_signin/settings.rs +6 -6
  308. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  309. data/rust/host/src/platform_signin/verify.rs +41 -41
  310. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  311. data/rust/host/src/presentation.rs +207 -63
  312. data/rust/host/src/presentation_write.rs +502 -187
  313. data/rust/host/src/query_step.rs +69 -18
  314. data/rust/host/src/rate_limit.rs +541 -142
  315. data/rust/host/src/reference_transform.rs +77 -25
  316. data/rust/host/src/reference_validate.rs +443 -104
  317. data/rust/host/src/resend.rs +190 -55
  318. data/rust/host/src/secrets.rs +116 -21
  319. data/rust/host/src/server.rs +483 -104
  320. data/rust/host/src/storage_shape.rs +88 -21
  321. data/rust/host/src/test_pg.rs +17 -7
  322. data/rust/host/src/ui_schema.rs +495 -207
  323. data/rust/host/src/wasm_runner.rs +21 -6
  324. data/rust/host/src/web/commerce.rs +101 -23
  325. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  326. data/rust/host/src/web/newsletter/template.rs +26 -7
  327. data/rust/host/src/web/newsletter/tests.rs +112 -23
  328. data/rust/host/src/web/newsletter.rs +209 -59
  329. data/rust/host/src/web/newsletter_send.rs +251 -79
  330. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  331. data/rust/host/src/web/registration_receipt.rs +100 -24
  332. data/rust/host/src/web/registrations.rs +1384 -332
  333. data/rust/host/src/web/resend_webhook.rs +138 -80
  334. data/rust/host/src/web.rs +2416 -508
  335. data/rust/lsp/Cargo.toml +36 -0
  336. data/rust/lsp/clippy.toml +4 -0
  337. data/rust/lsp/src/json.rs +29 -22
  338. data/rust/lsp/src/main.rs +81 -31
  339. data/rust/lsp/src/outline.rs +23 -7
  340. data/rust/lsp/src/rpc.rs +17 -4
  341. data/rust/parser/Cargo.toml +36 -0
  342. data/rust/parser/clippy.toml +4 -0
  343. data/rust/parser/src/build/closed_sets.rs +6 -1
  344. data/rust/parser/src/build/identity.rs +4 -3
  345. data/rust/parser/src/build/mod.rs +1 -0
  346. data/rust/parser/src/build/named_pattern.rs +26 -0
  347. data/rust/parser/src/build/naming.rs +2 -80
  348. data/rust/parser/src/build/pattern_subset.rs +6 -8
  349. data/rust/parser/src/build/query_derive.rs +3 -1
  350. data/rust/parser/src/build/query_inference.rs +14 -5
  351. data/rust/parser/src/build/read_model.rs +4 -1
  352. data/rust/parser/src/build/references.rs +4 -3
  353. data/rust/parser/src/canonical.rs +34 -15
  354. data/rust/parser/src/emit.rs +187 -105
  355. data/rust/parser/src/expr/ast_json.rs +134 -42
  356. data/rust/parser/src/expr/evaluator.rs +27 -9
  357. data/rust/parser/src/expr/mod.rs +54 -13
  358. data/rust/parser/src/expr/resolver.rs +299 -92
  359. data/rust/parser/src/ir.rs +35 -3
  360. data/rust/parser/src/keywords.rs +7168 -510
  361. data/rust/parser/src/lex.rs +11 -25
  362. data/rust/parser/src/lib.rs +1 -0
  363. data/rust/parser/src/main.rs +65 -6
  364. data/rust/parser/src/parse/adapter.rs +157 -0
  365. data/rust/parser/src/parse/aggregate.rs +19 -10
  366. data/rust/parser/src/parse/chapter.rs +16 -17
  367. data/rust/parser/src/parse/command.rs +35 -16
  368. data/rust/parser/src/parse/domain_port.rs +4 -7
  369. data/rust/parser/src/parse/entity.rs +21 -11
  370. data/rust/parser/src/parse/hecksagon.rs +10 -4
  371. data/rust/parser/src/parse/lifecycle.rs +9 -5
  372. data/rust/parser/src/parse/mod.rs +43 -91
  373. data/rust/parser/src/parse/needs.rs +21 -12
  374. data/rust/parser/src/parse/policy.rs +2 -2
  375. data/rust/parser/src/parse/process_manager.rs +9 -11
  376. data/rust/parser/src/parse/query.rs +8 -4
  377. data/rust/parser/src/parse/read_model.rs +57 -16
  378. data/rust/parser/src/parse/value_object.rs +7 -2
  379. data/rust/parser/src/parse/world.rs +322 -0
  380. data/rust/parser/src/ruby_value.rs +7 -6
  381. data/rust/src/exemplar/json.rs +4 -4
  382. data/rust/src/exemplar/mutations.rs +18 -0
  383. data/rust/src/exemplar/read_models.rs +1 -10
  384. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  385. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  386. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  387. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  388. data/rust/src/kernel/cli.rs +229 -112
  389. data/rust/src/kernel/dispatch.rs +323 -115
  390. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  391. data/rust/src/kernel/expr.rs +203 -86
  392. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  393. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  394. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  395. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  396. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  397. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  398. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  399. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  400. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  401. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  402. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  403. data/rust/src/kernel/expression_operators/text.rs +120 -27
  404. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  405. data/rust/src/kernel/json.rs +230 -148
  406. data/rust/src/kernel/mod.rs +46 -33
  407. data/rust/src/kernel/named_query.rs +63 -15
  408. data/rust/src/kernel/needs.rs +112 -25
  409. data/rust/src/kernel/orchestrate.rs +980 -581
  410. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  411. data/rust/src/kernel/pattern.rs +107 -34
  412. data/rust/src/kernel/query_comparators.rs +118 -41
  413. data/rust/src/kernel/query_ordering.rs +13 -5
  414. data/rust/src/kernel/read_model.rs +479 -181
  415. data/rust/src/kernel/reference_lookup.rs +122 -26
  416. data/rust/src/kernel/repository.rs +293 -66
  417. data/rust/src/kernel/routing.rs +58 -42
  418. data/rust/src/kernel/ruby_float.rs +139 -0
  419. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  420. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  421. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  422. data/rust/src/kernel/vocab/mod.rs +6 -6
  423. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  424. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  425. data/rust/src/lib.rs +50 -0
  426. data/rust/src/main.rs +4 -1
  427. data/rust/web/Cargo.toml +36 -0
  428. data/rust/web/clippy.toml +4 -0
  429. metadata +85 -15
  430. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  431. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  432. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  433. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  434. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  435. data/lib/hecks/ports/authentication.port +0 -6
  436. data/lib/hecks/ports/authentication.rb +0 -67
  437. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  438. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  439. data/prebuilt/verdicts-48318ca172edd21443b1ad02801c697fff100b746e828f09db987647641a5df2.json +0 -1
  440. data/rust/host/src/approval.rs +0 -652
@@ -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
  " }",
@@ -1,5 +1,7 @@
1
+ require_relative "../naming"
1
2
  require_relative "../projector"
2
3
  require_relative "../runtime/refusal_wording"
4
+ require_relative "../vocabulary"
3
5
  require_relative "vocabulary"
4
6
  require_relative "rust_vocabulary/enum_body"
5
7
  require_relative "rust_vocabulary/arguments"
@@ -33,12 +35,20 @@ module Hecks
33
35
  kind: :order }
34
36
  }.freeze
35
37
 
36
- RUST_KEYWORDS = %w[as async await break const continue crate dyn else enum extern false fn for if impl in let loop
37
- match mod move mut pub ref return self static struct super trait true type unsafe use where
38
- while abstract become box do final macro override priv typeof unsized virtual yield try].freeze
38
+ # Keywords Rust refuses as a raw identifier, so they cannot take the `r#` escape.
39
+ NOT_RAW = %w[crate self Self super].freeze
39
40
 
40
41
  module_function
41
42
 
43
+ # @param name [String] a field's name
44
+ # @return [String] the Rust field: raw-escaped when a keyword, with a trailing underscore
45
+ # when the keyword cannot be raw
46
+ def field_name(name)
47
+ return "#{name}_" if NOT_RAW.include?(name)
48
+
49
+ Hecks::Vocabulary.fetch("RustReservedWord").include?(name) ? "r##{name}" : name
50
+ end
51
+
42
52
  def call(bluebook:, options: {}) = render(bluebook)
43
53
 
44
54
  def render(bluebook)
@@ -121,14 +131,12 @@ module Hecks
121
131
  def accessor_name(field) = field == "refusal" ? "refusal_class" : field
122
132
 
123
133
  def variant_name(row, fields)
124
- name = fields.map { |field| pascal(row.fetch(field)) }.join
134
+ name = fields.map { |field| Naming.pascal(row.fetch(field)) }.join
125
135
  raise ArgumentError, "#{name.inspect} is not a Rust variant name" unless name.match?(/\A[A-Z][A-Za-z0-9]*\z/)
126
136
 
127
137
  name
128
138
  end
129
139
 
130
- def pascal(text) = text.to_s.split("_").map { |part| part[0].to_s.upcase + part[1..].to_s }.join
131
-
132
140
  # Only `\`, `"` and newlines need escaping in the text a vocabulary row holds.
133
141
  def rust_string(text)
134
142
  escaped = text.to_s.gsub("\\") { "\\\\" }.gsub('"') { "\\\"" }.gsub("\n") { "\\n" }
@@ -97,12 +97,6 @@ module Hecks
97
97
  # @return [Boolean] whether `value` reaches `ratio` on every background
98
98
  def reads?(value, against, ratio) = against.all? { |back| contrast(value, back) >= ratio }
99
99
 
100
- # @param background [String] a fill
101
- # @param light [String] a light foreground to try
102
- # @param dark [String] a dark foreground to try
103
- # @return [String] whichever reads better on `background`
104
- def foreground(background, light, dark) = contrast(light, background) >= contrast(dark, background) ? light : dark
105
-
106
100
  # @return [Float] the hue, in degrees, of a colour given by its channels
107
101
  def angle(red, green, blue, high, spread)
108
102
  degrees = if high == red then ((green - blue) / spread) % 6
@@ -0,0 +1,99 @@
1
+ require_relative "../plain_english"
2
+
3
+ module Hecks
4
+ module Projections
5
+ module Statements
6
+ # The statements a holder's lifecycle and commands carry: where it starts, what moves it
7
+ # between states, who does each command, and every rule a command checks before and after.
8
+ # Rule text is the author's own; only the command's name is turned into words around it.
9
+ #
10
+ # Actions.rules(order) # => ["Adding a topping requires: at most 10 toppings.", ...]
11
+ module Actions
12
+ module_function
13
+
14
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity] the construct with a lifecycle
15
+ # @return [Array<String>] where it starts, then one sentence per move
16
+ def lifecycle(holder)
17
+ lifecycle = holder.respond_to?(:lifecycle) && holder.lifecycle
18
+ return [] unless lifecycle
19
+
20
+ start = "#{PlainEnglish.upper(PlainEnglish.a(PlainEnglish.noun(holder.hecks_name)))} " \
21
+ "starts out #{PlainEnglish.state(lifecycle.default)}."
22
+ [start, *moves(lifecycle).map { |command, move| move_sentence(holder, command, move) }]
23
+ end
24
+
25
+ # Each command's moves, as `[command, { from: [...], to: state }]`, merged across the
26
+ # transitions a command declares from more than one state.
27
+ def moves(lifecycle)
28
+ merged = lifecycle.transitions.each_with_object({}) do |(command, transition), found|
29
+ key = [command.to_s, transition.target]
30
+ found[key] ||= { from: [], to: transition.target }
31
+ found[key][:from] |= Array(transition.from)
32
+ end
33
+ merged.map { |(command, _target), move| [command, move] }
34
+ end
35
+
36
+ # "Purchasing an order moves it from available to sold."
37
+ def move_sentence(holder, command, move)
38
+ doing = PlainEnglish.doing(command, holder.hecks_name)
39
+ noun = PlainEnglish.noun(holder.hecks_name)
40
+ object = doing.match?(/\b#{Regexp.escape(noun)}\b/) ? "it" : "the #{noun}"
41
+ "#{PlainEnglish.upper(doing)} #{move_phrase(move, object)}."
42
+ end
43
+
44
+ # "moves it from available to sold", "keeps it failed", or "moves it to closed".
45
+ #
46
+ # @param move [Hash] `{ from: [...], to: state }`, as `moves` answers it
47
+ # @param object [String] what is moved, such as "it" or "the order"
48
+ # @return [String] the verb phrase
49
+ def move_phrase(move, object)
50
+ to = PlainEnglish.state(move[:to])
51
+ return "keeps #{object} #{to}" if move[:from] == [move[:to]]
52
+ return "moves #{object} to #{to}" if move[:from].empty?
53
+
54
+ "moves #{object} from #{PlainEnglish.states(move[:from])} to #{to}"
55
+ end
56
+
57
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity] the construct the commands are on
58
+ # @return [Array<String>] each command's role, its rules, and what it guarantees after
59
+ def rules(holder)
60
+ holder.commands.flat_map { |command| command_rules(holder, command) }
61
+ end
62
+
63
+ def command_rules(holder, command)
64
+ doing = PlainEnglish.doing(command.hecks_name, holder.hecks_name)
65
+ role = command.role && "#{PlainEnglish.upper(doing)} is done by the #{PlainEnglish.noun(command.role)}."
66
+ [role, *checks(holder, command).map { |rule| "#{PlainEnglish.upper(doing)} requires: #{rule}." },
67
+ *command.ensures.map { |rule| "After #{doing}: #{text(rule)}." }].compact
68
+ end
69
+
70
+ # What a command checks first: the state it starts from, then each `given` in the
71
+ # author's words. The state is left out when a move sentence already says it.
72
+ #
73
+ # @param said_by_move [Boolean] whether a move sentence states the starting state
74
+ # @return [Array<String>] the rules, without a closing period
75
+ def checks(holder, command, said_by_move: true)
76
+ [state_check(holder, command, said_by_move), *command.givens.map { |rule| text(rule) }].compact
77
+ end
78
+
79
+ # "the order is available", for a command declared `from:` a state.
80
+ def state_check(holder, command, said_by_move)
81
+ return nil unless command.from
82
+ return nil if said_by_move && moved_by?(holder, command)
83
+
84
+ "the #{PlainEnglish.noun(holder.hecks_name)} is #{PlainEnglish.states(Array(command.from))}"
85
+ end
86
+
87
+ def moved_by?(holder, command)
88
+ lifecycle = holder.respond_to?(:lifecycle) && holder.lifecycle
89
+ return false unless lifecycle
90
+
91
+ lifecycle.transitions.any? { |name, _transition| name.to_s == command.hecks_name }
92
+ end
93
+
94
+ # A rule's description as written, without a trailing period.
95
+ def text(rule) = rule.description.to_s.strip.delete_suffix(".")
96
+ end
97
+ end
98
+ end
99
+ end