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
@@ -28,11 +28,10 @@ module Hecks
28
28
  # @param page [Hash] optionally `status:`, the submitted `values:`, the `error:` to show and
29
29
  # the `prefill:` values
30
30
  def command_form(domain, aggregate, command, action, **page)
31
- html("#{domain}::#{aggregate.hecks_name}.#{command.hecks_name}",
31
+ html(Humanize.phrase(command.hecks_name),
32
32
  CommandFormRenderer.render(registry: @registry, domain: domain, aggregate: aggregate, command: command,
33
33
  action: action, **page.slice(:values, :error, :prefill)),
34
- breadcrumbs: [[domain, "/"], [aggregate.hecks_name, "/#{domain}/#{aggregate.hecks_name}.html"],
35
- [command.hecks_name, nil]],
34
+ breadcrumbs: verb_breadcrumbs(domain, aggregate, command),
36
35
  status: page.fetch(:status, 200))
37
36
  end
38
37
 
@@ -25,11 +25,10 @@ module Hecks
25
25
  def query_html(domain, aggregate, query, asked)
26
26
  action = "/#{domain}/#{aggregate.hecks_name}/#{query.hecks_name}.html"
27
27
  results, error = asked.empty? ? [nil, nil] : run_query(domain, aggregate, query, asked)
28
- html("#{domain}::#{aggregate.hecks_name}.#{query.hecks_name}",
28
+ html(Humanize.phrase(query.hecks_name),
29
29
  QueryFormRenderer.render(registry: @registry, domain: domain, aggregate: aggregate, query: query,
30
30
  action: action, params: asked, results: results, error: error),
31
- breadcrumbs: [[domain, "/"], [aggregate.hecks_name, "/#{domain}/#{aggregate.hecks_name}.html"],
32
- [query.hecks_name, nil]],
31
+ breadcrumbs: verb_breadcrumbs(domain, aggregate, query),
33
32
  status: error ? 422 : 200)
34
33
  end
35
34
 
@@ -12,9 +12,9 @@ module Hecks
12
12
  return respond(405, "text/plain", "GET only") unless request.get?
13
13
  return aggregate_json(chapter, aggregate) unless format == "html"
14
14
 
15
- html("#{chapter.name}::#{aggregate.hecks_name}",
15
+ html(Naming.plural(Humanize.phrase(aggregate.hecks_name)),
16
16
  RecordRenderer.index(registry: @registry, domain: chapter.name, aggregate: aggregate),
17
- breadcrumbs: [[chapter.name, "/"], [aggregate.hecks_name, nil]])
17
+ breadcrumbs: [[chapter.name, "/"], [Humanize.phrase(aggregate.hecks_name), nil]])
18
18
  end
19
19
 
20
20
  def aggregate_json(chapter, aggregate)
@@ -52,10 +52,22 @@ module Hecks
52
52
  # `id:` last, as in `aggregate_route`.
53
53
  return json(200, instance.state.merge(id: instance.id)) if format != "html"
54
54
 
55
- html("#{domain}::#{aggregate.hecks_name} #{id}",
55
+ html("#{Humanize.phrase(aggregate.hecks_name)} #{id}",
56
56
  RecordRenderer.show(registry: @registry, domain: domain, aggregate: aggregate, id: id),
57
- breadcrumbs: [[domain, "/"], [aggregate.hecks_name, "/#{domain}/#{aggregate.hecks_name}.html"],
58
- [id, nil]])
57
+ breadcrumbs: aggregate_crumbs(domain, aggregate) + [[id, nil]])
58
+ end
59
+
60
+ # @return [Array<Array(String, String)>] the domain and aggregate crumbs every page
61
+ # under an aggregate starts with, the aggregate named in words
62
+ def aggregate_crumbs(domain, aggregate)
63
+ [[domain, "/"], [Humanize.phrase(aggregate.hecks_name), "/#{domain}/#{aggregate.hecks_name}.html"]]
64
+ end
65
+
66
+ # @param verb [Bluebook::Command, Bluebook::Query] the command or query the page is for
67
+ # @return [Array<Array(String, String), Array(String, nil)>] the aggregate crumbs, then
68
+ # the verb named in words as the current page
69
+ def verb_breadcrumbs(domain, aggregate, verb)
70
+ aggregate_crumbs(domain, aggregate) + [[Humanize.phrase(verb.hecks_name), nil]]
59
71
  end
60
72
 
61
73
  def find_aggregate(chapter, name)
@@ -43,7 +43,7 @@ module Hecks
43
43
  fields_html = fields_markup(registry, domain, all_fields, form)
44
44
  action = form.fetch(:action)
45
45
  cancel = "/#{domain}/#{aggregate.hecks_name}"
46
- [header(domain, aggregate, command), error_banner(form[:error]),
46
+ [header(aggregate, command), error_banner(form[:error]),
47
47
  form_markup(action, fields_html, command, cancel).chomp,
48
48
  inspect_panel(domain, aggregate, command, action, all_fields)].join("\n").concat("\n")
49
49
  end
@@ -64,7 +64,7 @@ module Hecks
64
64
  <form method="post" action="#{Escape.attr(action)}" novalidate>
65
65
  #{fields_html}
66
66
  <div class="actions">
67
- <button type="submit">#{Escape.html(command.hecks_name)}</button>
67
+ <button type="submit">#{Escape.html(Humanize.phrase(command.hecks_name))}</button>
68
68
  <a class="button secondary" href="#{Escape.attr(cancel)}">Cancel</a>
69
69
  </div>
70
70
  </form>
@@ -72,33 +72,58 @@ module Hecks
72
72
  end
73
73
 
74
74
  # Builds the `to` field a non-creating command uses to name the record it acts on,
75
- # labelled with the aggregate's identity paths.
75
+ # labelled with the aggregate's name and helped by what identifies one ("the order's
76
+ # name").
76
77
  #
77
78
  # @param aggregate [Bluebook::Aggregate] the aggregate whose records the picker offers
78
79
  # @return [Forms::Field] a `:reference` field at path `"to"` targeting `aggregate`
79
80
  def self.identity_field(aggregate)
80
- Field.new(path: "to", label: "#{aggregate.hecks_name} (#{aggregate.identity_paths.join(", ")})",
81
+ noun = Humanize.noun(aggregate.hecks_name)
82
+ Field.new(path: "to", label: Humanize.phrase(aggregate.hecks_name),
81
83
  kind: :reference, html_type: "text", target_aggregate: aggregate,
82
- help: "The record this command acts on.")
84
+ help: "Which #{noun} this is for, by the #{noun}'s #{identity_words(aggregate)}.")
83
85
  end
84
86
 
85
- # Renders the form's heading: the command's qualified name, its role and creates
86
- # badges, its goal, and its preconditions callout.
87
+ # Names what identifies a record in words: `["name.value"]` -> "name", a composite
88
+ # `["bank", "number"]` -> "bank and number". A dotted path names its first segment,
89
+ # since the hops after it reach inside a value object.
90
+ #
91
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose identity is named
92
+ # @return [String] the identity's field names as lowercase words; "id" when it has none
93
+ def self.identity_words(aggregate)
94
+ names = aggregate.identity_paths.map { |path| Humanize.noun(path.to_s.split(".").first) }.uniq
95
+ names.empty? ? "id" : Naming.to_sentence_list(names)
96
+ end
97
+
98
+ # Renders the form's heading: the command's name in words, its role and creates
99
+ # badges, its goal, and its preconditions callout. The code name stays in the inspect
100
+ # panel.
87
101
  #
88
- # @param domain [String] name of the domain (chapter) the aggregate belongs to
89
102
  # @param aggregate [Bluebook::Aggregate] the aggregate that declares the command
90
103
  # @param command [Bluebook::Command] the command being described
91
104
  # @return [String] HTML for the heading block
92
- def self.header(domain, aggregate, command)
105
+ def self.header(aggregate, command)
93
106
  <<~HTML
94
- <h1>#{Escape.html("#{domain}::#{aggregate.hecks_name}.#{command.hecks_name}")}</h1>
95
- #{%(<span class="badge role" title="Declared on the command; this prototype dispatches with no caller bound, so the check does not run.">role: #{Escape.html(command.role)} (not enforced here)</span>) if command.role}
96
- #{%(<span class="badge">creates a new #{Escape.html(aggregate.hecks_name)}</span>) if command.creates?}
107
+ <h1>#{Escape.html(Humanize.phrase(command.hecks_name))}</h1>
108
+ #{role_badge(command)}
109
+ #{%(<span class="badge">Creates a new #{Escape.html(Humanize.noun(aggregate.hecks_name))}</span>) if command.creates?}
97
110
  #{%(<p class="goal">#{Escape.html(command.goal)}</p>) if command.goal}
98
111
  #{givens_callout(command)}
99
112
  HTML
100
113
  end
101
114
 
115
+ # Names who the command is for. The preview dispatches with no caller bound, so the role
116
+ # is shown but not checked, and the badge says so.
117
+ #
118
+ # @param command [Bluebook::Command] the command whose declared role is shown
119
+ # @return [String] HTML for the badge, or `""` when the command declares no role
120
+ def self.role_badge(command)
121
+ return "" unless command.role
122
+
123
+ title = "The bluebook declares who does this. This preview signs nobody in, so it does not check."
124
+ %(<span class="badge role" title="#{title}">Done by: #{Escape.html(command.role)} (not checked in this preview)</span>)
125
+ end
126
+
102
127
  # Renders the command's `given` preconditions as a callout, so a caller sees what
103
128
  # would refuse the submission before making it.
104
129
  #
@@ -108,7 +133,8 @@ module Hecks
108
133
  return "" if command.givens.empty?
109
134
 
110
135
  items = command.givens.map { |given| "<li>#{Escape.html(given.description)}</li>" }
111
- %(<div class="callout"><strong>Preconditions</strong> — refused if any fail:<ul>#{items.join}</ul></div>)
136
+ %(<div class="callout"><strong>Checked first.</strong> The action is refused if any of these fail:) \
137
+ "<ul>#{items.join}</ul></div>"
112
138
  end
113
139
 
114
140
  # Renders a refused submission as an alert banner carrying the refusal's class name
@@ -121,7 +147,7 @@ module Hecks
121
147
 
122
148
  <<~HTML
123
149
  <div class="error-banner" role="alert">
124
- <p><strong>#{Escape.html(error.class.name.split("::").last)}</strong> — #{Escape.html(error.message)}</p>
150
+ <p><strong>Refused (#{Escape.html(error.class.name.split("::").last)}):</strong> #{Escape.html(error.message)}</p>
125
151
  </div>
126
152
  HTML
127
153
  end
@@ -174,9 +200,9 @@ module Hecks
174
200
  def self.inspect_head(verb, command, curl)
175
201
  <<~HTML
176
202
  <details class="inspect">
177
- <summary>Inspect — #{Escape.html(verb)}</summary>
203
+ <summary>Inspect: #{Escape.html(verb)}</summary>
178
204
  <p>Emits: #{emitted_events(command)}</p>
179
- <p>Equivalent request (as <code>curl</code>) — every field is <code>#{Escape.html("name.path")}</code>-encoded, form or JSON alike:</p>
205
+ <p>The same request as <code>curl</code>. Every field is sent by its dotted path (<code>#{Escape.html("name.path")}</code>), as a form or as JSON:</p>
180
206
  <div class="link-row"><code id="curl-snippet">#{Escape.html(curl)}</code><button type="button" class="copy" data-copy="#curl-snippet">copy</button></div>
181
207
  HTML
182
208
  end
@@ -65,7 +65,7 @@ module Hecks
65
65
  %(<option value="#{Escape.attr(option_value)}"#{" selected" if option_value.to_s == selected.to_s}>) \
66
66
  "#{Escape.html(option_label)}</option>"
67
67
  end
68
- blank = field.optional? ? %(<option value="">—</option>) : ""
68
+ blank = field.optional? ? %(<option value="">(none)</option>) : ""
69
69
  select_tag(field, "#{blank}#{options.join}")
70
70
  end
71
71
 
@@ -80,7 +80,7 @@ module Hecks
80
80
  end
81
81
 
82
82
  def self.reference_blank(field, value)
83
- return %(<option value="">—</option>) if field.optional?
83
+ return %(<option value="">(none)</option>) if field.optional?
84
84
 
85
85
  %(<option value="" disabled#{" selected" unless value}>choose one…</option>)
86
86
  end
@@ -51,7 +51,7 @@ module Hecks
51
51
 
52
52
  def self.list_tail(field, item)
53
53
  <<~HTML
54
- <span class="help" id="#{dom_id(field.path)}-help">One #{Escape.html(item.label.downcase)} per line#{" — each line is one JSON object" unless item.leaf?}.</span>
54
+ <span class="help" id="#{dom_id(field.path)}-help">One #{Escape.html(item.label.downcase)} per line#{", each line one JSON object" unless item.leaf?}.</span>
55
55
  </div>
56
56
  HTML
57
57
  end
@@ -3,6 +3,7 @@ require_relative "../bluebook/attribute"
3
3
  require_relative "../naming"
4
4
  require_relative "value_object_shape"
5
5
  require_relative "text_shape"
6
+ require_relative "rule_words"
6
7
 
7
8
  module Hecks
8
9
  # The forms surface: the `expose` DSL, the IR->HTML renderers and the Rack app (see forms.rb).
@@ -32,9 +33,20 @@ module Hecks
32
33
  Naming.words(segment)
33
34
  end
34
35
 
35
- # "amount.cents" -> "Amount → Cents", for a fieldset legend spanning several hops.
36
- def self.breadcrumb(path)
37
- path.to_s.split(".").map { |part| label(part) }.join(" → ")
36
+ # A code name as a phrase: "AddTopping" -> "Add topping", "Order" -> "Order".
37
+ #
38
+ # @param name [String, Symbol, #to_s] a command, query or aggregate name
39
+ # @return [String] the sentence-cased words
40
+ def self.phrase(name) = Naming.words(name)
41
+
42
+ # A code name as words inside a sentence: "BankAccount" -> "bank account", "ATMCard" ->
43
+ # "ATM card".
44
+ #
45
+ # @param name [String, Symbol, #to_s] an aggregate or field name
46
+ # @return [String] the words, the first lowercased unless it is an acronym
47
+ def self.noun(name)
48
+ words = Naming.words(name)
49
+ words.match?(/\A[A-Z]{2,}\b/) ? words : words.sub(/\A./, &:downcase)
38
50
  end
39
51
  end
40
52
 
@@ -79,12 +91,8 @@ module Hecks
79
91
 
80
92
  def self.reference_field(attribute, common)
81
93
  target = attribute.type.resolve
82
- Field.new(**common, kind: :reference, html_type: "text", target_aggregate: target,
83
- help: if target
84
- "References an existing #{target.hecks_name} by id."
85
- else
86
- "References an aggregate in another domain — enter its id."
87
- end)
94
+ help = target ? "An existing #{Humanize.noun(target.hecks_name)}, by its id." : "A record in another domain, by its id."
95
+ Field.new(**common, kind: :reference, html_type: "text", target_aggregate: target, help: help)
88
96
  end
89
97
 
90
98
  # Mirrors Runtime::Value::Admission#admitted_members (same split, chapter walk and
@@ -119,16 +127,26 @@ module Hecks
119
127
  return closed_set_field(shape, common) if shape.closed_set?
120
128
  return money_field(shape, common) if ValueObjectShape.money?(shape)
121
129
 
122
- # A single-attribute value object names a scalar, not a group: unwrap it so the form
123
- # asks for one thing and the inner attribute's own pattern drives the input type.
124
- if (inner = ValueObjectShape.sole_attribute(shape))
125
- return resolve(inner, aggregate: aggregate, path: "#{common[:path]}.#{inner.name}")
126
- .tap { |field| field.optional = common[:optional] || field.optional }
127
- end
130
+ inner = ValueObjectShape.sole_attribute(shape)
131
+ return sole_attribute_field(shape, inner, aggregate, common) if inner
128
132
 
129
133
  group_field(shape, aggregate, common)
130
134
  end
131
135
 
136
+ # A single-attribute value object names a scalar, not a group: unwrap it so the form
137
+ # asks for one thing and the inner attribute's own pattern drives the input type. The
138
+ # label stays the command's own name for it (`topping`), never the inner `value`, and
139
+ # the help says what the value object's rules accept.
140
+ #
141
+ # @return [Forms::Field] the inner attribute's field at the dotted path
142
+ def self.sole_attribute_field(shape, inner, aggregate, common)
143
+ field = resolve(inner, aggregate: aggregate, path: "#{common[:path]}.#{inner.name}")
144
+ field.label = common[:label]
145
+ field.optional = common[:optional] || field.optional
146
+ field.help = RuleWords.describe(inner, shape.invariants) if field.leaf? && field.kind != :reference
147
+ field
148
+ end
149
+
132
150
  def self.cross_aggregate_value_object(aggregate, type_name)
133
151
  aggregate.hecks_owner&.aggregates&.each do |sibling|
134
152
  found = sibling.value_object(type_name)
@@ -145,10 +163,10 @@ module Hecks
145
163
  def self.money_field(shape, common)
146
164
  cents = Field.new(path: "#{common[:path]}.cents", label: "Amount (cents)", kind: :number,
147
165
  html_type: "number", step: "1", optional: common[:optional],
148
- default: shape.attribute(:cents)&.default, help: "Whole cents — 1050 is $10.50.")
166
+ default: shape.attribute(:cents)&.default, help: "In cents: 1050 is $10.50.")
149
167
  currency = Field.new(path: "#{common[:path]}.currency", label: "Currency", kind: :text, html_type: "text",
150
168
  optional: true, default: shape.attribute(:currency)&.default || "USD",
151
- help: "Three-letter code.")
169
+ help: "Three-letter code, such as USD.")
152
170
  Field.new(path: common[:path], label: common[:label], kind: :money, optional: common[:optional],
153
171
  children: [cents, currency])
154
172
  end
@@ -173,6 +191,7 @@ module Hecks
173
191
  end
174
192
 
175
193
  def self.primitive_field(attribute, common)
194
+ common = common.merge(help: RuleWords.describe(attribute))
176
195
  case attribute.type.to_s
177
196
  when "Integer" then Field.new(**common, kind: :number, html_type: "number", step: "1")
178
197
  when "Float" then Field.new(**common, kind: :number, html_type: "number", step: "any")
@@ -1,4 +1,5 @@
1
1
  require_relative "html"
2
+ require_relative "field_shape"
2
3
 
3
4
  module Hecks
4
5
  module Forms
@@ -40,7 +41,7 @@ module Hecks
40
41
  "#{aggregate.queries.size} quer#{aggregate.queries.size == 1 ? "y" : "ies"}"
41
42
  <<~HTML
42
43
  <li><a href="/#{Escape.attr(name)}/#{Escape.attr(aggregate.hecks_name)}.html">
43
- <span>#{Escape.html(aggregate.hecks_name)}</span><span class="kind">#{Escape.html(counts)}</span>
44
+ <span>#{Escape.html(Humanize.phrase(aggregate.hecks_name))}</span><span class="kind">#{Escape.html(counts)}</span>
44
45
  </a></li>
45
46
  HTML
46
47
  end
@@ -35,7 +35,7 @@ module Hecks
35
35
  #{body}
36
36
  </main>
37
37
  <footer class="site-footer">
38
- <p>Generated straight from the loaded bluebook IR — nothing here is hand-authored per page. See <code>docs/command-form-and-query-form-bluebook.md</code>.</p>
38
+ <p>Every page here is generated from the domain's description (its bluebook). None is written by hand.</p>
39
39
  </footer>
40
40
  <script>#{SCRIPT}</script>
41
41
  </body>
@@ -203,19 +203,6 @@ module Hecks
203
203
  end
204
204
  into
205
205
  end
206
-
207
- # Renders held values as a URL query string, percent-encoding each flattened path and
208
- # value.
209
- #
210
- # @param fields [Array<Forms::Field>] the field tree naming which paths to read
211
- # @param values [Hash{String, Symbol => Object}] held values, flat or nested; see
212
- # `FieldRenderer.dig`
213
- # @return [String] `key=value` pairs joined with `&`, without a leading `?`; `""` when
214
- # `flatten` yields no pairs
215
- def self.to_query_string(fields, values)
216
- pairs = flatten(fields, values)
217
- pairs.map { |key, value| "#{URI.encode_www_form_component(key)}=#{URI.encode_www_form_component(value)}" }.join("&")
218
- end
219
206
  end
220
207
  end
221
208
  end
@@ -22,7 +22,7 @@ module Hecks
22
22
  reference_options = ReferenceOptions.collect(registry, domain, fields)
23
23
 
24
24
  <<~HTML
25
- #{header(domain, aggregate, query)}
25
+ #{header(query)}
26
26
  #{canonical_link(action, fields)}
27
27
  #{quick_links(action, fields)}
28
28
  #{filter_form(action, fields, view.fetch(:params, {}), reference_options)}
@@ -32,9 +32,9 @@ module Hecks
32
32
  HTML
33
33
  end
34
34
 
35
- def self.header(domain, aggregate, query)
35
+ def self.header(query)
36
36
  <<~HTML
37
- <h1>#{Escape.html("#{domain}::#{aggregate.hecks_name}.#{query.hecks_name}")}</h1>
37
+ <h1>#{Escape.html(Humanize.phrase(query.hecks_name))}</h1>
38
38
  #{%(<p class="goal">#{Escape.html(query.description)}</p>) if query.description}
39
39
  #{badges(query)}
40
40
  HTML
@@ -50,7 +50,7 @@ module Hecks
50
50
  paths = Params.paths(fields)
51
51
  template = paths.empty? ? action : "#{action}?#{paths.map { |path| "#{path}={#{path}}" }.join("&")}"
52
52
  <<~HTML
53
- <p class="help">Every query is a plain GET — this exact URL is bookmarkable, linkable from a dashboard, curlable, whatever a feature needs:</p>
53
+ <p class="help">This query is a plain link. Bookmark it, share it, or put it on a dashboard:</p>
54
54
  <div class="link-row"><code id="canonical-link">#{Escape.html(template)}</code><button type="button" class="copy" data-copy="#canonical-link">copy</button></div>
55
55
  HTML
56
56
  end
@@ -81,7 +81,8 @@ module Hecks
81
81
  def self.error_banner(error)
82
82
  return "" unless error
83
83
 
84
- %(<div class="error-banner" role="alert"><p><strong>#{Escape.html(error.class.name.split("::").last)}</strong> — ) \
84
+ kind = Escape.html(error.class.name.split("::").last)
85
+ %(<div class="error-banner" role="alert"><p><strong>Refused (#{kind}):</strong> ) \
85
86
  "#{Escape.html(error.message)}</p></div>"
86
87
  end
87
88
 
@@ -96,7 +97,7 @@ module Hecks
96
97
  paths = Params.paths(fields)
97
98
  <<~HTML
98
99
  <details class="inspect">
99
- <summary>Inspect — #{Escape.html(verb)}</summary>
100
+ <summary>Inspect: #{Escape.html(verb)}</summary>
100
101
  <p>Parameters: #{paths.empty? ? "<em>none</em>" : paths.map { |p| "<code>#{Escape.html(p)}</code>" }.join(", ")}</p>
101
102
  <pre>#{Escape.html(JSON.pretty_generate(query.to_h))}</pre>
102
103
  </details>
@@ -11,7 +11,7 @@ module Hecks
11
11
  def self.index(registry:, domain:, aggregate:)
12
12
  instances = registry.repository(domain, aggregate).all
13
13
  <<~HTML
14
- <h1>#{Escape.html("#{domain}::#{aggregate.hecks_name}")}</h1>
14
+ <h1>#{Escape.html(Naming.plural(Humanize.phrase(aggregate.hecks_name)))}</h1>
15
15
  #{%(<p class="goal">#{Escape.html(aggregate.description)}</p>) if aggregate.description}
16
16
  #{creating_links(domain, aggregate)}
17
17
  <h2>Records (#{instances.size})</h2>
@@ -26,7 +26,7 @@ module Hecks
26
26
 
27
27
  links = creators.map do |cmd|
28
28
  %(<a class="button" href="/#{domain}/#{aggregate.hecks_name}/#{cmd.hecks_name}.html">) \
29
- "+ #{Escape.html(cmd.hecks_name)}</a>"
29
+ "+ #{Escape.html(Humanize.phrase(cmd.hecks_name))}</a>"
30
30
  end
31
31
  %(<div class="actions">#{links.join}</div>)
32
32
  end
@@ -40,7 +40,7 @@ module Hecks
40
40
  state = instance.state
41
41
  current = aggregate.lifecycle && state[aggregate.lifecycle.field]
42
42
  <<~HTML
43
- <h1>#{Escape.html("#{domain}::#{aggregate.hecks_name}")} <span class="mono">#{Escape.html(id)}</span></h1>
43
+ <h1>#{Escape.html(Humanize.phrase(aggregate.hecks_name))} <span class="mono">#{Escape.html(id)}</span></h1>
44
44
  #{lifecycle_badge(aggregate, current)}
45
45
  #{state_table(state)}
46
46
  <h2>Commands</h2>
@@ -76,7 +76,8 @@ module Hecks
76
76
  # Omits lifecycle transitions that dispatch would refuse from the current state.
77
77
  def self.command_links(domain, aggregate, id, current_state)
78
78
  commands = aggregate.commands.reject(&:creates?).select { |cmd| applies?(aggregate, cmd, current_state) }
79
- return "<p><em>No commands act on an existing #{Escape.html(aggregate.hecks_name)}.</em></p>" if commands.empty?
79
+ noun = Escape.html(Humanize.noun(aggregate.hecks_name))
80
+ return "<p><em>No commands act on an existing #{noun}.</em></p>" if commands.empty?
80
81
 
81
82
  items = commands.map { |cmd| command_item(domain, aggregate, cmd, id) }
82
83
  %(<ul class="verb-list">#{items.join}</ul>)
@@ -86,7 +87,7 @@ module Hecks
86
87
  def self.command_item(domain, aggregate, cmd, id)
87
88
  # The id is free-form, so it is percent-encoded as a query value.
88
89
  href = "/#{domain}/#{aggregate.hecks_name}/#{cmd.hecks_name}.html?to=#{Escape.url(id)}"
89
- %(<li><a href="#{Escape.attr(href)}"><span>#{Escape.html(cmd.hecks_name)}</span>) \
90
+ %(<li><a href="#{Escape.attr(href)}"><span>#{Escape.html(Humanize.phrase(cmd.hecks_name))}</span>) \
90
91
  "<span class=\"kind\">#{Escape.html(cmd.goal.to_s)}</span></a></li>"
91
92
  end
92
93
 
@@ -110,15 +111,18 @@ module Hecks
110
111
 
111
112
  cmd_items = commands.map { |c| verb_item(domain, aggregate, c, "command") }
112
113
  query_items = aggregate.queries.map { |q| verb_item(domain, aggregate, q, "query") }
113
- %(<h2>Every command &amp; query on #{Escape.html(aggregate.hecks_name)}</h2>) \
114
+ %(<h2>Commands and queries for #{plural_noun(aggregate)}</h2>) \
114
115
  "<ul class=\"verb-list\">#{(cmd_items + query_items).join}</ul>"
115
116
  end
116
117
 
117
118
  # @param kind [String] the word shown beside the verb, `"command"` or `"query"`
118
119
  # @return [String] the list item linking to the verb's own page
120
+ # @return [String] the aggregate's name as a lowercase plural, HTML-escaped: "orders"
121
+ def self.plural_noun(aggregate) = Escape.html(Naming.plural(Humanize.noun(aggregate.hecks_name)))
122
+
119
123
  def self.verb_item(domain, aggregate, verb, kind)
120
124
  %(<li><a href="/#{domain}/#{aggregate.hecks_name}/#{verb.hecks_name}.html">) \
121
- "<span>#{Escape.html(verb.hecks_name)}</span><span class=\"kind\">#{kind}</span></a></li>"
125
+ "<span>#{Escape.html(Humanize.phrase(verb.hecks_name))}</span><span class=\"kind\">#{kind}</span></a></li>"
122
126
  end
123
127
  end
124
128
  end
@@ -0,0 +1,100 @@
1
+ require_relative "../bluebook/named_pattern"
2
+
3
+ module Hecks
4
+ module Forms
5
+ module FieldShape
6
+ # Says in plain words what a field accepts, read only from what the bluebook declares:
7
+ # the attribute's type, its named `matches:` pattern, and the invariants of the value
8
+ # object it sits in. An invariant this module cannot read is quoted by its own
9
+ # description rather than guessed at.
10
+ module RuleWords
11
+ # Words for each `Bluebook::NamedPattern` name; a raw regex has none and is not shown.
12
+ PATTERN_WORDS = {
13
+ present: "Must not be blank.",
14
+ not_empty: "Must not be empty.",
15
+ no_whitespace: "No spaces.",
16
+ snake_name: "Lowercase letters, digits and underscores, starting with a letter.",
17
+ git_sha: "A commit id: 7 to 40 hexadecimal characters.",
18
+ digits: "Digits only."
19
+ }.freeze
20
+
21
+ # The noun a numeric type reads as, for "A whole number above zero."
22
+ NUMBER_NOUNS = { "Integer" => "A whole number", "Float" => "A number" }.freeze
23
+
24
+ # One comparison of the attribute against a literal number, as its canonical source
25
+ # spells it: `value > 0`, `cents >= 0`.
26
+ COMPARISON = /\A(?<name>\w+) (?<op>>=|<=|>|<) (?<bound>-?\d+(?:\.\d+)?)\z/
27
+
28
+ # How each comparison reads after the noun: "above 5", "10 or less".
29
+ BOUND_WORDS = { ">" => "above %s", ">=" => "%s or more", "<" => "below %s", "<=" => "%s or less" }.freeze
30
+
31
+ module_function
32
+
33
+ # Describes what `attribute` accepts, or nil when nothing declared can be said in words.
34
+ #
35
+ # @param attribute [Bluebook::Attribute] the scalar attribute the field asks for
36
+ # @param invariants [Array<Bluebook::Invariant>] the rules of the value object that
37
+ # wraps `attribute`; empty for a bare attribute
38
+ # @return [String, nil] one or more sentences, such as "A whole number above zero."
39
+ def describe(attribute, invariants = [])
40
+ readable, quoted = invariants.partition { |rule| reading(attribute, rule) }
41
+ sentences = [type_sentence(attribute, readable), pattern_sentence(attribute, readable)]
42
+ sentences += quoted.map { |rule| "Rule: #{rule.description}." }
43
+ text = sentences.compact.uniq.join(" ")
44
+ text.empty? ? nil : text
45
+ end
46
+
47
+ # @return [String, nil] the numeric noun with every readable bound, such as
48
+ # "A whole number above zero." or "A whole number, zero or more."; nil for a
49
+ # non-numeric attribute
50
+ def type_sentence(attribute, readable)
51
+ noun = NUMBER_NOUNS[attribute.type.to_s]
52
+ return nil unless noun
53
+
54
+ bounds = Naming.to_sentence_list(readable.map { |rule| reading(attribute, rule) }.grep_v(:not_blank))
55
+ return "#{noun}." if bounds.empty?
56
+
57
+ "#{noun}#{bounds.start_with?("above", "below") ? " " : ", "}#{bounds}."
58
+ end
59
+
60
+ # @return [String, nil] "Must not be blank." when the named pattern or a readable
61
+ # invariant says so, or the named pattern's own words
62
+ def pattern_sentence(attribute, readable)
63
+ named = Bluebook::NamedPattern::TABLE.key(attribute.pattern.to_s)
64
+ return PATTERN_WORDS.fetch(:present) if readable.any? { |rule| reading(attribute, rule) == :not_blank }
65
+
66
+ named && PATTERN_WORDS[named]
67
+ end
68
+
69
+ # Reads one invariant over the attribute alone.
70
+ #
71
+ # @return [String, Symbol, nil] a bound's words ("above zero"), `:not_blank`, or nil
72
+ # when the rule is not one this module can read
73
+ def reading(attribute, rule)
74
+ name = attribute.name.to_s
75
+ canonical = rule.canonical.to_s.strip
76
+ return :not_blank if blank_check?(name, canonical)
77
+ return "above zero" if canonical == "#{name}.positive?"
78
+
79
+ comparison_words(name, canonical)
80
+ end
81
+
82
+ # @return [Boolean] whether the rule refuses an empty or all-whitespace value
83
+ def blank_check?(name, canonical)
84
+ ["!#{name}.to_s.empty?", "!#{name}.to_s.strip.empty?", "!#{name}.empty?", "!#{name}.strip.empty?"]
85
+ .include?(canonical)
86
+ end
87
+
88
+ # @return [String, nil] the comparison's words, with zero spelled out; nil when the
89
+ # rule is not one comparison of `name` against a number
90
+ def comparison_words(name, canonical)
91
+ match = COMPARISON.match(canonical)
92
+ return nil unless match && match[:name] == name
93
+
94
+ bound = match[:bound] == "0" ? "zero" : match[:bound]
95
+ format(BOUND_WORDS.fetch(match[:op]), bound)
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -1,7 +1,8 @@
1
1
  module Hecks
2
2
  module Forms
3
3
  # Shared value-object shape classification, so callers such as
4
- # field_shape.rb, ui_schema.rb and sql_query_builder.rb agree on it.
4
+ # field_shape.rb and sql_query_builder.rb agree on it. There is no Ruby ui_schema; the Rust
5
+ # host builds the UI schema (rust/host/src/ui_schema.rs) from the same field-shape rules.
5
6
  module ValueObjectShape
6
7
  module_function
7
8
 
@@ -18,14 +18,14 @@ Hecks.bluebook "Change" do
18
18
  identified_by :run
19
19
 
20
20
  value_object "RunKey" do
21
- attribute :value, String, pattern: '[^ \t\n\r]'
21
+ attribute :value, String, matches: :present
22
22
 
23
23
  invariant("a change is named") { !value.to_s.empty? }
24
24
  end
25
25
 
26
26
  # The name the script is registered under with the adapter.
27
27
  value_object "ScriptName" do
28
- attribute :value, String, pattern: '[^ \t\n\r]'
28
+ attribute :value, String, matches: :present
29
29
 
30
30
  invariant("a change names its script") { !value.to_s.empty? }
31
31
  end
@@ -35,9 +35,6 @@ module Hecks
35
35
  array: %w[arr_num arr_str].freeze
36
36
  }.freeze
37
37
 
38
- # Element type per array attribute, so a block parameter gets the right leaf set.
39
- ARRAY_ELEMENT_TYPE = { "arr_num" => :numeric, "arr_str" => :string }.freeze
40
-
41
38
  BLOCK_PARAM = "el".freeze
42
39
 
43
40
  MAX_DEPTH = 3
@@ -19,7 +19,7 @@ module Hecks
19
19
  end
20
20
 
21
21
  def string_body(name)
22
- ["attribute :value, String, pattern: '[^ \\t\\n\\r]'",
22
+ ["attribute :value, String, matches: :present",
23
23
  "invariant(#{"a #{name} is not blank".inspect}) { !value.to_s.empty? }"]
24
24
  end
25
25