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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52208049da300a9004b163c5e48cfe5587711ad45a3cb4d17a5bd1aa4eca8567
4
- data.tar.gz: 1788cde0c4a89814395a4be9b3396cc8b5fd823aa408da88534aae617afbd0f1
3
+ metadata.gz: 4a6fed3e5b894585b2fa7253696873a4218980f6982b4afeeda5effdd5a4aba4
4
+ data.tar.gz: b29a6ce60d5ae9142f0b53ee062979397f3c1384ce2f6da8c70b14396425c989
5
5
  SHA512:
6
- metadata.gz: 5ad958aea6cbf148bf0164808eae59179ec03743f1591f193f85e196c9aa7c5a25376d9857ad3d42d57303c7af721811cddf11f776c88f48914f46c4522edc87
7
- data.tar.gz: d1148d6aa2ced591c069816c3f17047d08d0aa902e03340dbd4b7c35c20be8eb0907bda11f00c746fdc8955ae0a9225aafe43e3b92715efb42df0f0d73c3b0a8
6
+ metadata.gz: 77b6974a39536f06fd5bffeb121899ca35b1563e5db172bac48dfeef426c445ed459bea60bc004ea29fdb14b14149a72467b61dddce02aa1cc61b2bb9c11b944
7
+ data.tar.gz: 56fd3680ca94d0060d0ff5e1324c3ccbefb03f5dfc4ac03760cdfb8ee6ad2081c70cef85e4161a66098acb396bdb17c2a485185ff473bee49546a9f0a7336d65
@@ -0,0 +1,34 @@
1
+ require "json"
2
+ require "securerandom"
3
+ require_relative "payment_signature"
4
+
5
+ module PizzaShop
6
+ # A stand-in for the payment processor, for tests and a local run: it writes the delivery a
7
+ # Stripe-style processor would send when a payment succeeds, and signs it with the shared
8
+ # secret. It talks to no network and holds no real credentials.
9
+ #
10
+ # Only the processor is fake. `PaymentWebhook`, which receives the delivery, is the adapter a
11
+ # real deployment runs.
12
+ class FakeProcessor
13
+ # @param secret [String] the webhook signing secret this processor shares with the shop
14
+ def initialize(secret:)
15
+ @secret = secret
16
+ end
17
+
18
+ # Writes and signs the delivery for a payment that succeeded.
19
+ #
20
+ # @param pizza [String] the pizza's name, carried in `metadata` as the shop attached it
21
+ # @param customer_name [String] who paid, also carried in `metadata`
22
+ # @param cents [Integer] the amount the processor received, in cents
23
+ # @param at [Time] the moment the delivery is signed
24
+ # @return [Array(String, String)] the JSON body and its signature header value
25
+ def payment_succeeded(pizza:, customer_name:, cents:, at: Time.now)
26
+ body = JSON.generate(
27
+ id: "evt_#{SecureRandom.hex(8)}",
28
+ type: PaymentWebhook::SUCCEEDED,
29
+ data: { object: { amount_received: cents, metadata: { pizza: pizza, customer_name: customer_name } } }
30
+ )
31
+ [body, PaymentSignature.sign(@secret, body, at: at)]
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,66 @@
1
+ require "openssl"
2
+ require "rack/utils"
3
+
4
+ module PizzaShop
5
+ # The signature a payment processor puts on each webhook delivery, in the timestamped scheme
6
+ # (`t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<body>">`) that Stripe-style processors use.
7
+ #
8
+ # Kept apart from `PaymentWebhook` so the same scheme signs a delivery (`FakeProcessor`) and
9
+ # checks one, and so the check can be read without the HTTP around it.
10
+ module PaymentSignature
11
+ # Raised for a delivery that cannot prove it came from the processor.
12
+ class Invalid < StandardError; end
13
+
14
+ # How far either side of now a delivery's timestamp may sit before it is refused as a
15
+ # replay, in seconds.
16
+ TOLERANCE = 300
17
+
18
+ module_function
19
+
20
+ # Signs a body the way the processor does.
21
+ #
22
+ # @param secret [String] the shared webhook signing secret
23
+ # @param body [String] the exact bytes of the delivery
24
+ # @param at [Time] the moment the delivery is signed
25
+ # @return [String] the header value, `t=<unix seconds>,v1=<hex digest>`
26
+ def sign(secret, body, at: Time.now)
27
+ stamp = at.to_i
28
+ "t=#{stamp},v1=#{digest(secret, stamp, body)}"
29
+ end
30
+
31
+ # Checks a delivery's signature header against its body.
32
+ #
33
+ # Compared in constant time: `==` leaks how many leading bytes of a forged digest matched.
34
+ #
35
+ # @param secret [String] the shared webhook signing secret
36
+ # @param header [String, nil] the delivery's signature header value
37
+ # @param body [String] the exact bytes of the delivery
38
+ # @param now [Time] the moment the delivery arrived
39
+ # @return [void]
40
+ # @raise [Invalid] if the header is missing or malformed, the timestamp is outside
41
+ # `TOLERANCE`, or the digest does not match
42
+ def verify!(secret, header, body, now: Time.now)
43
+ stamp, signed = parts(header)
44
+ raise Invalid, "the signature is #{(now.to_i - stamp).abs}s old, past #{TOLERANCE}s" if stale?(stamp, now)
45
+ return if Rack::Utils.secure_compare(digest(secret, stamp, body), signed)
46
+
47
+ raise Invalid, "the signature does not match the body"
48
+ end
49
+
50
+ # @api private
51
+ def parts(header)
52
+ fields = header.to_s.split(",").to_h { |pair| pair.split("=", 2) }
53
+ raise Invalid, "no signature, or not in the t=…,v1=… form" unless fields["t"].to_s.match?(/\A\d+\z/) && fields["v1"]
54
+
55
+ [fields["t"].to_i, fields["v1"]]
56
+ end
57
+
58
+ # @api private
59
+ def stale?(stamp, now) = (now.to_i - stamp).abs > TOLERANCE
60
+
61
+ # @api private
62
+ def digest(secret, stamp, body)
63
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, "#{stamp}.#{body}")
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,87 @@
1
+ require "json"
2
+ require "rack"
3
+ require_relative "payment_signature"
4
+
5
+ module PizzaShop
6
+ # The driving adapter between a payment processor and Pizzas: a Rack app that verifies the
7
+ # processor's signed "payment succeeded" delivery and restates it in the domain's words through
8
+ # `Order.PaymentGateway.Receive`, the port `pizzas.hecksagon` declares.
9
+ #
10
+ # It does not decide whether the pizza sells. The port only emits `PizzaPaymentReceived`;
11
+ # `OnPizzaPaymentReceived` reaches `Purchase`, whose `given` rules decide. A payment the domain
12
+ # declines is still answered 200, saying why: the fact is true, and a non-2xx would only make
13
+ # the processor retry it.
14
+ class PaymentWebhook
15
+ # The header the processor signs each delivery in.
16
+ SIGNATURE_HEADER = "HTTP_STRIPE_SIGNATURE".freeze
17
+
18
+ # The processor's event type for money that arrived; every other type is acknowledged.
19
+ SUCCEEDED = "payment_intent.succeeded".freeze
20
+
21
+ # `secret:` has no default: a missing secret would silently accept anyone's delivery.
22
+ #
23
+ # @param runtime [Hecks::Runtime::Dispatcher] the booted Pizzas runtime the port is told on
24
+ # @param secret [String] the processor's webhook signing secret
25
+ # @param clock [#call] answers the current `Time`, so a test can sign in the past
26
+ # @raise [ArgumentError] if `secret` is nil or empty
27
+ def initialize(runtime:, secret:, clock: -> { Time.now })
28
+ raise ArgumentError, "no payment webhook secret configured" if secret.to_s.empty?
29
+
30
+ @runtime = runtime
31
+ @secret = secret
32
+ @clock = clock
33
+ end
34
+
35
+ # Answers one delivery from the processor.
36
+ #
37
+ # @param env [Hash{String => Object}] the Rack environment
38
+ # @return [Array(Integer, Hash{String => String}, Array<String>)] the Rack response: 405 for
39
+ # anything but POST, 401 for a bad signature, 400 for a body that is not JSON, 422 for a
40
+ # payment the domain cannot read, otherwise 200 with what happened as JSON
41
+ def call(env)
42
+ request = Rack::Request.new(env)
43
+ return respond(405, error: "MethodNotAllowed", message: "POST only") unless request.post?
44
+
45
+ body = request.body.read
46
+ PaymentSignature.verify!(@secret, request.get_header(SIGNATURE_HEADER), body, now: @clock.call)
47
+ route(JSON.parse(body))
48
+ rescue PaymentSignature::Invalid => e
49
+ respond(401, error: "InvalidSignature", message: e.message)
50
+ rescue JSON::ParserError => e
51
+ respond(400, error: "MalformedPayload", message: e.message)
52
+ end
53
+
54
+ private
55
+
56
+ def route(event)
57
+ return respond(200, ok: true, ignored: event["type"].to_s) unless event["type"] == SUCCEEDED
58
+
59
+ receive(event.dig("data", "object") || {})
60
+ end
61
+
62
+ def receive(payment)
63
+ pizza, customer = payment.fetch("metadata", {}).values_at("pizza", "customer_name")
64
+ return unreadable("metadata.pizza and metadata.customer_name name the order") unless pizza && customer
65
+
66
+ respond(200, ok: true, pizza: pizza, **tell(pizza, customer, payment["amount_received"]))
67
+ rescue *Hecks::Runtime::DOMAIN_REFUSALS => e
68
+ unreadable(e.message)
69
+ end
70
+
71
+ # Reads this delivery's reactions off the log by position, which assumes deliveries are
72
+ # handled one at a time, as the single-threaded development server does.
73
+ def tell(pizza, customer, cents)
74
+ before = @runtime.registry.reaction_log.size
75
+ result = @runtime.dispatch("Pizzas::Order.PaymentGateway.Receive",
76
+ to: pizza, with: { customer_name: { value: customer }, amount: { cents: cents } })
77
+ declined = @runtime.registry.reaction_log[before..].find { |reaction| !reaction[:delivered] }
78
+ { events: result.events.map(&:name), sold: declined.nil?, refused: declined&.dig(:reason) }.compact
79
+ end
80
+
81
+ def unreadable(message) = respond(422, error: "UnreadablePayment", message: message)
82
+
83
+ def respond(status, body)
84
+ [status, { "content-type" => "application/json" }, [JSON.generate(body)]]
85
+ end
86
+ end
87
+ end
@@ -45,6 +45,39 @@ Hecks.behaviors "Pizzas" do
45
45
  expect emits: ["PizzaPurchased"]
