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
@@ -0,0 +1,156 @@
1
+ require "json"
2
+ require "rack"
3
+ require_relative "../composer"
4
+ require_relative "domains"
5
+ require_relative "workbench"
6
+
7
+ module Hecks
8
+ module Composer
9
+ # The Composer's driving side, on the hecksagon side: a Rack app that serves the editor page
10
+ # and the JSON routes it speaks to, over one workspace directory at a time. It binds no
11
+ # business meaning. The routes are the tables below: the page and files, the editor's own
12
+ # (save, remove, parse, check, ask), the domain list, and the lower panels', which {Panels}
13
+ # answers.
14
+ class Web
15
+ PAGE = File.expand_path("page.html", __dir__).freeze
16
+ PREFIX = "/composer".freeze
17
+
18
+ # @param workspace [String] the domain directory the editor opens
19
+ # @param agents [Runtime::Registry, nil] see {Workbench#initialize}
20
+ # @param root [String] the checkout whose domains the picker lists
21
+ # @return [Web] a Rack app
22
+ def self.app(workspace:, agents: nil, root: Parse::ROOT) = new(workspace: workspace, agents: agents, root: root)
23
+
24
+ # @param workspace [String] the domain directory the editor opens
25
+ # @param agents [Runtime::Registry, nil] see {Workbench#initialize}
26
+ # @param root [String] the checkout whose domains the picker lists
27
+ def initialize(workspace:, agents: nil, root: Parse::ROOT)
28
+ @agents = agents
29
+ @root = File.expand_path(root)
30
+ @bench = Workbench.new(workspace: workspace, agents: agents)
31
+ end
32
+
33
+ # @param env [Hash] the Rack environment
34
+ # @return [Array] the Rack response triple
35
+ def call(env)
36
+ request = Rack::Request.new(env)
37
+ route(request)
38
+ rescue Workspace::OutsideWorkspace, KeyError, JSON::ParserError => e
39
+ json(400, error: e.message)
40
+ rescue Domains::Unknown => e
41
+ json(404, error: e.message)
42
+ rescue Parse::ParserMissing => e
43
+ json(503, error: e.message)
44
+ end
45
+
46
+ private
47
+
48
+ # Routes that answer from the workspace alone, those that read a JSON body first, and those
49
+ # the lower panels ask (answered by {Panels}).
50
+ READS = { "GET " => :page, "GET #{PREFIX}" => :page, "GET #{PREFIX}/files" => :listing,
51
+ "GET #{PREFIX}/draft" => :conversation, "GET #{PREFIX}/domains" => :domains }.freeze
52
+ WRITES = { "PUT #{PREFIX}/file" => :save, "DELETE #{PREFIX}/file" => :remove,
53
+ "POST #{PREFIX}/parse" => :parse, "POST #{PREFIX}/check" => :check,
54
+ "POST #{PREFIX}/ask" => :ask, "POST #{PREFIX}/open" => :open_domain }.freeze
55
+ PANEL_ROUTES = { "POST #{PREFIX}/live/commands" => :commands, "POST #{PREFIX}/live/dispatch" => :dispatch,
56
+ "POST #{PREFIX}/live/records" => :records, "POST #{PREFIX}/behaviors" => :behaviors,
57
+ "POST #{PREFIX}/timeline" => :timeline, "POST #{PREFIX}/board/edit" => :board_edit,
58
+ "GET #{PREFIX}/catalog" => :catalog,
59
+ "POST #{PREFIX}/help" => :help, "POST #{PREFIX}/run" => :run,
60
+ "GET #{PREFIX}/eras" => :eras, "POST #{PREFIX}/job" => :start_job,
61
+ "GET #{PREFIX}/job" => :job_state, "DELETE #{PREFIX}/job" => :cancel_job }.freeze
62
+ private_constant :READS, :WRITES, :PANEL_ROUTES
63
+
64
+ def route(request)
65
+ key = "#{request.request_method} #{request.path_info.chomp("/")}"
66
+ answer_for(key, request) || json(404, error: "no such route")
67
+ end
68
+
69
+ # The response the key's table gives, or nil when no table has the key.
70
+ def answer_for(key, request)
71
+ return send(READS[key]) if READS.key?(key)
72
+ return send(WRITES[key], body(request)) if WRITES.key?(key)
73
+
74
+ json(200, @bench.panels.public_send(PANEL_ROUTES[key], body(request))) if PANEL_ROUTES.key?(key)
75
+ end
76
+
77
+ def workspace = @bench.workspace
78
+
79
+ def listing = json(200, files: files)
80
+
81
+ def conversation = json(200, draft: @bench.session.summary(@bench.slug))
82
+
83
+ # The known domains, and the directory the editor has open now.
84
+ def domains = json(200, domains: Domains.known(root: @root), current: File.basename(workspace))
85
+
86
+ # Opens a known domain as a scratch copy, replacing everything kept for the old workspace.
87
+ def open_domain(body)
88
+ id = body.fetch("id")
89
+ opened = Domains.open(root: @root, id: id)
90
+ @bench.close
91
+ @bench = Workbench.new(workspace: opened, agents: @agents)
92
+ json(200, opened: id, current: File.basename(opened))
93
+ end
94
+
95
+ def ask(body)
96
+ json(200, @bench.assistant.ask(body.fetch("prompt")))
97
+ rescue Agent::Unavailable => e
98
+ json(503, error: e.message)
99
+ rescue Agent::ValidationError => e
100
+ json(502, error: e.message)
101
+ end
102
+
103
+ def page = [200, { "content-type" => "text/html; charset=utf-8" }, [File.read(PAGE)]]
104
+
105
+ def files
106
+ Workspace.files(workspace).map { |path| { path: path, text: File.read(File.join(workspace, path)) } }
107
+ end
108
+
109
+ def save(body)
110
+ path = domain_path(body) or return refused(body)
111
+
112
+ target = File.join(workspace, path)
113
+ FileUtils.mkdir_p(File.dirname(target))
114
+ File.write(target, body.fetch("text"))
115
+ @bench.session.record(path: path, text: body.fetch("text"))
116
+ json(200, saved: path)
117
+ end
118
+
119
+ def remove(body)
120
+ path = domain_path(body) or return refused(body)
121
+
122
+ FileUtils.rm_f(File.join(workspace, path))
123
+ json(200, removed: path)
124
+ end
125
+
126
+ # The request's path when it names a domain file inside the workspace, else nil.
127
+ def domain_path(body)
128
+ path = body.fetch("path")
129
+ Workspace.refuse_escapes!([path])
130
+ path if Workspace::EXTENSIONS.include?(File.extname(path).delete_prefix("."))
131
+ end
132
+
133
+ def refused(body) = json(400, error: "#{body["path"].inspect} is not a domain file")
134
+
135
+ def parse(body)
136
+ refusal = Workspace.stage(dir: workspace, buffers: buffers(body)) { |stage| Parse.call(stage) }
137
+ json(200, diagnostic: refusal)
138
+ end
139
+
140
+ def check(body)
141
+ json(200, Check.call(dir: workspace, buffers: buffers(body), chapter_name: body["chapter_name"]))
142
+ end
143
+
144
+ def buffers(body) = body.fetch("buffers", {})
145
+
146
+ def body(request)
147
+ raw = request.body&.read.to_s
148
+ raw.empty? ? {} : JSON.parse(raw)
149
+ end
150
+
151
+ def json(status, payload)
152
+ [status, { "content-type" => "application/json" }, [JSON.generate(payload)]]
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,51 @@
1
+ require_relative "agent"
2
+ require_relative "assistant"
3
+ require_relative "panels"
4
+ require_relative "session"
5
+
6
+ module Hecks
7
+ module Composer
8
+ # Everything the editor keeps for one workspace, on the hecksagon side: the draft bookkeeping,
9
+ # the agent that edits it and the panels beneath the editor. Opening another domain builds a
10
+ # new workbench, so nothing of the old workspace carries over.
11
+ class Workbench
12
+ # @return [String] the domain directory this workbench is for
13
+ attr_reader :workspace
14
+
15
+ # @param workspace [String] the domain directory
16
+ # @param agents [Runtime::Registry, nil] a registry holding the `composer_agent` port and one
17
+ # adapter; the one `HECKS_COMPOSER_AGENT` names (`claude`, the default, or `fake`) when nil
18
+ def initialize(workspace:, agents: nil)
19
+ @workspace = File.expand_path(workspace)
20
+ @agents = agents
21
+ end
22
+
23
+ # @return [String] the workspace's directory name, which names its draft
24
+ def slug = File.basename(workspace)
25
+
26
+ # @return [Session] the draft bookkeeping, booted on first use
27
+ def session = @session ||= Session.new
28
+
29
+ # @return [Assistant] the agent loop over this workspace
30
+ def assistant = @assistant ||= Assistant.new(workspace: workspace, agents: agents, session: session)
31
+
32
+ # @return [Panels] what answers the lower panels
33
+ def panels = @panels ||= Panels.new(workspace: workspace, session: session)
34
+
35
+ # Releases what the panels booted.
36
+ # @return [void]
37
+ def close = @panels&.close
38
+
39
+ private
40
+
41
+ def agents = @agents ||= default_agents
42
+
43
+ # The adapter `HECKS_COMPOSER_AGENT` names: the real `claude`, or a scripted `fake`.
44
+ def default_agents
45
+ name = ENV.fetch("HECKS_COMPOSER_AGENT", "claude") == "fake" ? "fake_agent" : "claude_composer"
46
+ require_relative "adapters/#{name}"
47
+ Agent.registry_with(File.join(Agent::ADAPTERS, "#{name}.adapter"))
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,81 @@
1
+ require "fileutils"
2
+ require "tmpdir"
3
+
4
+ module Hecks
5
+ module Composer
6
+ # A workspace is a directory of domain files; an editor holds unsaved text for some of them.
7
+ # Staging lays that text over a copy of the files on disk so a check sees what is on screen
8
+ # and the directory itself stays untouched.
9
+ module Workspace
10
+ # The files a domain is made of; anything else in the directory is data, not declaration.
11
+ EXTENSIONS = %w[bluebook hecksagon world adapter port behaviors].freeze
12
+
13
+ # Raised when an unsaved buffer names a path outside the workspace.
14
+ class OutsideWorkspace < ArgumentError; end
15
+
16
+ module_function
17
+
18
+ # Copies the workspace's declaration files into a temporary directory, lays the buffers over
19
+ # them, and yields that directory.
20
+ #
21
+ # @param dir [String] the workspace directory
22
+ # @param buffers [Hash{String => String}] unsaved text by path relative to `dir`; a path
23
+ # with no file on disk yet is created
24
+ # @yieldparam stage [String] the temporary directory holding the staged files
25
+ # @return [Object] what the block returned
26
+ # @raise [OutsideWorkspace] if a buffer's path is absolute or climbs out of the workspace
27
+ def stage(dir:, buffers: {})
28
+ stage = materialize(dir: dir, buffers: buffers)
29
+ yield stage
30
+ ensure
31
+ FileUtils.rm_rf(stage) if stage
32
+ end
33
+
34
+ # Stages as {stage} does, but leaves the directory for the caller to remove, so a booted
35
+ # domain can keep reading its files after this returns.
36
+ #
37
+ # @param dir [String] the workspace directory
38
+ # @param buffers [Hash{String => String}] unsaved text by path relative to `dir`
39
+ # @return [String] the temporary directory holding the staged files
40
+ # @raise [OutsideWorkspace] if a buffer's path is absolute or climbs out of the workspace
41
+ def materialize(dir:, buffers: {})
42
+ refuse_escapes!(buffers.keys)
43
+ stage = Dir.mktmpdir("hecks-composer")
44
+ copy_declarations(dir, stage)
45
+ buffers.each { |path, text| write(File.join(stage, path), text) }
46
+ stage
47
+ rescue StandardError
48
+ FileUtils.rm_rf(stage) if stage
49
+ raise
50
+ end
51
+
52
+ # @param stage [String] a staged directory
53
+ # @return [Array<String>] the declaration files in it, relative and sorted
54
+ def files(stage)
55
+ Dir.glob("**/*.{#{EXTENSIONS.join(",")}}", base: stage).sort
56
+ end
57
+
58
+ # @api private
59
+ def copy_declarations(dir, stage)
60
+ files(dir).each do |relative|
61
+ FileUtils.mkdir_p(File.dirname(File.join(stage, relative)))
62
+ FileUtils.cp(File.join(dir, relative), File.join(stage, relative))
63
+ end
64
+ end
65
+
66
+ # @api private
67
+ def write(path, text)
68
+ FileUtils.mkdir_p(File.dirname(path))
69
+ File.write(path, text)
70
+ end
71
+
72
+ # @api private
73
+ def refuse_escapes!(paths)
74
+ offender = paths.find { |path| path.start_with?("/") || path.split("/").include?("..") }
75
+ return unless offender
76
+
77
+ raise OutsideWorkspace, "#{offender.inspect} is not inside the workspace"
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,17 @@
1
+ require_relative "composer/plain"
2
+ require_relative "composer/workspace"
3
+ require_relative "composer/parse"
4
+ require_relative "composer/outline"
5
+ require_relative "composer/layout"
6
+ require_relative "composer/behavior_run"
7
+ require_relative "composer/live"
8
+ require_relative "composer/toolbox"
9
+ require_relative "composer/jobs"
10
+ require_relative "composer/check"
11
+
12
+ module Hecks
13
+ # The Composer's outside, on the hecksagon side: staging a workspace, judging its text and
14
+ # projecting what it declares. The Composer chapter's bluebook is in `composer/bluebook/`.
15
+ module Composer
16
+ end
17
+ end
@@ -0,0 +1,22 @@
1
+ module Hecks
2
+ module Corpus
3
+ # Where `.adapter` files live, for the parser parity check. Extended onto `Corpus`.
4
+ module Adapters
5
+ # Directories that hold copies or build output, never a source `.adapter`.
6
+ EXCLUDED_DIRS = %w[vendor node_modules target tmp .git .aws-sam].freeze
7
+
8
+ ADAPTER_GLOBS = %w[lib/**/*.adapter examples/**/*.adapter spec/fixtures/**/*.adapter].freeze
9
+
10
+ # Every `.adapter` file the repo carries.
11
+ #
12
+ # @param root [String] repository root to search under
13
+ # @return [Array<String>] absolute paths, sorted, none under a vendored, `node_modules`,
14
+ # `target` or scratch directory
15
+ def adapter_files(root: Corpus::ROOT)
16
+ ADAPTER_GLOBS.flat_map { |glob| Dir.glob(File.join(root, glob)) }.uniq.sort.reject do |path|
17
+ path.delete_prefix("#{root}/").split("/").intersect?(EXCLUDED_DIRS)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -3,7 +3,7 @@ module Hecks
3
3
  # Every Rust-facing list reads `rust/Cargo.toml`'s `[features]` as its
