hecks 3.14.0 → 5.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 (427) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/adapters/fake_processor.rb +34 -0
  3. data/examples/pizzas/adapters/payment_signature.rb +66 -0
  4. data/examples/pizzas/adapters/payment_webhook.rb +87 -0
  5. data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
  6. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  7. data/examples/pizzas/config.ru +22 -0
  8. data/exe/hecks +1 -1
  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 +9 -16
  12. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  13. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  14. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  15. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  16. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  17. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  18. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  19. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  20. data/lib/hecks/bluebook/translation.rb +18 -10
  21. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  22. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  23. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  24. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  25. data/lib/hecks/bluebook.rb +2 -0
  26. data/lib/hecks/chapters.rb +3 -3
  27. data/lib/hecks/cli/project_cli/launcher_source.rb +1 -1
  28. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  29. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  30. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  31. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  32. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  33. data/lib/hecks/composer/adapters/jev.rb +66 -0
  34. data/lib/hecks/composer/agent.rb +113 -0
  35. data/lib/hecks/composer/assistant.rb +110 -0
  36. data/lib/hecks/composer/behavior_run.rb +53 -0
  37. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  38. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  39. data/lib/hecks/composer/board.rb +65 -0
  40. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  41. data/lib/hecks/composer/board_edit/source.rb +84 -0
  42. data/lib/hecks/composer/board_edit.rb +144 -0
  43. data/lib/hecks/composer/check.rb +89 -0
  44. data/lib/hecks/composer/domains.rb +81 -0
  45. data/lib/hecks/composer/jobs.rb +134 -0
  46. data/lib/hecks/composer/layout.rb +37 -0
  47. data/lib/hecks/composer/live.rb +150 -0
  48. data/lib/hecks/composer/outline.rb +48 -0
  49. data/lib/hecks/composer/page.html +989 -0
  50. data/lib/hecks/composer/panels.rb +142 -0
  51. data/lib/hecks/composer/parse.rb +83 -0
  52. data/lib/hecks/composer/plain.rb +28 -0
  53. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  54. data/lib/hecks/composer/session.rb +141 -0
  55. data/lib/hecks/composer/toolbox.rb +166 -0
  56. data/lib/hecks/composer/web.rb +156 -0
  57. data/lib/hecks/composer/workbench.rb +51 -0
  58. data/lib/hecks/composer/workspace.rb +81 -0
  59. data/lib/hecks/composer.rb +17 -0
  60. data/lib/hecks/corpus/adapters.rb +22 -0
  61. data/lib/hecks/corpus/rust.rb +1 -1
  62. data/lib/hecks/corpus/worlds.rb +19 -0
  63. data/lib/hecks/corpus.rb +5 -0
  64. data/lib/hecks/forms/app/commands.rb +2 -3
  65. data/lib/hecks/forms/app/queries.rb +2 -3
  66. data/lib/hecks/forms/app/records.rb +17 -5
  67. data/lib/hecks/forms/app/responses.rb +3 -1
  68. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  69. data/lib/hecks/forms/field_controls.rb +2 -2
  70. data/lib/hecks/forms/field_renderer.rb +1 -1
  71. data/lib/hecks/forms/field_shape.rb +36 -17
  72. data/lib/hecks/forms/index_renderer.rb +2 -1
  73. data/lib/hecks/forms/page.rb +1 -1
  74. data/lib/hecks/forms/params.rb +0 -13
  75. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  76. data/lib/hecks/forms/record_renderer.rb +11 -7
  77. data/lib/hecks/forms/rule_words.rb +100 -0
  78. data/lib/hecks/forms/value_object_shape.rb +2 -1
  79. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  80. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  81. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  82. data/lib/hecks/gate/stages.yml +25 -0
  83. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  84. data/lib/hecks/grammar/evolve.rb +0 -3
  85. data/lib/hecks/grammar/translation.bluebook +1 -1
  86. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  87. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  88. data/lib/hecks/hecks/adapters/first_admin.rb +3 -3
  89. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  90. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  91. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  92. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  93. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  94. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  95. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  96. data/lib/hecks/hecks/codebase.behaviors +6 -0
  97. data/lib/hecks/hecks/codebase.bluebook +21 -0
  98. data/lib/hecks/hecks/composer.behaviors +68 -0
  99. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  100. data/lib/hecks/hecks/custodian.behaviors +0 -6
  101. data/lib/hecks/hecks/custodian.bluebook +3 -80
  102. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  103. data/lib/hecks/hecks/hecks.world +1 -1
  104. data/lib/hecks/hecks/rehearsal.bluebook +7 -13
  105. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  106. data/lib/hecks/kernel_capabilities.rb +1 -1
  107. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  108. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  109. data/lib/hecks/ports/access_control.rb +2 -2
  110. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  111. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  112. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  113. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  114. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  115. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  116. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  117. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  118. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  119. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  120. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  121. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  122. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  123. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  124. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  125. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  126. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  127. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  128. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  129. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  130. data/lib/hecks/ports.rb +0 -1
  131. data/lib/hecks/projection_files.rb +3 -1
  132. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  133. data/lib/hecks/projections/glossary/entries.rb +10 -4
  134. data/lib/hecks/projections/glossary/groups.rb +42 -0
  135. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  136. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  137. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  138. data/lib/hecks/projections/glossary/html.rb +1 -2
  139. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  140. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  141. data/lib/hecks/projections/glossary/page.css +143 -153
  142. data/lib/hecks/projections/glossary/page.js +63 -3
  143. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  144. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  145. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  146. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  147. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  148. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  149. data/lib/hecks/projections/glossary.rb +2 -1
  150. data/lib/hecks/projections/plain_english.rb +97 -0
  151. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  152. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  153. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  154. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  155. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  156. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +7 -2
  157. data/lib/hecks/projections/statements/actions.rb +99 -0
  158. data/lib/hecks/projections/statements/relationships.rb +86 -0
  159. data/lib/hecks/projections/statements.rb +15 -24
  160. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  161. data/lib/hecks/projector/exporter/translations.rb +18 -35
  162. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  163. data/lib/hecks/projector/exporter.rb +5 -1
  164. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  165. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  166. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  167. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  168. data/lib/hecks/runtime/outbox.rb +2 -9
  169. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  170. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  171. data/lib/hecks/runtime/value/unwrapping.rb +60 -0
  172. data/lib/hecks/runtime/value.rb +10 -21
  173. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  174. data/lib/hecks/rust_build/wasm.rb +0 -12
  175. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  176. data/lib/hecks/rust_format.rb +77 -0
  177. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  178. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  179. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  180. data/lib/hecks/tools/translation_audit.rb +8 -10
  181. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  182. data/lib/hecks/version.rb +1 -1
  183. data/lib/hecks/vocabulary.rb +2 -1
  184. data/lib/hecks.rb +3 -0
  185. data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → ad68f9e06c8068c2a4176b57abf3083d591d5d8980c2b3e897b97331f965b643.marshal} +0 -0
  186. data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → ee22ec8642c4006717ba2b80f46e5efb81de459eef9dfec043e838c0ad4fd720.marshal} +0 -0
  187. data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-b69f5dcbba79c20daa0493476fa7c317004a30aa2bc3d3dc6a722ea70ae2b43b.json} +1 -1
  188. data/rust/Cargo.toml +40 -0
  189. data/rust/build/Cargo.toml +36 -0
  190. data/rust/build/clippy.toml +4 -0
  191. data/rust/build/src/adapters_pass.rs +196 -0
  192. data/rust/build/src/binding_pass.rs +54 -13
  193. data/rust/build/src/build_artifact.rs +23 -5
  194. data/rust/build/src/cargo_sync.rs +69 -26
  195. data/rust/build/src/lineage_pass.rs +101 -87
  196. data/rust/build/src/main.rs +11 -2
  197. data/rust/build/src/pipeline.rs +80 -19
  198. data/rust/build/src/reserved_names.rs +6 -50
  199. data/rust/build/src/resolve.rs +82 -35
  200. data/rust/build/src/rustfmt.rs +100 -0
  201. data/rust/build/src/seams_pass.rs +85 -16
  202. data/rust/build/src/subprocess.rs +30 -6
  203. data/rust/build/src/tmp.rs +5 -2
  204. data/rust/build/src/translation_pass.rs +72 -22
  205. data/rust/build/src/world_pass.rs +109 -0
  206. data/rust/clippy.toml +4 -0
  207. data/rust/codegen/Cargo.toml +36 -0
  208. data/rust/codegen/clippy.toml +4 -0
  209. data/rust/codegen/src/asks.rs +40 -14
  210. data/rust/codegen/src/attr.rs +2 -2
  211. data/rust/codegen/src/bridging.rs +676 -117
  212. data/rust/codegen/src/commands.rs +979 -373
  213. data/rust/codegen/src/constraints.rs +88 -25
  214. data/rust/codegen/src/dependency_planning.rs +128 -34
  215. data/rust/codegen/src/domain_generator.rs +306 -170
  216. data/rust/codegen/src/exemplar.rs +87 -32
  217. data/rust/codegen/src/expr_emitter.rs +54 -6
  218. data/rust/codegen/src/fielded.rs +193 -41
  219. data/rust/codegen/src/hecks_naming.rs +124 -16
  220. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  221. data/rust/codegen/src/json.rs +80 -44
  222. data/rust/codegen/src/json_codec.rs +585 -149
  223. data/rust/codegen/src/literal.rs +104 -138
  224. data/rust/codegen/src/main.rs +151 -52
  225. data/rust/codegen/src/manifest.rs +76 -14
  226. data/rust/codegen/src/mutations.rs +1212 -239
  227. data/rust/codegen/src/naming.rs +111 -60
  228. data/rust/codegen/src/optional_pass.rs +90 -28
  229. data/rust/codegen/src/ports.rs +5 -4
  230. data/rust/codegen/src/prelude.rs +139 -49
  231. data/rust/codegen/src/presenters.rs +335 -0
  232. data/rust/codegen/src/queries.rs +601 -226
  233. data/rust/codegen/src/reactions.rs +268 -74
  234. data/rust/codegen/src/read_models.rs +574 -229
  235. data/rust/codegen/src/reference_specs.rs +16 -3
  236. data/rust/codegen/src/registry.rs +128 -79
  237. data/rust/codegen/src/reserved_names.rs +6 -50
  238. data/rust/codegen/src/shared.rs +71 -22
  239. data/rust/codegen/src/sidecars.rs +15 -5
  240. data/rust/codegen/src/skip_reason.rs +8 -2
  241. data/rust/codegen/src/types.rs +306 -108
  242. data/rust/host/Cargo.lock +1 -0
  243. data/rust/host/Cargo.toml +40 -4
  244. data/rust/host/HECKS_RELEASE +1 -1
  245. data/rust/host/clippy.toml +4 -0
  246. data/rust/host/src/api.rs +698 -218
  247. data/rust/host/src/auth.rs +491 -214
  248. data/rust/host/src/b64.rs +91 -0
  249. data/rust/host/src/bin/expr_harness.rs +6 -2
  250. data/rust/host/src/bin/lineage_harness.rs +47 -11
  251. data/rust/host/src/bin/mint_harness.rs +70 -37
  252. data/rust/host/src/bin/validate_harness.rs +16 -8
  253. data/rust/host/src/boot.rs +144 -68
  254. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  255. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  256. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  257. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  258. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  259. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  260. data/rust/host/src/checkout.rs +78 -32
  261. data/rust/host/src/clock.rs +8 -0
  262. data/rust/host/src/commerce_ir.rs +333 -95
  263. data/rust/host/src/crypto.rs +33 -0
  264. data/rust/host/src/dispatch.rs +1037 -331
  265. data/rust/host/src/expr_json.rs +1021 -273
  266. data/rust/host/src/extension.rs +24 -4
  267. data/rust/host/src/field_hints.rs +11 -7
  268. data/rust/host/src/fuzz_support.rs +85 -13
  269. data/rust/host/src/host.rs +100 -0
  270. data/rust/host/src/http.rs +22 -0
  271. data/rust/host/src/ir.rs +326 -16
  272. data/rust/host/src/journal.rs +277 -176
  273. data/rust/host/src/lambda_client.rs +219 -53
  274. data/rust/host/src/lib.rs +16 -4
  275. data/rust/host/src/log.rs +62 -11
  276. data/rust/host/src/mint.rs +1153 -326
  277. data/rust/host/src/needs.rs +185 -44
  278. data/rust/host/src/payments/keystore.rs +197 -45
  279. data/rust/host/src/payments/roles.rs +39 -7
  280. data/rust/host/src/payments/tests.rs +1427 -282
  281. data/rust/host/src/payments.rs +391 -112
  282. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  283. data/rust/host/src/platform_signin/config.rs +11 -11
  284. data/rust/host/src/platform_signin/cookie.rs +2 -2
  285. data/rust/host/src/platform_signin/extension.rs +2 -1
  286. data/rust/host/src/platform_signin/handler.rs +9 -8
  287. data/rust/host/src/platform_signin/jti.rs +4 -2
  288. data/rust/host/src/platform_signin/jwks.rs +2 -1
  289. data/rust/host/src/platform_signin/jws.rs +2 -37
  290. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  291. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  292. data/rust/host/src/platform_signin/pages.rs +20 -20
  293. data/rust/host/src/platform_signin/session.rs +8 -1
  294. data/rust/host/src/platform_signin/settings.rs +6 -6
  295. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  296. data/rust/host/src/platform_signin/verify.rs +41 -41
  297. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  298. data/rust/host/src/presentation.rs +207 -63
  299. data/rust/host/src/presentation_write.rs +502 -187
  300. data/rust/host/src/query_step.rs +69 -18
  301. data/rust/host/src/rate_limit.rs +541 -142
  302. data/rust/host/src/reference_transform.rs +77 -25
  303. data/rust/host/src/reference_validate.rs +443 -104
  304. data/rust/host/src/resend.rs +190 -55
  305. data/rust/host/src/secrets.rs +116 -21
  306. data/rust/host/src/server.rs +555 -104
  307. data/rust/host/src/storage_shape.rs +88 -21
  308. data/rust/host/src/test_pg.rs +17 -7
  309. data/rust/host/src/ui_schema.rs +495 -207
  310. data/rust/host/src/wasm_runner.rs +21 -6
  311. data/rust/host/src/web/commerce.rs +101 -23
  312. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  313. data/rust/host/src/web/newsletter/template.rs +26 -7
  314. data/rust/host/src/web/newsletter/tests.rs +112 -23
  315. data/rust/host/src/web/newsletter.rs +209 -59
  316. data/rust/host/src/web/newsletter_send.rs +251 -79
  317. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  318. data/rust/host/src/web/registration_receipt.rs +100 -24
  319. data/rust/host/src/web/registrations.rs +1384 -332
  320. data/rust/host/src/web/resend_webhook.rs +138 -80
  321. data/rust/host/src/web.rs +2416 -508
  322. data/rust/lsp/Cargo.toml +36 -0
  323. data/rust/lsp/clippy.toml +4 -0
  324. data/rust/lsp/src/json.rs +29 -22
  325. data/rust/lsp/src/main.rs +81 -31
  326. data/rust/lsp/src/outline.rs +23 -7
  327. data/rust/lsp/src/rpc.rs +17 -4
  328. data/rust/parser/Cargo.toml +36 -0
  329. data/rust/parser/clippy.toml +4 -0
  330. data/rust/parser/src/build/closed_sets.rs +6 -1
  331. data/rust/parser/src/build/identity.rs +4 -3
  332. data/rust/parser/src/build/mod.rs +1 -1
  333. data/rust/parser/src/build/named_pattern.rs +4 -1
  334. data/rust/parser/src/build/naming.rs +2 -80
  335. data/rust/parser/src/build/pattern_subset.rs +6 -8
  336. data/rust/parser/src/build/query_derive.rs +3 -1
  337. data/rust/parser/src/build/query_inference.rs +14 -5
  338. data/rust/parser/src/build/read_model.rs +4 -1
  339. data/rust/parser/src/build/references.rs +4 -3
  340. data/rust/parser/src/canonical.rs +34 -15
  341. data/rust/parser/src/emit.rs +187 -105
  342. data/rust/parser/src/expr/ast_json.rs +134 -42
  343. data/rust/parser/src/expr/evaluator.rs +27 -9
  344. data/rust/parser/src/expr/mod.rs +54 -13
  345. data/rust/parser/src/expr/resolver.rs +299 -92
  346. data/rust/parser/src/ir.rs +35 -3
  347. data/rust/parser/src/keywords.rs +7168 -516
  348. data/rust/parser/src/lex.rs +11 -25
  349. data/rust/parser/src/lib.rs +1 -0
  350. data/rust/parser/src/main.rs +65 -6
  351. data/rust/parser/src/parse/adapter.rs +157 -0
  352. data/rust/parser/src/parse/aggregate.rs +19 -10
  353. data/rust/parser/src/parse/chapter.rs +16 -17
  354. data/rust/parser/src/parse/command.rs +35 -16
  355. data/rust/parser/src/parse/domain_port.rs +4 -7
  356. data/rust/parser/src/parse/entity.rs +21 -11
  357. data/rust/parser/src/parse/hecksagon.rs +10 -4
  358. data/rust/parser/src/parse/lifecycle.rs +9 -5
  359. data/rust/parser/src/parse/mod.rs +22 -91
  360. data/rust/parser/src/parse/needs.rs +21 -12
  361. data/rust/parser/src/parse/policy.rs +2 -2
  362. data/rust/parser/src/parse/process_manager.rs +9 -11
  363. data/rust/parser/src/parse/query.rs +8 -4
  364. data/rust/parser/src/parse/read_model.rs +57 -16
  365. data/rust/parser/src/parse/value_object.rs +7 -2
  366. data/rust/parser/src/parse/world.rs +322 -0
  367. data/rust/parser/src/ruby_value.rs +7 -6
  368. data/rust/src/exemplar/json.rs +4 -4
  369. data/rust/src/exemplar/mutations.rs +18 -0
  370. data/rust/src/exemplar/read_models.rs +1 -10
  371. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  372. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  373. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  374. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  375. data/rust/src/kernel/cli.rs +244 -114
  376. data/rust/src/kernel/dispatch.rs +323 -115
  377. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  378. data/rust/src/kernel/expr.rs +203 -86
  379. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  380. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  381. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  382. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  383. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  384. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  385. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  386. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  387. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  388. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  389. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  390. data/rust/src/kernel/expression_operators/text.rs +120 -27
  391. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  392. data/rust/src/kernel/json.rs +244 -148
  393. data/rust/src/kernel/mod.rs +47 -33
  394. data/rust/src/kernel/named_query.rs +63 -15
  395. data/rust/src/kernel/needs.rs +112 -25
  396. data/rust/src/kernel/orchestrate.rs +980 -581
  397. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  398. data/rust/src/kernel/pattern.rs +107 -34
  399. data/rust/src/kernel/present.rs +261 -0
  400. data/rust/src/kernel/query_comparators.rs +118 -41
  401. data/rust/src/kernel/query_ordering.rs +13 -5
  402. data/rust/src/kernel/read_model.rs +479 -181
  403. data/rust/src/kernel/reference_lookup.rs +122 -26
  404. data/rust/src/kernel/repository.rs +293 -66
  405. data/rust/src/kernel/routing.rs +58 -42
  406. data/rust/src/kernel/ruby_float.rs +139 -0
  407. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  408. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  409. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  410. data/rust/src/kernel/vocab/mod.rs +6 -6
  411. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  412. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  413. data/rust/src/lib.rs +50 -0
  414. data/rust/src/main.rs +4 -1
  415. data/rust/web/Cargo.toml +36 -0
  416. data/rust/web/clippy.toml +4 -0
  417. metadata +85 -15
  418. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  419. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  420. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  421. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  422. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  423. data/lib/hecks/ports/authentication.port +0 -6
  424. data/lib/hecks/ports/authentication.rb +0 -67
  425. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  426. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  427. data/rust/host/src/approval.rs +0 -652
