hecks 3.13.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (440) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/adapters/fake_processor.rb +34 -0
  3. data/examples/pizzas/adapters/payment_signature.rb +66 -0
  4. data/examples/pizzas/adapters/payment_webhook.rb +87 -0
  5. data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
  6. data/examples/pizzas/bluebook/pizzas.bluebook +4 -4
  7. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  8. data/examples/pizzas/config.ru +22 -0
  9. data/lib/hecks/adapters/driven/prism.rb +0 -2
  10. data/lib/hecks/adapters/driven.rb +0 -2
  11. data/lib/hecks/adapters/driving/json.rb +1 -7
  12. data/lib/hecks/bluebook/dsl/attribute_collector.rb +15 -5
  13. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  14. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  15. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  16. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  17. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  18. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  19. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  20. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  21. data/lib/hecks/bluebook/named_pattern.rb +33 -0
  22. data/lib/hecks/bluebook/translation.rb +18 -10
  23. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  24. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  25. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  26. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  27. data/lib/hecks/bluebook.rb +3 -0
  28. data/lib/hecks/chapters.rb +3 -3
  29. data/lib/hecks/cli/domain_stub.rb +1 -1
  30. data/lib/hecks/cli/interview_aggregate.rb +4 -4
  31. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  32. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  33. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  34. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  35. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  36. data/lib/hecks/composer/adapters/jev.rb +66 -0
  37. data/lib/hecks/composer/agent.rb +113 -0
  38. data/lib/hecks/composer/assistant.rb +110 -0
  39. data/lib/hecks/composer/behavior_run.rb +53 -0
  40. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  41. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  42. data/lib/hecks/composer/board.rb +65 -0
  43. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  44. data/lib/hecks/composer/board_edit/source.rb +84 -0
  45. data/lib/hecks/composer/board_edit.rb +144 -0
  46. data/lib/hecks/composer/check.rb +89 -0
  47. data/lib/hecks/composer/domains.rb +81 -0
  48. data/lib/hecks/composer/jobs.rb +134 -0
  49. data/lib/hecks/composer/layout.rb +37 -0
  50. data/lib/hecks/composer/live.rb +150 -0
  51. data/lib/hecks/composer/outline.rb +48 -0
  52. data/lib/hecks/composer/page.html +989 -0
  53. data/lib/hecks/composer/panels.rb +142 -0
  54. data/lib/hecks/composer/parse.rb +83 -0
  55. data/lib/hecks/composer/plain.rb +28 -0
  56. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  57. data/lib/hecks/composer/session.rb +141 -0
  58. data/lib/hecks/composer/toolbox.rb +166 -0
  59. data/lib/hecks/composer/web.rb +156 -0
  60. data/lib/hecks/composer/workbench.rb +51 -0
  61. data/lib/hecks/composer/workspace.rb +81 -0
  62. data/lib/hecks/composer.rb +17 -0
  63. data/lib/hecks/corpus/adapters.rb +22 -0
  64. data/lib/hecks/corpus/rust.rb +1 -1
  65. data/lib/hecks/corpus/worlds.rb +19 -0
  66. data/lib/hecks/corpus.rb +5 -0
  67. data/lib/hecks/deploy/bluebook/deploy.bluebook +34 -34
  68. data/lib/hecks/forms/app/commands.rb +2 -3
  69. data/lib/hecks/forms/app/queries.rb +2 -3
  70. data/lib/hecks/forms/app/records.rb +17 -5
  71. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  72. data/lib/hecks/forms/field_controls.rb +2 -2
  73. data/lib/hecks/forms/field_renderer.rb +1 -1
  74. data/lib/hecks/forms/field_shape.rb +36 -17
  75. data/lib/hecks/forms/index_renderer.rb +2 -1
  76. data/lib/hecks/forms/page.rb +1 -1
  77. data/lib/hecks/forms/params.rb +0 -13
  78. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  79. data/lib/hecks/forms/record_renderer.rb +11 -7
  80. data/lib/hecks/forms/rule_words.rb +100 -0
  81. data/lib/hecks/forms/value_object_shape.rb +2 -1
  82. data/lib/hecks/framework/bluebook/change.bluebook +2 -2
  83. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  84. data/lib/hecks/fuzzing/domain_generator/block_rendering.rb +1 -1
  85. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  86. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  87. data/lib/hecks/gate/stages.yml +25 -0
  88. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  89. data/lib/hecks/grammar/evolve.rb +0 -3
  90. data/lib/hecks/grammar/translation.bluebook +1 -1
  91. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  92. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  93. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  94. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  95. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  96. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  97. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  98. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  99. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  100. data/lib/hecks/hecks/codebase.behaviors +6 -0
  101. data/lib/hecks/hecks/codebase.bluebook +58 -37
  102. data/lib/hecks/hecks/composer.behaviors +68 -0
  103. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  104. data/lib/hecks/hecks/custodian.behaviors +0 -6
  105. data/lib/hecks/hecks/custodian.bluebook +15 -92
  106. data/lib/hecks/hecks/hecks.bluebook +2 -2
  107. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  108. data/lib/hecks/hecks/hecks.world +1 -1
  109. data/lib/hecks/hecks/rehearsal.bluebook +10 -16
  110. data/lib/hecks/hecks/tooling.bluebook +22 -22
  111. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  112. data/lib/hecks/kernel_capabilities.rb +1 -1
  113. data/lib/hecks/language/bluebook/aggregate.bluebook +1 -0
  114. data/lib/hecks/language/bluebook/command.bluebook +1 -0
  115. data/lib/hecks/language/bluebook/entity.bluebook +1 -0
  116. data/lib/hecks/language/bluebook/query.bluebook +1 -0
  117. data/lib/hecks/language/bluebook/shape.bluebook +8 -7
  118. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  119. data/lib/hecks/language/hecksagon/port_operation.bluebook +1 -0
  120. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  121. data/lib/hecks/ports/access_control.rb +2 -2
  122. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  123. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  124. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  125. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  126. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  127. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  128. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  129. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  130. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  131. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  132. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  133. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  134. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  135. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  136. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  137. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  138. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  139. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  140. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  141. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  142. data/lib/hecks/ports.rb +0 -1
  143. data/lib/hecks/projection_files.rb +3 -1
  144. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  145. data/lib/hecks/projections/glossary/entries.rb +10 -4
  146. data/lib/hecks/projections/glossary/groups.rb +42 -0
  147. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  148. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  149. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  150. data/lib/hecks/projections/glossary/html.rb +1 -2
  151. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  152. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  153. data/lib/hecks/projections/glossary/page.css +143 -153
  154. data/lib/hecks/projections/glossary/page.js +63 -3
  155. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  156. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  157. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  158. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  159. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  160. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  161. data/lib/hecks/projections/glossary.rb +2 -1
  162. data/lib/hecks/projections/plain_english.rb +97 -0
  163. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  164. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  165. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  166. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  167. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  168. data/lib/hecks/projections/statements/actions.rb +99 -0
  169. data/lib/hecks/projections/statements/relationships.rb +86 -0
  170. data/lib/hecks/projections/statements.rb +15 -24
  171. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  172. data/lib/hecks/projector/exporter/translations.rb +18 -35
  173. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  174. data/lib/hecks/projector/exporter.rb +5 -1
  175. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  176. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  177. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  178. data/lib/hecks/quality_control/quality_control.bluebook +51 -51
  179. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  180. data/lib/hecks/runtime/outbox.rb +2 -9
  181. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  182. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  183. data/lib/hecks/runtime/value.rb +13 -0
  184. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  185. data/lib/hecks/rust_build/wasm.rb +0 -12
  186. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  187. data/lib/hecks/rust_format.rb +77 -0
  188. data/lib/hecks/site/bluebook/site.bluebook +5 -5
  189. data/lib/hecks/sme/bluebook/sme.bluebook +12 -12
  190. data/lib/hecks/tickets/bluebook/tickets.bluebook +3 -3
  191. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  192. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  193. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  194. data/lib/hecks/tools/translation_audit.rb +8 -10
  195. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  196. data/lib/hecks/version.rb +1 -1
  197. data/lib/hecks/vocabulary.rb +2 -1
  198. data/lib/hecks.rb +3 -0
  199. data/prebuilt/{58208399bada10b0db99928c48e38789288fc58f7b71a72b21bfa386680e32ed.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
  200. data/prebuilt/{2b59737fbdbd5f44edda0fddce2611f45d76087b3a7c782d1f1a17ea58c77948.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
  201. data/prebuilt/verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json +1 -0
  202. data/rust/Cargo.toml +40 -0
  203. data/rust/build/Cargo.toml +36 -0
  204. data/rust/build/clippy.toml +4 -0
  205. data/rust/build/src/adapters_pass.rs +196 -0
  206. data/rust/build/src/binding_pass.rs +54 -13
  207. data/rust/build/src/build_artifact.rs +23 -5
  208. data/rust/build/src/cargo_sync.rs +69 -26
  209. data/rust/build/src/lineage_pass.rs +101 -87
  210. data/rust/build/src/main.rs +11 -2
  211. data/rust/build/src/pipeline.rs +80 -19
  212. data/rust/build/src/reserved_names.rs +6 -50
  213. data/rust/build/src/resolve.rs +82 -35
  214. data/rust/build/src/rustfmt.rs +100 -0
  215. data/rust/build/src/seams_pass.rs +85 -16
  216. data/rust/build/src/subprocess.rs +30 -6
  217. data/rust/build/src/tmp.rs +5 -2
  218. data/rust/build/src/translation_pass.rs +72 -22
  219. data/rust/build/src/world_pass.rs +109 -0
  220. data/rust/clippy.toml +4 -0
  221. data/rust/codegen/Cargo.toml +36 -0
  222. data/rust/codegen/clippy.toml +4 -0
  223. data/rust/codegen/src/asks.rs +40 -14
  224. data/rust/codegen/src/attr.rs +2 -2
  225. data/rust/codegen/src/bridging.rs +676 -117
  226. data/rust/codegen/src/commands.rs +979 -373
  227. data/rust/codegen/src/constraints.rs +88 -25
  228. data/rust/codegen/src/dependency_planning.rs +128 -34
  229. data/rust/codegen/src/domain_generator.rs +300 -170
  230. data/rust/codegen/src/exemplar.rs +87 -32
  231. data/rust/codegen/src/expr_emitter.rs +54 -6
  232. data/rust/codegen/src/fielded.rs +193 -41
  233. data/rust/codegen/src/hecks_naming.rs +124 -16
  234. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  235. data/rust/codegen/src/json.rs +80 -44
  236. data/rust/codegen/src/json_codec.rs +585 -149
  237. data/rust/codegen/src/literal.rs +104 -138
  238. data/rust/codegen/src/main.rs +145 -52
  239. data/rust/codegen/src/manifest.rs +76 -14
  240. data/rust/codegen/src/mutations.rs +1212 -239
  241. data/rust/codegen/src/naming.rs +111 -60
  242. data/rust/codegen/src/optional_pass.rs +90 -28
  243. data/rust/codegen/src/ports.rs +5 -4
  244. data/rust/codegen/src/prelude.rs +139 -49
  245. data/rust/codegen/src/queries.rs +601 -226
  246. data/rust/codegen/src/reactions.rs +268 -74
  247. data/rust/codegen/src/read_models.rs +606 -205
  248. data/rust/codegen/src/reference_specs.rs +16 -3
  249. data/rust/codegen/src/registry.rs +128 -79
  250. data/rust/codegen/src/reserved_names.rs +6 -50
  251. data/rust/codegen/src/shared.rs +71 -22
  252. data/rust/codegen/src/sidecars.rs +15 -5
  253. data/rust/codegen/src/skip_reason.rs +8 -2
  254. data/rust/codegen/src/types.rs +306 -108
  255. data/rust/host/Cargo.lock +1 -0
  256. data/rust/host/Cargo.toml +40 -4
  257. data/rust/host/HECKS_RELEASE +1 -1
  258. data/rust/host/clippy.toml +4 -0
  259. data/rust/host/src/api.rs +698 -218
  260. data/rust/host/src/auth.rs +491 -214
  261. data/rust/host/src/b64.rs +91 -0
  262. data/rust/host/src/bin/expr_harness.rs +6 -2
  263. data/rust/host/src/bin/lineage_harness.rs +47 -11
  264. data/rust/host/src/bin/mint_harness.rs +70 -37
  265. data/rust/host/src/bin/validate_harness.rs +16 -8
  266. data/rust/host/src/boot.rs +144 -68
  267. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  268. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  269. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  270. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  271. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  272. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  273. data/rust/host/src/checkout.rs +78 -32
  274. data/rust/host/src/clock.rs +8 -0
  275. data/rust/host/src/commerce_ir.rs +333 -95
  276. data/rust/host/src/crypto.rs +33 -0
  277. data/rust/host/src/dispatch.rs +990 -331
  278. data/rust/host/src/expr_json.rs +1021 -273
  279. data/rust/host/src/extension.rs +24 -4
  280. data/rust/host/src/field_hints.rs +11 -7
  281. data/rust/host/src/fuzz_support.rs +85 -13
  282. data/rust/host/src/host.rs +100 -0
  283. data/rust/host/src/http.rs +22 -0
  284. data/rust/host/src/ir.rs +326 -16
  285. data/rust/host/src/journal.rs +277 -176
  286. data/rust/host/src/lambda_client.rs +219 -53
  287. data/rust/host/src/lib.rs +16 -4
  288. data/rust/host/src/log.rs +62 -11
  289. data/rust/host/src/mint.rs +1153 -326
  290. data/rust/host/src/needs.rs +185 -44
  291. data/rust/host/src/payments/keystore.rs +197 -45
  292. data/rust/host/src/payments/roles.rs +191 -0
  293. data/rust/host/src/payments/tests.rs +1445 -275
  294. data/rust/host/src/payments.rs +409 -121
  295. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  296. data/rust/host/src/platform_signin/config.rs +11 -11
  297. data/rust/host/src/platform_signin/cookie.rs +2 -2
  298. data/rust/host/src/platform_signin/extension.rs +2 -1
  299. data/rust/host/src/platform_signin/handler.rs +9 -8
  300. data/rust/host/src/platform_signin/jti.rs +4 -2
  301. data/rust/host/src/platform_signin/jwks.rs +2 -1
  302. data/rust/host/src/platform_signin/jws.rs +2 -37
  303. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  304. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  305. data/rust/host/src/platform_signin/pages.rs +20 -20
  306. data/rust/host/src/platform_signin/session.rs +8 -1
  307. data/rust/host/src/platform_signin/settings.rs +6 -6
  308. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  309. data/rust/host/src/platform_signin/verify.rs +41 -41
  310. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  311. data/rust/host/src/presentation.rs +207 -63
  312. data/rust/host/src/presentation_write.rs +502 -187
  313. data/rust/host/src/query_step.rs +69 -18
  314. data/rust/host/src/rate_limit.rs +541 -142
  315. data/rust/host/src/reference_transform.rs +77 -25
  316. data/rust/host/src/reference_validate.rs +443 -104
  317. data/rust/host/src/resend.rs +190 -55
  318. data/rust/host/src/secrets.rs +116 -21
  319. data/rust/host/src/server.rs +483 -104
  320. data/rust/host/src/storage_shape.rs +88 -21
  321. data/rust/host/src/test_pg.rs +17 -7
  322. data/rust/host/src/ui_schema.rs +495 -207
  323. data/rust/host/src/wasm_runner.rs +21 -6
  324. data/rust/host/src/web/commerce.rs +101 -23
  325. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  326. data/rust/host/src/web/newsletter/template.rs +26 -7
  327. data/rust/host/src/web/newsletter/tests.rs +112 -23
  328. data/rust/host/src/web/newsletter.rs +209 -59
  329. data/rust/host/src/web/newsletter_send.rs +251 -79
  330. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  331. data/rust/host/src/web/registration_receipt.rs +100 -24
  332. data/rust/host/src/web/registrations.rs +1384 -332
  333. data/rust/host/src/web/resend_webhook.rs +138 -80
  334. data/rust/host/src/web.rs +2416 -508
  335. data/rust/lsp/Cargo.toml +36 -0
  336. data/rust/lsp/clippy.toml +4 -0
  337. data/rust/lsp/src/json.rs +29 -22
  338. data/rust/lsp/src/main.rs +81 -31
  339. data/rust/lsp/src/outline.rs +23 -7
  340. data/rust/lsp/src/rpc.rs +17 -4
  341. data/rust/parser/Cargo.toml +36 -0
  342. data/rust/parser/clippy.toml +4 -0
  343. data/rust/parser/src/build/closed_sets.rs +6 -1
  344. data/rust/parser/src/build/identity.rs +4 -3
  345. data/rust/parser/src/build/mod.rs +1 -0
  346. data/rust/parser/src/build/named_pattern.rs +26 -0
  347. data/rust/parser/src/build/naming.rs +2 -80
  348. data/rust/parser/src/build/pattern_subset.rs +6 -8
  349. data/rust/parser/src/build/query_derive.rs +3 -1
  350. data/rust/parser/src/build/query_inference.rs +14 -5
  351. data/rust/parser/src/build/read_model.rs +4 -1
  352. data/rust/parser/src/build/references.rs +4 -3
  353. data/rust/parser/src/canonical.rs +34 -15
  354. data/rust/parser/src/emit.rs +187 -105
  355. data/rust/parser/src/expr/ast_json.rs +134 -42
  356. data/rust/parser/src/expr/evaluator.rs +27 -9
  357. data/rust/parser/src/expr/mod.rs +54 -13
  358. data/rust/parser/src/expr/resolver.rs +299 -92
  359. data/rust/parser/src/ir.rs +35 -3
  360. data/rust/parser/src/keywords.rs +7168 -510
  361. data/rust/parser/src/lex.rs +11 -25
  362. data/rust/parser/src/lib.rs +1 -0
  363. data/rust/parser/src/main.rs +65 -6
  364. data/rust/parser/src/parse/adapter.rs +157 -0
  365. data/rust/parser/src/parse/aggregate.rs +19 -10
  366. data/rust/parser/src/parse/chapter.rs +16 -17
  367. data/rust/parser/src/parse/command.rs +35 -16
  368. data/rust/parser/src/parse/domain_port.rs +4 -7
  369. data/rust/parser/src/parse/entity.rs +21 -11
  370. data/rust/parser/src/parse/hecksagon.rs +10 -4
  371. data/rust/parser/src/parse/lifecycle.rs +9 -5
  372. data/rust/parser/src/parse/mod.rs +43 -91
  373. data/rust/parser/src/parse/needs.rs +21 -12
  374. data/rust/parser/src/parse/policy.rs +2 -2
  375. data/rust/parser/src/parse/process_manager.rs +9 -11
  376. data/rust/parser/src/parse/query.rs +8 -4
  377. data/rust/parser/src/parse/read_model.rs +57 -16
  378. data/rust/parser/src/parse/value_object.rs +7 -2
  379. data/rust/parser/src/parse/world.rs +322 -0
  380. data/rust/parser/src/ruby_value.rs +7 -6
  381. data/rust/src/exemplar/json.rs +4 -4
  382. data/rust/src/exemplar/mutations.rs +18 -0
  383. data/rust/src/exemplar/read_models.rs +1 -10
  384. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  385. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  386. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  387. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  388. data/rust/src/kernel/cli.rs +229 -112
  389. data/rust/src/kernel/dispatch.rs +323 -115
  390. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  391. data/rust/src/kernel/expr.rs +203 -86
  392. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  393. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  394. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  395. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  396. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  397. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  398. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  399. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  400. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  401. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  402. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  403. data/rust/src/kernel/expression_operators/text.rs +120 -27
  404. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  405. data/rust/src/kernel/json.rs +230 -148
  406. data/rust/src/kernel/mod.rs +46 -33
  407. data/rust/src/kernel/named_query.rs +63 -15
  408. data/rust/src/kernel/needs.rs +112 -25
  409. data/rust/src/kernel/orchestrate.rs +980 -581
  410. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  411. data/rust/src/kernel/pattern.rs +107 -34
  412. data/rust/src/kernel/query_comparators.rs +118 -41
  413. data/rust/src/kernel/query_ordering.rs +13 -5
  414. data/rust/src/kernel/read_model.rs +479 -181
  415. data/rust/src/kernel/reference_lookup.rs +122 -26
  416. data/rust/src/kernel/repository.rs +293 -66
  417. data/rust/src/kernel/routing.rs +58 -42
  418. data/rust/src/kernel/ruby_float.rs +139 -0
  419. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  420. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  421. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  422. data/rust/src/kernel/vocab/mod.rs +6 -6
  423. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  424. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  425. data/rust/src/lib.rs +50 -0
  426. data/rust/src/main.rs +4 -1
  427. data/rust/web/Cargo.toml +36 -0
  428. data/rust/web/clippy.toml +4 -0
  429. metadata +85 -15
  430. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  431. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  432. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  433. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  434. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  435. data/lib/hecks/ports/authentication.port +0 -6
  436. data/lib/hecks/ports/authentication.rb +0 -67
  437. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  438. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  439. data/prebuilt/verdicts-48318ca172edd21443b1ad02801c697fff100b746e828f09db987647641a5df2.json +0 -1
  440. data/rust/host/src/approval.rs +0 -652
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1a1eacc3bae3f940928e23269c95e0306f291ec5a2b6d62268e254f12f1902e
4
- data.tar.gz: e4168d9e617dbad8912a65dbe10be8a3a32a6a08b18fd84542a05fa9803aeaef
3
+ metadata.gz: acccf4e21574d64b971cfbfe6a34f08f0bc86fdb8c74a8c58a472042138f01f1
4
+ data.tar.gz: 582d4e043a4b0145c25f6dfa8e223c47da7f55e60205fbef6485f6b406a2ffe2
5
5
  SHA512:
6
- metadata.gz: a49c8992b247138d92232d00123dd73986be81040bb1356fc912f97905644bbb35a38af431a1d81475b9ce07c644ab8c2aab3370c3fabaf685c0f8a93a16bd70
7
- data.tar.gz: 48ccea615a818989da2a01babb5c9d344a5a7c94fbe2eea47301a1437f29aa671134011a626f317c128bcaf1aaa912a21f056e30c8b133643123df129aa7a470
6
+ metadata.gz: 01f4db2b60db4d05aa7682fb7abdab6206abae36f45031656b9461fa064ec61236301b731f0b64c58cdee4d26624fe8a9d61042c379ae34f3274d036b31644ad
7
+ data.tar.gz: 9d1c2b9990aa214173c76634eda0dbe2f32651e80fe27e54c204d1cf0877a25e00334fc2784700754cf502196548b8e3a59549974f68789e5e31dd8defb5c0be
@@ -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" }
@@ -14,7 +14,7 @@ Hecks.bluebook "Pizzas" do
14
14
  identified_by :name
15
15
 
16
16
  value_object "PizzaName" do
17
- attribute :value, String, pattern: '[^ \t\n\r]'
17
+ attribute :value, String, matches: :present
18
18
 
19
19
  invariant("a pizza is named") { !value.to_s.empty? }
20
20
  end
@@ -26,13 +26,13 @@ Hecks.bluebook "Pizzas" do
26
26
  end
27
27
 
28
28
  value_object "CustomerName" do
29
- attribute :value, String, pattern: '[^ \t\n\r]'
29
+ attribute :value, String, matches: :present
30
30
 
31
31
  invariant("a customer is named") { !value.to_s.empty? }
32
32
  end
33
33
 
34
34
  value_object "ToppingName" do
35
- attribute :value, String, pattern: '[^ \t\n\r]'
35
+ attribute :value, String, matches: :present
36
36
 
37
37
  invariant("a topping is named") { !value.to_s.empty? }
38
38
  # The pattern lets a form feed through; `.strip` is what sees past it.
@@ -46,7 +46,7 @@ Hecks.bluebook "Pizzas" do
46
46
  end
47
47
 
48
48
  value_object "Topping" do
49
- attribute :name, String, pattern: '[^ \t\n\r]'
49
+ attribute :name, String, matches: :present
50
50
  attribute :amount, Integer
51
51
  end
52
52
 
@@ -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
@@ -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,13 +81,7 @@ 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
86
  # Unwraps a record, a query row, or any value holding `Runtime::Value`s into plain
93
87
  # Hashes, Arrays and scalars a JSON encoder can walk.
@@ -17,7 +17,7 @@ module Hecks
17
17
  private_constant :UNSET
18
18
 
19
19
  ATTRIBUTE_FIELD_OPTIONS = %i[default optional pattern admits].freeze
20
- ATTRIBUTE_OPTIONS = [*ATTRIBUTE_FIELD_OPTIONS, :one_of].freeze
20
+ ATTRIBUTE_OPTIONS = [*ATTRIBUTE_FIELD_OPTIONS, :one_of, :matches].freeze
21
21
  private_constant :ATTRIBUTE_FIELD_OPTIONS, :ATTRIBUTE_OPTIONS
22
22
 
23
23
  # Returns the attributes declared so far, minting the accumulating list on first use.
@@ -34,12 +34,12 @@ module Hecks
34
34
  # Declares one field on the owning construct; the type is a bare constant (ADR 0025).
35
35
  #
36
36
  # attribute :op, String, admits: "Vocabulary::QueryComparator"
37
- # `admits:` names an already-declared closed set as qualified text, checked on read.
38
- # `one_of:` declares an inline closed set, meaningful only inside a `value_object`.
37
+ # `admits:` names a declared closed set as qualified text. `one_of:` declares an inline one.
39
38
  #
40
39
  # @param name [Symbol] the field's name
41
40
  # @param type [Module, ListOf, OneOf] a bare constant or a wrapper
42
- # @param options [Hash] `default:`, `optional:`, `pattern:`, `admits:` or `one_of:`
41
+ # @param options [Hash] `default:`, `optional:`, `pattern:`, `matches:` (a `NamedPattern`),
42
+ # `admits:` or `one_of:`
43
43
  # @return [void]
44
44
  # @raise [Malformed] on a duplicate name, a missing or quoted type, a disallowed pattern
45
45
  # @raise [ArgumentError] on any other keyword
@@ -86,7 +86,17 @@ module Hecks
86
86
  raise ArgumentError, "unknown keyword#{"s" if unknown.size > 1}: #{unknown.map(&:inspect).join(", ")}"
87
87
  end
88
88
 
89
- { default: nil, optional: false, pattern: nil, admits: nil, one_of: nil }.merge(options)
89
+ given = { default: nil, optional: false, pattern: nil, admits: nil, one_of: nil }.merge(options)
90
+ expand_matches(given)
91
+ end
92
+
93
+ # `matches: :name` stands for the pattern the language gives that name.
94
+ def expand_matches(given)
95
+ matches = given.delete(:matches)
96
+ return given unless matches
97
+ raise ArgumentError, "matches: and pattern: both state the pattern" if given[:pattern]
98
+
99
+ given.merge(pattern: NamedPattern.source(matches))
90
100
  end
91
101
 
92
102
  def typed_attribute(name, type, field_options)
@@ -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)