46
46
  end
47
47
 
48
+ test "Purchase refuses a payment that does not cover the price" do
49
+ tests "Purchase", on: "Order"
50
+ setup "CreatePizza", name: "Underpaid", pizza: { price_cents: { cents: 1200 }, size: "large" }
51
+ setup "AddTopping", name: "Underpaid", topping: "Basil", amount: 1
52
+ input name: "Underpaid", customer_name: "Chris", amount: { cents: 500 }
53
+ expect refused: "the payment covers the price"
54
+ end
55
+
56
+ # The processor's way in: the port only announces the payment, and OnPizzaPaymentReceived
57
+ # carries it to Purchase, whose rules decide whether the pizza sells.
58
+ test "a payment received through the gateway sells the pizza" do
59
+ tests "Pizzas::Order.PaymentGateway.Receive"
60
+ setup "CreatePizza", name: "Paid For", pizza: { price_cents: { cents: 1200 }, size: "large" }
61
+ setup "AddTopping", name: "Paid For", topping: "Basil", amount: 1
62
+ input to: "Paid For", with: { customer_name: { value: "Chris" }, amount: { cents: 1200 } }
63
+ expect emits: ["PizzaPaymentReceived", "PizzaPurchased"]
64
+ end
65
+
66
+ test "an underpayment through the gateway is recorded, but sells nothing" do
67
+ tests "Pizzas::Order.PaymentGateway.Receive"
68
+ setup "CreatePizza", name: "Short", pizza: { price_cents: { cents: 1200 }, size: "large" }
69
+ setup "AddTopping", name: "Short", topping: "Basil", amount: 1
70
+ input to: "Short", with: { customer_name: { value: "Chris" }, amount: { cents: 500 } }
71
+ expect emits: ["PizzaPaymentReceived"]
72
+ end
73
+
74
+ test "a payment through the gateway for a pizza with no toppings sells nothing" do
75
+ tests "Pizzas::Order.PaymentGateway.Receive"
76
+ setup "CreatePizza", name: "Plain", pizza: { price_cents: { cents: 900 }, size: "small" }
77
+ input to: "Plain", with: { customer_name: { value: "Chris" }, amount: { cents: 900 } }
78
+ expect emits: ["PizzaPaymentReceived"]
79
+ end
80
+
48
81
  test "a sold pizza is no longer Available" do