@@ -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
 
@@ -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
@@ -74,12 +74,9 @@ module Hecks
74
74
 
75
75
  def hecksagon_names_in(path) = File.read(path).scan(HECKSAGON_NAME).flatten
76
76
 
77
- # Deletes every `translations/*.bluebook` in the copy. A compute/rekey translation
78
- # edge refuses to boot under any non-lineage-capable adapter (only `PostgresEra`
79
- # answers `lineage_capable? == true`), so every other mode strips it here rather
80
- # than tripping that refusal. Safe to drop: an ephemeral, zero-history boot never
81
- # has a pre-existing era-1 row to translate — the edge only matters at mint time,
82
- # against a real database, never here.
77
+ # Deletes every `translations/*.bluebook` in the copy. Safe to drop: an ephemeral,
78
+ # zero-history boot never has a pre-existing era-1 row to translate; the edge only
79
+ # matters at mint time, against a real database, never here.
83
80
  def strip_translations!(copy)
84
81
  Dir.glob(File.join(copy, "**", "translations", "*.bluebook")).each { |path| File.delete(path) }
85
82
  end
@@ -1,4 +1,4 @@
1
- require_relative "../../query_specification/common/comparators"
1
+ require_relative "../../vocabulary"
2
2
  require_relative "../../query_specification/common/where_clause"