4
4
  # one source, rather than each consumer typing it out by hand.
5
5
  GENERATED_DIR = "rust/src/generated".freeze
6
- RUST_DOMAIN_KINDS = %i[example stress fixture].freeze
6
+ RUST_DOMAIN_KINDS = %i[example stress semantics fixture].freeze
7
7
 
8
8
  RustDomain = Struct.new(:feature, :dir, :kind)
9
9
 
@@ -0,0 +1,19 @@
1
+ module Hecks
2
+ module Corpus
3
+ # Where `.world` files live, for the parser parity check. Extended onto `Corpus`.
4
+ module Worlds
5
+ WORLD_GLOBS = %w[lib/**/*.world examples/**/*.world qa/**/*.world spec/fixtures/**/*.world].freeze
6
+
7
+ # Every `.world` file the repo carries.
8
+ #
9
+ # @param root [String] repository root to search under
10
+ # @return [Array<String>] absolute paths, sorted, none under a vendored, `node_modules`,
11
+ # `target` or scratch directory
12
+ def world_files(root: Corpus::ROOT)
13
+ WORLD_GLOBS.flat_map { |glob| Dir.glob(File.join(root, glob)) }.uniq.sort.reject do |path|
14
+ path.delete_prefix("#{root}/").split("/").intersect?(Adapters::EXCLUDED_DIRS)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
data/lib/hecks/corpus.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  require_relative "fuzzing/target_capabilities"
2
+ require_relative "corpus/adapters"
2
3
  require_relative "corpus/routes"