49
82
  tests "Available", on: "Order", kind: :query
50
83
  setup "CreatePizza", name: "Fresh", pizza: { price_cents: { cents: 1000 }, size: "small" }
@@ -5,14 +5,16 @@ Hecks.hecksagon "Pizzas" do
5
5
  Pizzas::Order.persisted_by("Memory")
6
6
 
7
7
  # Every way in the example is named: the console (Ruby), `hecks run` (Cli), the forms and
8
- # storehouse reader (Json) and `hecks mcp` (Mcp). An adapter left out would refuse the domain.
8
+ # storehouse reader (Json, served with the payment webhook by `examples/pizzas/config.ru`) and
9
+ # `hecks mcp` (Mcp). An adapter left out would refuse the domain.
9
10
  driven_by "Ruby"
10
11
  driven_by "Cli"
11
12
  driven_by "Json"
12
13
  driven_by "Mcp"
13
14
 
14
- # The primary port: an outside adapter (a payment webhook) translates an external fact into
15
- # our vocabulary; business rules stay on Purchase, reached via OnPizzaPaymentReceived.
15
+ # The primary port: an outside adapter (the payment webhook in
16
+ # `examples/pizzas/adapters/payment_webhook.rb`) translates an external fact into our
17
+ # vocabulary; business rules stay on Purchase, reached via OnPizzaPaymentReceived.
16
18
  Pizzas::Order.port "PaymentGateway" do