3
3
  require_relative "../../query_specification/field_path"
4
4
  require_relative "../../ports/query/in_memory"
@@ -9,10 +9,6 @@ module Hecks
9
9
  # Answers the ad hoc `{aggregate:, field:, op:, value:}` filter steps that bypass the
10
10
  # declared query DSL.
11
11
  module Filters
12
- # Mirrors QuerySpecification::Common::COMPARATORS, not the Rust
13
- # kernel's own comparator enum, which is missing `none_in_state` and has drifted.
14
- FILTER_COMPARATORS = Hecks::QuerySpecification::Common::COMPARATORS.map(&:to_s).freeze
15
-
16
12
  # Answers one ad hoc filter step, the mirror image of kernel/cli.rs's own
17
13
  # `run_filter`, calling the same production `Ports::Query::InMemory` rather than
18
14
  # re-deriving comparator behavior by hand. Sorted by id ascending regardless,
@@ -31,7 +27,7 @@ module Hecks
31
27
 
32
28
  # A malformed ad-hoc ask is a fault (C8.3), not a bare RuntimeError.
33
29
  def check_comparator!(comparator)
34
- return if FILTER_COMPARATORS.include?(comparator)
30
+ return if Hecks::Vocabulary.fetch("QueryComparator").include?(comparator)
35
31
 