3
4
  require_relative "corpus/rust"
4
5
  require_relative "corpus/sources"
6
+ require_relative "corpus/worlds"
5
7
 
6
8
  module Hecks
7
9
  # The corpus: every place in this repo that holds a real domain, named once.
@@ -36,6 +38,7 @@ module Hecks
36
38
  deploy: "lib/hecks/deploy/bluebook/*.bluebook",
37
39
  site: "lib/hecks/site/bluebook/*.bluebook",
38
40
  tickets: "lib/hecks/tickets/bluebook/*.bluebook",
41
+ composer: "lib/hecks/composer/bluebook/*.bluebook",
39
42
  tenancy: "lib/hecks/tenancy/bluebook/*.bluebook",
40
43
  sme: "lib/hecks/sme/bluebook/*.bluebook",
41
44
  fixture: "spec/fixtures/**/*.bluebook"
@@ -43,8 +46,10 @@ module Hecks
43
46
 
44
47
  KINDS = (DIRECTORY_KINDS.keys + FILE_KINDS.keys).freeze
45
48
 
49
+ extend Adapters
46
50
  extend Rust
47
51
  extend Sources
52
+ extend Worlds
48
53
 
49
54
  module_function
50
55
 
@@ -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)
@@ -1,5 +1,6 @@
1
1
  require "json"
2
2
  require_relative "../page"
3
+ require_relative "../../runtime/value"
3
4
 
4
5
  module Hecks
5
6
  module Forms
@@ -13,8 +14,9 @@ module Hecks
13
14
  respond(status, "text/html; charset=utf-8", Page.render(title: title, body: body, breadcrumbs: breadcrumbs))
14
15
  end
15
16
 
17
+ # A single-field value object answers as its bare field, as `Json.materialize` does.
16
18
  def json(status, payload)
17
- respond(status, "application/json", JSON.pretty_generate(payload))
19
+ respond(status, "application/json", JSON.pretty_generate(Runtime::Value.materialize_unwrapped(payload)))
18
20
  end
19
21
 
20
22
  def redirect(location)
@@ -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