17
19
  operation "Receive" do
18
20
  attribute :customer_name, CustomerName
@@ -0,0 +1,22 @@
1
+ # The pizza shop on the web: both driving adapters in front of one booted Pizzas.
2
+ #
3
+ # PAYMENT_WEBHOOK_SECRET=whsec_local bundle exec rackup examples/pizzas/config.ru -p 9292
4
+ #
5
+ # `/` is the forms app (the Json driving adapter `pizzas.hecksagon` admits): a page per command,
6
+ # so a chef creates a pizza and adds toppings and a customer buys one. `/webhooks/payments` is
7
+ # where the payment processor reports a payment, told to the domain through
8
+ # `Order.PaymentGateway.Receive`. A development server: no sign-in and no CSRF protection.
9
+ require "hecks"
10
+ require "hecks/forms"
11
+ require_relative "adapters/payment_webhook"
12
+
13
+ runtime = Hecks.boot(__dir__)
14
+ Hecks::Forms.configure("PizzaShop") { expose "Pizzas" }
15
+
16
+ map "/webhooks/payments" do
17
+ run PizzaShop::PaymentWebhook.new(runtime: runtime, secret: ENV.fetch("PAYMENT_WEBHOOK_SECRET"))
18
+ end
19
+
20
+ map "/" do
21
+ run Hecks::Forms::App.for(registry: runtime.registry, app_name: "PizzaShop")
22
+ end
data/exe/hecks CHANGED
@@ -63,7 +63,7 @@ if reason
63
63
  if MEMORY_COMMANDS.include?(ARGV.first.to_s.chomp("!"))