36
32
  raise Bluebook::Expression::EvaluationError, "unknown query comparator #{comparator.inspect}"
37
33
  end
@@ -84,6 +84,20 @@ pre_push:
84
84
  title: comment blocks, no run of comment lines past ADR 0075's cap
85
85
  run: [bundle, exec, exe/hecks, style_run.check_comments, "paths=lib,spec,examples", only=long_block, --wait]
86
86
  blocked: a comment block ran past the cap; shorten it.
87
+ - &check_rust_fmt
88
+ id: rust_fmt
89
+ title: rustfmt, every Rust crate laid out as `cargo fmt` would
90
+ run:
91
+ - sh
92
+ - -ec
93
+ - |
94
+ # The main crate's generated modules are not committed, so cargo fmt would stop at a
95
+ # missing file on a fresh checkout; its hand-written entry points are named instead.
96
+ rustfmt --check --edition 2021 rust/src/main.rs rust/src/kernel/mod.rs rust/tests/*.rs
97
+ for crate in parser codegen lsp host web build; do
98
+ cargo fmt --check --manifest-path "rust/$crate/Cargo.toml"
99
+ done
100
+ blocked: a Rust file is not laid out as rustfmt has it; run `cargo fmt` in its crate (rust/src/exemplar is exempt, it is text templates). Generated Rust is laid out by its generators, so fix the generator, not the output.
87
101
 
88
102
  # The checks a push of only unguarded lanes (`main`) runs in place of pre_push. The slow suite is
89
103
  # skipped there; the style checks are not, since an offense that lands on `main` is a red CI run,
@@ -140,6 +154,17 @@ ci:
140
154
  - *check_comment_style
141
155
  - *check_comment_blocks
142
156
  - *check_codegen_drift
157
+ - *check_rust_fmt
158
+ - id: rust_clippy
159
+ title: clippy, every Rust crate with its warnings as errors
160
+ run:
161
+ - sh
162
+ - -ec
163
+ - |
164
+ for crate in . parser codegen lsp host web build; do
165
+ cargo clippy --all-targets --manifest-path "rust/$crate/Cargo.toml" -- -D warnings
166
+ done
167
+ blocked: clippy found a warning in a Rust crate, under the strict `[lints.clippy]` table of its Cargo.toml. The main crate needs rust/src/generated, so run `hecks regenerate_corpus --confirm` first on a fresh checkout. Fix the code, or allow a lint at the one site with a line saying why; a warning in generated Rust is fixed in its generator.
143
168
  - id: lint_deploy_recipes
144
169
  title: generated deploy recipes, a command with no checked exit status or no echo
145
170
  run: [bundle, exec, exe/hecks, deploy, makefile_check.lint, --wait]
@@ -157,15 +157,9 @@ module Hecks
157
157
  # First `end` after the opener is the block's own — member rows sit bare (ADR 0025).
158
158
  def keyword_blocks(source) = seed_blocks(source, "KeywordSeed")
159
159
 
160
- # The first KeywordSeed block's source text.
161
- def keyword_block(source) = seed_block(source, "KeywordSeed", required: true)
162
-
163
160
  # ArgumentSeed blocks; see keyword_blocks for why the first bare end
164
161
  # after the opener is already the block's own.
165
162
  def argument_blocks(source) = seed_blocks(source, "ArgumentSeed")
166
-
167
- # The first ArgumentSeed block's source text.
168
- def argument_block(source) = seed_block(source, "ArgumentSeed", required: true)
169
163
  end
170
164
  end
171
165
  end
@@ -96,9 +96,6 @@ module Hecks
96
96
  end
97
97
  end
98
98
 
99
- # Narrow compatibility entry point for single-file callers; syntax_paths is normal.
100
- def syntax_path = syntax_paths.first
101
-
102
99
  # Resolves the file(s) a call should search or write.
103
100
  def paths_for(path) = path ? Array(path) : syntax_paths
104
101
 
@@ -23,7 +23,7 @@ Hecks.bluebook "Translation" do
23
23
  # "retired" is the edge-level declaration (TranslationBuilder#retired).
24
24
  value_object "Kind" do
25
25
  attribute :value, String,
26
- one_of: ["rename", "move", "convert", "drop", "retype", "retired", "compute", "rekey",
26
+ one_of: ["rename", "move", "convert", "drop", "retype", "retired", "derive", "rekey_to",
27
27
  "backfill", "unresolved"]
28
28
  end
29
29
 
@@ -0,0 +1,56 @@
1
+ require_relative "corpus_tasks"
2
+
3
+ module Hecks
4
+ module Adapters
5
+ module Codebase
6
+ # What Codebase's `CorpusRun` asks of the working tree to open the Composer: serve its editor
7
+ # over one domain directory, on the loopback address only, until the server is stopped.
8
+ #
9
+ # The server hook is the one `CorpusTasks` has, so a spec replaces both servers the same way.
10
+ module Composing
11
+ # Every operation this family carries out.
12
+ OPERATIONS = %w[compose].freeze
13
+
14
+ # The port the editor listens on when none is named.
15
+ DEFAULT_PORT = 4570
16
+
17
+ # The directory the editor opens when none is named, relative to the checkout.
18
+ DEFAULT_WORKSPACE = "examples/pizzas/bluebook".freeze
19
+
20
+ # The only address the editor binds: it carries no authentication.
21
+ HOST = "127.0.0.1".freeze
22
+
23
+ module_function
24
+
25
+ # Opens the editor.
26
+ #
27
+ # @param operation [String] `compose`
28
+ # @param held [Hash] the `CorpusRun` record's fields: `port`, `domains` (the directory)
29
+ # @param tree [Tree] the working tree, already known to be a hecks checkout
30
+ # @param shell [#capture, nil] unused
31
+ # @return [String] a note that the server stopped
32
+ def call(operation, held, tree, shell: nil)
33
+ _ = [operation, shell]
34
+ require "hecks/composer/web"
35
+ fields = held.transform_values { |value| value.is_a?(Hash) ? value[:value] : value }
36
+ workspace = File.expand_path(fields[:domains] || DEFAULT_WORKSPACE, tree.root)
37
+ port = fields[:port] || DEFAULT_PORT
38
+ warn "composer over #{workspace} on http://#{HOST}:#{port}/composer - no authentication, " \
39
+ "no CSRF protection, no caller identity; do not expose beyond localhost"
40
+ app = Composer::Web.app(workspace: workspace)
41
+ (CorpusTasks.web_server || method(:serve)).call(app: app, port: port, host: HOST)
42
+ "composed on port #{port}, now stopped"
43
+ end
44
+
45
+ # @param app [#call] the Rack app
46
+ # @param port [Integer] the port
47
+ # @param host [String] the address to bind
48
+ # @return [void]
49
+ def serve(app:, port:, host:)
50
+ require "rackup"
51
+ Rackup::Server.start(app: app, Host: host, Port: port, server: "webrick")
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -11,6 +11,7 @@ require_relative "inspection"
11
11
  require_relative "codemods"
12
12
  require_relative "test_suite"
13
13
  require_relative "corpus_tasks"
14
+ require_relative "composing"
14
15
  require_relative "publishing"
15
16
  require_relative "promotion"
16
17
  require_relative "../git"
@@ -183,7 +184,8 @@ module Hecks
183
184
  # The task families, each carrying out the operations it lists.
184
185
  FAMILIES = [Codebase::Language, Codebase::KernelTables, Codebase::Conformance, Codebase::Regeneration,
185
186
  Codebase::Gate, Codebase::Style, Codebase::Inspection, Codebase::Codemods, Codebase::TestSuite,
186
- Codebase::CorpusTasks, Codebase::Publishing, Codebase::Promotion].freeze
187
+ Codebase::CorpusTasks, Codebase::Composing, Codebase::Publishing,
188
+ Codebase::Promotion].freeze
187
189
  private_constant :FAMILIES
188
190
 
189
191
  # The facts of a promotion or a watch, naming the operation so policies can tell them apart.
@@ -57,7 +57,7 @@ module Hecks
57
57
  provider = membership_provider
58
58
  verbs, gate, people = check_unadministered(provider)
59
59
 
60
- admitted = people.none? { |person| person.dig(:email, :value) == email }
60
+ admitted = people.none? { |person| person[:email] == email }
61
61
  admit(provider, verbs.fetch(:admit), email, name || email[/\A[^@]+/]) if admitted
62
62
  grant(provider, verbs.fetch(:grant), email, role || gate)
63
63
  Result.new(email: email, role: role || gate, admitted: admitted)
@@ -103,10 +103,10 @@ module Hecks
103
103
  end
104
104
 
105
105
  def refuse_if_administered!(people, roles)
106
- held = people.select { |person| roles.map(&:downcase).include?(person.dig(:role, :value).to_s.downcase) }
106
+ held = people.select { |person| roles.map(&:downcase).include?(person[:role].to_s.downcase) }
107
107
  return if held.empty?
108
108
 
109
- who = held.map { |person| person.dig(:email, :value) }.join(", ")
109
+ who = held.map { |person| person[:email] }.join(", ")
110
110
  raise Runtime::NotFound, "an administrator already exists (#{who}); grant roles through the " \
111
111
  "domain's own #{roles.first} access instead"
112
112
  end
@@ -117,32 +117,6 @@ module Hecks
117
117
  def apply_compaction(held, kind)
118
118
  Compaction.new(plain(held[:domain]), aggregates: names(held[:aggregates]), kind: kind).apply!.join("\n")
119
119
  end
120
-
121
- # Writes `translations/<edge>.approval`; the host reads it at its next boot.
122
- def apply_approve_translation(held)
123
- domain = HeldDomain.open(plain(held[:domain]))
124
- finding = domain.reading { |lineage| audited_edge(domain, lineage) }
125
- document = approval_document(domain, finding, held)
126
- path = Translation::ApprovalFile.write!(domain.directory, finding.edge, document)
127
- "approval of edge #{document["edge"]} written to #{path} (digest #{document["edge_digest"][0, 12]}…, " \
128
- "by #{document["approved_by"]}). Commit it with the edge; the host applies it at its next boot."
129
- end
130
-
131
- def approval_document(domain, finding, held)
132
- Translation::ApprovalFile.build(
133
- edge: finding.edge, approved_by: Git.new.identity(chdir: domain.directory),
134
- approved_at: Time.now.utc.iso8601, rehearsal: rehearsal_block(held)
135
- )
136
- end
137
-
138
- def audited_edge(domain, lineage)
139
- eras = domain.held_eras(lineage)
140
- finding = eras.empty? ? nil : EdgeAudit.call(domain, lineage, eras)
141
- raise Runtime::WiringError, "#{domain.bluebook.name} has no edge to approve" unless finding
142
- raise Runtime::WiringError, "#{finding.report}\nAUDIT REFUSED." unless finding.ok
143
-
144
- finding
145
- end
146
120
  end
147
121
  end
148
122
  end