64
64
  why = begin
65
65
  require "json"
66
- JSON.parse(text).dig("state", "refusal", "value")
66
+ JSON.parse(text).dig("state", "refusal")
67
67
  rescue JSON::ParserError
68
68
  nil
69
69
  end
@@ -4,8 +4,6 @@ require_relative "../../bluebook/expression/canonical_form"
4
4
 
5
5
  module Hecks
6
6
  module Adapters
7
- class NotExtractable < StandardError; end
8
-
9
7
  # Extracts a `given`/`ensures`/invariant block's own source text via
10
8
  # `prism`, canonicalised into readable, comparable text (era diffing, docs).
11
9
  module Prism
@@ -16,7 +16,6 @@ require_relative "driven/local_storage"
16
16
  require_relative "driven/prism"
17
17
  require_relative "driven/folder"
18
18
  require_relative "driven/d1"
19
- require_relative "driven/mock_stripe_adapter"
20
19
  require_relative "driven/tenant_provisioner"
21
20
  require_relative "driven/secure_random_identity"
22
21
  require_relative "driven/in_process_key_vault"
@@ -25,6 +24,5 @@ require_relative "driven/system_clock"
25
24
  # `identity_generation` adapter would make that port ambiguous for every boot.
26
25
  require_relative "driven/governance_authorization"
27
26
  require_relative "driven/identity_registry"
28
- require_relative "driven/google_authentication"
29
27
  require_relative "driven/claude_code"
30
28
  require_relative "driven/script_runner"
@@ -81,28 +81,21 @@ module Hecks
81
81
  # other value is a leaf
82
82
  # @return [Hash{Symbol => Object}, Array, Object] a new structure of the same shape
83
83
  # with Symbol keys; a leaf is returned as it came
84
- def deep_symbolize(value)
85
- case value
86
- when Hash then value.to_h { |k, v| [k.to_sym, deep_symbolize(v)] }
87
- when Array then value.map { |item| deep_symbolize(item) }
88
- else value
89
- end
90
- end
84
+ def deep_symbolize(value) = Runtime::Value.deep_symbolize(value)
91
85
 
92
- # Unwraps a record, a query row, or any value holding `Runtime::Value`s into plain
93
- # Hashes, Arrays and scalars a JSON encoder can walk.
94
- #
95
- # Delegates to `Runtime::Value.materialize`; the only addition is `#to_h` on a
96
- # `Handle`, which `materialize` does not recognize.
86
+ # Unwraps a record, a query row, or any value holding `Runtime::Value`s into the plain
87
+ # data a caller is answered with. A single-field value object answers as its bare
88
+ # field (`name: "Margherita"`), the shape a command takes it in; stored state stays
89
+ # wrapped.
97
90
  #
98
91
  # @param value [Adapters::Driving::Handle, Runtime::Value, Hash, Array, Object] what to
99
- # unwrap;
100
- # a `Handle` is read through its `to_h`, so its `:id` comes along
92
+ # unwrap; a `Handle` is read through its `to_h`, so its `:id` comes along
101
93
  # @return [Hash, Array, Object] the same data with every `Runtime::Value` replaced
102
- # by a Hash of its fields; any other value is returned as it came
94
+ # by its sole field's value, or by a Hash of its fields when it has more than one;
95
+ # any other value is returned as it came
103
96
  def materialize(value)
104
97
  value = value.to_h if value.is_a?(Handle)
105
- Runtime::Value.materialize(value)
98
+ Runtime::Value.materialize_unwrapped(value)
106
99
  end
107
100
 
108
101
  # Turns a command's JSON body, raw or already parsed, into the `to:`/`with:`
@@ -101,8 +101,8 @@ module Hecks
101
101
  ["TranslationAggregate", "move"] => :move_impl,
102
102
  ["TranslationAggregate", "convert"] => :convert_impl,
103
103
  ["TranslationAggregate", "retype"] => :retype_impl,
104
- ["TranslationAggregate", "compute"] => :compute_impl,
105
- ["TranslationAggregate", "rekey"] => :rekey_impl,
104
+ ["TranslationAggregate", "derive"] => :derive_impl,
105
+ ["TranslationAggregate", "rekey_to"] => :rekey_to_impl,
106
106
  ["TranslationAggregate", "backfill"] => :backfill_impl,
107
107
  ["TranslationAggregate", "unresolved"] => :unresolved_impl,
108
108
  ["Query", "limit"] => :limit_impl,
@@ -0,0 +1,58 @@
1
+ module Hecks
2
+ module Bluebook
3
+ module DSL
4
+ # The `derive` and `rekey_to` words of a `.translation` aggregate block: the two rules that
5
+ # compute from the record's own text and need no SQL.
6
+ module DerivationWords
7
+ # Declares a new field computed from the record's own text.
8
+ #
9
+ # The expression reads the held era's data, so it runs before any other rule and sees the
10
+ # fields exactly as they were stored. It changes nothing it reads: remove a source with
11
+ # `drop`. A record where the expression has no value (a field missing or not text) gets
12
+ # no new field.
13
+ #
14
+ # @param to [Symbol, String] the field's path in the destination era
15
+ # @param as [String] the expression, in `Bluebook::TranslationExpression`'s language
16
+ # @return [Array<Bluebook::TranslationDerive>] every derive declared so far, this one last
17
+ # @raise [Bluebook::DSL::Malformed] if `to` or `as` is empty, `to` is derived twice, or `as`
18
+ # is not a well-formed expression
19
+ def derive_impl(to, as:)
20
+ raise Malformed, "a derive needs a destination path" if to.to_s.empty?
21
+ raise Malformed, "a derive needs its as: expression" if as.to_s.empty?
22
+ raise Malformed, "#{to} is derived twice" if derived?(to)
23
+
24
+ @derives << TranslationDerive.new(to.to_s, checked_expression(as))
25
+ end
26
+
27
+ # Declares that the aggregate's identity is the value of a field a `derive` computes.
28
+ #
29
+ # The id is that value for every record where the derive has a value, and stays what it was
30
+ # for the rest.
31
+ #
32
+ # @param to [Symbol, String] the destination field of a `derive` declared earlier
33
+ # @return [Array<Bluebook::TranslationRekey>] every rekey declared so far, this one last
34
+ # @raise [Bluebook::DSL::Malformed] if `to` is empty, no earlier `derive` writes it, or the
35
+ # aggregate is already rekeyed
36
+ def rekey_to_impl(to)
37
+ raise Malformed, "a rekey_to needs the field to take the id from" if to.to_s.empty?
38
+ raise Malformed, "rekey_to #{to} needs a derive of #{to} declared before it" unless derived?(to)
39
+ raise Malformed, "an aggregate is rekeyed once" unless @rekeys.empty?
40
+
41
+ @rekeys << TranslationRekey.new(to.to_s)
42
+ end
43
+
44
+ private
45
+
46
+ def derived?(to) = @derives.any? { |derive| derive.to == to.to_s }
47
+
48
+ # The expression's text, once the language has accepted it.
49
+ def checked_expression(text)
50
+ TranslationExpression.parse(text)
51
+ text.to_s
52
+ rescue TranslationExpression::Refused => e
53
+ raise Malformed, "a derive's as: expression is refused: #{e.message}"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,3 +1,4 @@
1
+ require_relative "derivation_words"
1
2
  require_relative "word_gate"
2
3
  module Hecks
3
4
  module Bluebook
@@ -7,6 +8,7 @@ module Hecks
7
8
  class TranslationAggregateBuilder
8
9
  GRAMMAR_CONTEXT = "TranslationAggregate".freeze
9
10
 
11
+ include DerivationWords
10
12
  include WordGate
11
13
 
12
14
  # @param name [String, Symbol] the aggregate's name in the destination era
@@ -86,40 +88,6 @@ module Hecks
86
88
  @retypes << TranslationRetype.new(old_type.to_s, to.to_s)
87
89
  end
88
90
 
89
- # Declares a field computed by a hand-written Postgres SQL expression.
90
- #
91
- # The scaffold never proposes one; a human writes it, and human-sampled review is
92
- # its only verification.
93
- #
94
- # @param old_path [String, Symbol] the source field's path in the held era
95
- # @param to [String, Symbol] the field's path in the destination era
96
- # @param sql [String] the Postgres SQL expression computing the destination value
97
- # @return [Array<Bluebook::TranslationCompute>] every compute declared so far, this one
98
- # last
99
- # @raise [Bluebook::DSL::Malformed] if `old_path`, `to`, or `sql` is empty
100
- def compute_impl(old_path, to:, sql:)
101
- raise Malformed, "a compute needs a destination path (to:)" if to.to_s.empty?
102
- raise Malformed, "a compute needs a source path" if old_path.to_s.empty?
103
- raise Malformed, "a compute needs its sql: expression" if sql.to_s.empty?
104
-
105
- @computes << TranslationCompute.new(old_path.to_s, to.to_s, sql.to_s)
106
- end
107
-
108
- # Declares a hand-written Postgres SQL expression that recomputes the aggregate's own
109
- # identity.
110
- #
111
- # No path arguments, unlike the rules above: only the record's key is recomputed, not
112
- # state moved into it. Verified the same human-reviewed-only way as `compute`.
113
- #
114
- # @param sql [String] the Postgres SQL expression computing the destination identity
115
- # @return [Array<Bluebook::TranslationRekey>] every rekey declared so far, this one last
116
- # @raise [Bluebook::DSL::Malformed] if `sql` is empty
117
- def rekey_impl(sql:)
118
- raise Malformed, "a rekey needs its sql: expression" if sql.to_s.empty?
119
-
120
- @rekeys << TranslationRekey.new(sql.to_s)
121
- end
122
-
123
91
  # Declares a newly added, required attribute and the default existing records read
124
92
  # until a real value is written.
125
93
  #
@@ -159,7 +127,8 @@ module Hecks
159
127
  def build
160
128
  TranslationAggregate.new(
161
129
  name: @name, was: @was, renames: @renames, moves: @moves, converts: @converts,
162
- drops: @drops, retypes: @retypes, computes: @computes, rekeys: @rekeys, backfills: @backfills
130
+ drops: @drops, retypes: @retypes, derives: @derives, rekeys: @rekeys,
131
+ backfills: @backfills
163
132
  )
164
133
  end
165
134
 
@@ -171,7 +140,7 @@ module Hecks
171
140
  @converts = []
172
141
  @drops = []
173
142
  @retypes = []
174
- @computes = []
143
+ @derives = []
175
144
  @rekeys = []
176
145
  @backfills = []
177
146
  end
@@ -182,7 +151,7 @@ module Hecks
182
151
  rendered = Array(candidates).map { |candidate| render_path(candidate) }
183
152
  if rendered.empty?
184
153
  "#{@name}'s translation leaves #{render_path(name)} unresolved (no candidate matched — " \
185
- "consider drop, or compute on Postgres) — replace unresolved with a real rule before booting."
154
+ "consider drop) — replace unresolved with a real rule before booting."
186
155
  else
187
156
  "#{@name}'s translation leaves #{render_path(name)} unresolved (candidates: #{rendered.join(", ")}) — " \
188
157
  "replace unresolved with a rename, move, convert, or drop before booting."
@@ -97,15 +97,6 @@ module Hecks
97
97
  "`value >= 100 && value <= 599` in place of `value.between?(100, 599)`"
98
98
  end
99
99
 
100
- # Every name a rule resolves at its root: the first segment of each `lookup` path, unique.
101
- def lookup_heads(ast)
102
- heads = []
103
- each_node(ast) do |node|
104
- heads << node["path"].first.to_s if node["op"] == "lookup" && node["path"].is_a?(::Array)
105
- end
106
- heads.uniq
107
- end
108
-
109
100
  # Visits `node` and every Hash nested inside it, depth-first.
110
101
  def each_node(node, &block)
111
102
  case node
@@ -64,7 +64,7 @@ module Hecks
64
64
  attr_reader :domain, :binds, :subscriptions, :attachments, :translates, :driving
65
65
 
66
66
  # The driving adapters `driven_by` can name.
67
- DRIVING_ADAPTERS = %w[Ruby Cli Json Mcp].freeze
67
+ DRIVING_ADAPTERS = %w[Ruby Cli Json Mcp Web].freeze
68
68
 
69
69
  # Every optional keyword and what it holds when the declaration omits it.
70
70
  FIELD_DEFAULTS = {
@@ -3,7 +3,7 @@ module Hecks
3
3
  module MetaValidator
4
4
  class TranslationJudge
5
5
  # Offers each rule collection of one translated aggregate: renames, moves, converts,
6
- # drops, retypes, computes, rekeys and backfills.
6
+ # drops, retypes, derives, rekeys and backfills.
7
7
  module Rules
8
8
  private
9
9
 
@@ -44,19 +44,16 @@ module Hecks
44
44
  end
45
45
  end
46
46
 
47
- def judge_computes(aggregate, name)
48
- Array(aggregate.computes).each do |compute|
49
- send_to("Translation::TranslationAggregate.AddCompute", name, to: name,
50
- with: { from: v(compute.from),
51
- to: v(compute.to),
52
- sql: v(compute.sql) })
47
+ def judge_derives(aggregate, name)
48
+ Array(aggregate.derives).each do |derive|
49
+ fields = { to: v(derive.to), expression: v(derive.expression) }
50
+ send_to("Translation::TranslationAggregate.AddDerive", name, to: name, with: fields)
53
51
  end
54
52
  end
55
53
 
56
54
  def judge_rekeys(aggregate, name)
57
- Array(aggregate.rekeys).each do |rekey|
58
- send_to("Translation::TranslationAggregate.AddRekey", name, to: name,
59
- with: { sql: v(rekey.sql) })
55
+ aggregate.rekeys.each do |rekey|
56
+ send_to("Translation::TranslationAggregate.AddRekeyTo", name, to: name, with: { to: v(rekey.to) })
60
57
  end
61
58
  end
62
59
 
@@ -8,6 +8,9 @@ module Hecks
8
8
  class TranslationJudge
9
9
  include Rules
10
10
 
11
+ # The rule lists an aggregate carries, in the order they are offered to the language.
12
+ RULE_KINDS = %i[renames moves converts drops retypes derives rekeys backfills].freeze
13
+
11
14
  attr_reader :refusals
12
15
 
13
16
  # @param translation [Bluebook::Translation] the built translation to judge
@@ -67,14 +70,7 @@ module Hecks
67
70
  name = aggregate.name
68
71
  declare_aggregate(translation, aggregate, name)
69
72
 
70
- judge_renames(aggregate, name)
71
- judge_moves(aggregate, name)
72
- judge_converts(aggregate, name)
73
- judge_drops(aggregate, name)
74
- judge_retypes(aggregate, name)
75
- judge_computes(aggregate, name)
76
- judge_rekeys(aggregate, name)
77
- judge_backfills(aggregate, name)
73
+ RULE_KINDS.each { |kind| send("judge_#{kind}", aggregate, name) }
78
74
  end
79
75
 
80
76
  def declare_aggregate(translation, aggregate, name)
@@ -203,17 +203,6 @@ module Hecks
203
203
  registry
204
204
  end
205
205
 
206
- # Whether `grammar_registry`'s memoized singleton has finished its
207
- # fixpoint judge and attached-chapter load.
208
- #
209
- # A cache keyed on "finished" instead of its own inputs already missed
210
- # data for a whole build window once.
211
- #
212
- # @return [Boolean]
213
- def self.grammar_registry_ready?
214
- @grammar_registry && @grammar_ready_for == @grammar_registry.object_id
215
- end
216
-
217
206
  # Loads every `ATTACHED_GRAMMAR_DIR` chapter file into `registry`,
218
207
  # judged the ordinary way since the fixpoint has already run.
219
208
  #