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
@@ -1,5 +1,7 @@
1
+ require_relative "../naming"
1
2
  require_relative "../projector"
2
3
  require_relative "../runtime/refusal_wording"
4
+ require_relative "../vocabulary"
3
5
  require_relative "vocabulary"
4
6
  require_relative "rust_vocabulary/enum_body"
5
7
  require_relative "rust_vocabulary/arguments"
@@ -33,12 +35,20 @@ module Hecks
33
35
  kind: :order }
34
36
  }.freeze
35
37
 
36
- RUST_KEYWORDS = %w[as async await break const continue crate dyn else enum extern false fn for if impl in let loop
37
- match mod move mut pub ref return self static struct super trait true type unsafe use where
38
- while abstract become box do final macro override priv typeof unsized virtual yield try].freeze
38
+ # Keywords Rust refuses as a raw identifier, so they cannot take the `r#` escape.
39
+ NOT_RAW = %w[crate self Self super].freeze
39
40
 
40
41
  module_function
41
42
 
43
+ # @param name [String] a field's name
44
+ # @return [String] the Rust field: raw-escaped when a keyword, with a trailing underscore
45
+ # when the keyword cannot be raw
46
+ def field_name(name)
47
+ return "#{name}_" if NOT_RAW.include?(name)
48
+
49
+ Hecks::Vocabulary.fetch("RustReservedWord").include?(name) ? "r##{name}" : name
50
+ end
51
+
42
52
  def call(bluebook:, options: {}) = render(bluebook)
43
53
 
44
54
  def render(bluebook)
@@ -121,14 +131,12 @@ module Hecks
121
131
  def accessor_name(field) = field == "refusal" ? "refusal_class" : field
122
132
 
123
133
  def variant_name(row, fields)
124
- name = fields.map { |field| pascal(row.fetch(field)) }.join
134
+ name = fields.map { |field| Naming.pascal(row.fetch(field)) }.join
125
135
  raise ArgumentError, "#{name.inspect} is not a Rust variant name" unless name.match?(/\A[A-Z][A-Za-z0-9]*\z/)
126
136
 
127
137
  name
128
138
  end
129
139
 
130
- def pascal(text) = text.to_s.split("_").map { |part| part[0].to_s.upcase + part[1..].to_s }.join
131
-
132
140
  # Only `\`, `"` and newlines need escaping in the text a vocabulary row holds.
133
141
  def rust_string(text)
134
142
  escaped = text.to_s.gsub("\\") { "\\\\" }.gsub('"') { "\\\"" }.gsub("\n") { "\\n" }
@@ -97,12 +97,6 @@ module Hecks
97
97
  # @return [Boolean] whether `value` reaches `ratio` on every background
98
98
  def reads?(value, against, ratio) = against.all? { |back| contrast(value, back) >= ratio }
99
99
 
100
- # @param background [String] a fill
101
- # @param light [String] a light foreground to try
102
- # @param dark [String] a dark foreground to try
103
- # @return [String] whichever reads better on `background`
104
- def foreground(background, light, dark) = contrast(light, background) >= contrast(dark, background) ? light : dark
105
-
106
100
  # @return [Float] the hue, in degrees, of a colour given by its channels
107
101
  def angle(red, green, blue, high, spread)
108
102
  degrees = if high == red then ((green - blue) / spread) % 6
@@ -23,9 +23,14 @@ export function hostUrl(options: HostOptions = {}): string {
23
23
  return (options.url ?? (process.env[EDITOR.hostEnv] || EDITOR.hostDefault)).replace(/\/+$/, "");
24
24
  }
25
25
 
26
- /** A client for the domain this editor edits. */
26
+ /**
27
+ * A client for the domain this editor edits. It reads and writes the stored shape (`bare: false`),
28
+ * since its forms walk every value object by the schema's own fields.
29
+ */
27
30
  export function hostClient(options: HostOptions = {}): HostClient {
28
- return new HostClient({ domain: options.domain ?? SCHEMA.domain, url: hostUrl(options), fetch: options.fetch, timeoutMs: EDITOR.timeoutMs });
31
+ return new HostClient({
32
+ domain: options.domain ?? SCHEMA.domain, url: hostUrl(options), fetch: options.fetch, timeoutMs: EDITOR.timeoutMs, bare: false,
33
+ });
29
34
  }
30
35
 
31
36
  /**
@@ -0,0 +1,99 @@
1
+ require_relative "../plain_english"
2
+
3
+ module Hecks
4
+ module Projections
5
+ module Statements
6
+ # The statements a holder's lifecycle and commands carry: where it starts, what moves it
7
+ # between states, who does each command, and every rule a command checks before and after.
8
+ # Rule text is the author's own; only the command's name is turned into words around it.
9
+ #
10
+ # Actions.rules(order) # => ["Adding a topping requires: at most 10 toppings.", ...]
11
+ module Actions
12
+ module_function
13
+
14
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity] the construct with a lifecycle
15
+ # @return [Array<String>] where it starts, then one sentence per move
16
+ def lifecycle(holder)
17
+ lifecycle = holder.respond_to?(:lifecycle) && holder.lifecycle
18
+ return [] unless lifecycle
19
+
20
+ start = "#{PlainEnglish.upper(PlainEnglish.a(PlainEnglish.noun(holder.hecks_name)))} " \
21
+ "starts out #{PlainEnglish.state(lifecycle.default)}."
22
+ [start, *moves(lifecycle).map { |command, move| move_sentence(holder, command, move) }]
23
+ end
24
+
25
+ # Each command's moves, as `[command, { from: [...], to: state }]`, merged across the
26
+ # transitions a command declares from more than one state.
27
+ def moves(lifecycle)
28
+ merged = lifecycle.transitions.each_with_object({}) do |(command, transition), found|
29
+ key = [command.to_s, transition.target]
30
+ found[key] ||= { from: [], to: transition.target }
31
+ found[key][:from] |= Array(transition.from)
32
+ end
33
+ merged.map { |(command, _target), move| [command, move] }
34
+ end
35
+
36
+ # "Purchasing an order moves it from available to sold."
37
+ def move_sentence(holder, command, move)
38
+ doing = PlainEnglish.doing(command, holder.hecks_name)
39
+ noun = PlainEnglish.noun(holder.hecks_name)
40
+ object = doing.match?(/\b#{Regexp.escape(noun)}\b/) ? "it" : "the #{noun}"
41
+ "#{PlainEnglish.upper(doing)} #{move_phrase(move, object)}."
42
+ end
43
+
44
+ # "moves it from available to sold", "keeps it failed", or "moves it to closed".
45
+ #
46
+ # @param move [Hash] `{ from: [...], to: state }`, as `moves` answers it
47
+ # @param object [String] what is moved, such as "it" or "the order"
48
+ # @return [String] the verb phrase
49
+ def move_phrase(move, object)
50
+ to = PlainEnglish.state(move[:to])
51
+ return "keeps #{object} #{to}" if move[:from] == [move[:to]]
52
+ return "moves #{object} to #{to}" if move[:from].empty?
53
+
54
+ "moves #{object} from #{PlainEnglish.states(move[:from])} to #{to}"
55
+ end
56
+
57
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity] the construct the commands are on
58
+ # @return [Array<String>] each command's role, its rules, and what it guarantees after
59
+ def rules(holder)
60
+ holder.commands.flat_map { |command| command_rules(holder, command) }
61
+ end
62
+
63
+ def command_rules(holder, command)
64
+ doing = PlainEnglish.doing(command.hecks_name, holder.hecks_name)
65
+ role = command.role && "#{PlainEnglish.upper(doing)} is done by the #{PlainEnglish.noun(command.role)}."
66
+ [role, *checks(holder, command).map { |rule| "#{PlainEnglish.upper(doing)} requires: #{rule}." },
67
+ *command.ensures.map { |rule| "After #{doing}: #{text(rule)}." }].compact
68
+ end
69
+
70
+ # What a command checks first: the state it starts from, then each `given` in the
71
+ # author's words. The state is left out when a move sentence already says it.
72
+ #
73
+ # @param said_by_move [Boolean] whether a move sentence states the starting state
74
+ # @return [Array<String>] the rules, without a closing period
75
+ def checks(holder, command, said_by_move: true)
76
+ [state_check(holder, command, said_by_move), *command.givens.map { |rule| text(rule) }].compact
77
+ end
78
+
79
+ # "the order is available", for a command declared `from:` a state.
80
+ def state_check(holder, command, said_by_move)
81
+ return nil unless command.from
82
+ return nil if said_by_move && moved_by?(holder, command)
83
+
84
+ "the #{PlainEnglish.noun(holder.hecks_name)} is #{PlainEnglish.states(Array(command.from))}"
85
+ end
86
+
87
+ def moved_by?(holder, command)
88
+ lifecycle = holder.respond_to?(:lifecycle) && holder.lifecycle
89
+ return false unless lifecycle
90
+
91
+ lifecycle.transitions.any? { |name, _transition| name.to_s == command.hecks_name }
92
+ end
93
+
94
+ # A rule's description as written, without a trailing period.
95
+ def text(rule) = rule.description.to_s.strip.delete_suffix(".")
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,86 @@
1
+ require_relative "../plain_english"
2
+
3
+ module Hecks
4
+ module Projections
5
+ module Statements
6
+ # What one thing holds or points at, said from its attribute's name, target and cardinality.
7
+ # A reference stored under its target's own name reads as belonging; one stored under a
8
+ # role ("source", "disputed_by") keeps the role, so two references to one target differ.
9
+ #
10
+ # Relationships.sentence(transfer, source) # => "Each transfer has one source account."
11
+ module Relationships
12
+ PREPOSITIONS = %w[by to for from with of on in at].freeze
13
+
14
+ # The built-in value types, which a list names by its field rather than its element.
15
+ PRIMITIVES = %w[String Integer Float TrueClass FalseClass Boolean].freeze
16
+
17
+ module_function
18
+
19
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity] the construct the attribute is on
20
+ # @param attribute [Bluebook::Attribute] one of its attributes
21
+ # @return [String, nil] the sentence, or nil for a plain field
22
+ def sentence(holder, attribute)
23
+ subject = "Each #{PlainEnglish.noun(holder.hecks_name)}"
24
+ return list_sentence(subject, attribute) if attribute.list?
25
+ return nil unless attribute.relationship
26
+
27
+ "#{subject} #{reference_phrase(attribute)}."
28
+ end
29
+
30
+ # "Each order holds a list of toppings." The element is named when the field is not
31
+ # simply its plural: "Each account holds its ledger, a list of ledger entries."
32
+ def list_sentence(subject, attribute)
33
+ field = PlainEnglish.noun(attribute.name)
34
+ element = element_noun(attribute)
35
+ return "#{subject} holds a list of #{field}." if element.nil? || field.end_with?(element)
36
+
37
+ "#{subject} holds its #{field}, a list of #{element}."
38
+ end
39
+
40
+ # The plural element noun, or nil for a list of plain values (text, numbers).
41
+ def element_noun(attribute)
42
+ type = attribute.type
43
+ name = type.respond_to?(:target_name) ? type.target_name : type.to_s
44
+ return nil if PRIMITIVES.include?(name) || name.include?("<")
45
+
46
+ PlainEnglish.plural(PlainEnglish.noun(name))
47
+ end
48
+
49
+ # The verb phrase after "Each <holder>".
50
+ def reference_phrase(attribute)
51
+ target_name = attribute.type.target_name
52
+ target = PlainEnglish.noun(target_name)
53
+ return own_name_phrase(attribute, target) if Naming.snake(target_name) == attribute.name.to_s
54
+
55
+ role = PlainEnglish.noun(attribute.name)
56
+ return prepositional_phrase(role, target, attribute.optional?) if PREPOSITIONS.include?(role.split.last)
57
+
58
+ role_phrase(role, target, attribute.optional?)
59
+ end
60
+
61
+ # "belongs to one customer", or "has one" for `has_one`; "can" when it is optional.
62
+ def own_name_phrase(attribute, target)
63
+ has = attribute.relationship == "has_one"
64
+ return "#{has ? "can have" : "can belong to"} one #{target}" if attribute.optional?
65
+
66
+ "#{has ? "has" : "belongs to"} one #{target}"
67
+ end
68
+
69
+ # "can be disputed by one customer", "reports to at most one employee".
70
+ def prepositional_phrase(role, target, optional)
71
+ if role.split.first.match?(/(ed|en|wn)\z/)
72
+ optional ? "can be #{role} one #{target}" : "is #{role} one #{target}"
73
+ else
74
+ optional ? "#{role} at most one #{target}" : "#{role} one #{target}"
75
+ end
76
+ end
77
+
78
+ # "has one source account"; a role that already ends in its target is not repeated.
79
+ def role_phrase(role, target, optional)
80
+ named = role.end_with?(target.split.last) ? role : "#{role} #{target}"
81
+ optional ? "can have one #{named}" : "has one #{named}"
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -1,11 +1,15 @@
1
1
  require_relative "../projector"
2
+ require_relative "statements/relationships"
3
+ require_relative "statements/actions"
2
4
 
3
5
  module Hecks
4
6
  module Projections
5
- # A domain's declared facts as flat plain-English sentences, one checkable statement per fact.
6
- # Never invents one: relationships read from shape, invariants use the author's description.
7
+ # A domain's declared facts as flat plain-English sentences, one checkable statement per fact:
8
+ # what each thing holds and points at, what is always true of it, how it moves between
9
+ # states, and what each of its commands checks. Never invents one: relationships read from
10
+ # shape and names, and every rule uses the author's own description.
7
11
  #
8
- # Pizzas.project(Projections::Statements) # => ["A Pizza has many toppings.", ...]
12
+ # Pizzas.project(Projections::Statements) # => ["Each order holds a list of toppings.", ...]
9
13
  module Statements
10
14
  extend Hecks::Projector::Target
11
15
 
@@ -18,8 +22,8 @@ module Hecks
18
22
  # @param bluebook [Bluebook::Chapter] the chapter being projected
19
23
  # @param options [Hash{Symbol => Object}] ignored; present to satisfy the
20
24
  # `Projector::Target` calling convention
21
- # @return [Array<String>] one sentence per attribute relationship and invariant,
22
- # in holder order
25
+ # @return [Array<String>] each holder's relationships, invariants, moves and command
26
+ # rules, in holder order
23
27
  def call(bluebook:, options: {})
24
28
  holders(bluebook).flat_map { |holder| statements_for(holder) }
25
29
  end
@@ -27,31 +31,18 @@ module Hecks
27
31
  def holders(bluebook) = bluebook.aggregates.flat_map { |aggregate| [aggregate, *aggregate.entities] }
28
32
 
29
33
  def statements_for(holder)
30
- attribute_statements(holder) + invariant_statements(holder)
34
+ attribute_statements(holder) + invariant_statements(holder) +
35
+ Actions.lifecycle(holder) + Actions.rules(holder)
31
36
  end
32
37
 
33
- # `has_many` and `list_of` read the same in English; `attribute.name` is the author's noun.
38
+ # @param holder [Bluebook::Aggregate, Bluebook::Entity] the construct being read
39
+ # @return [Array<String>] one sentence per list or reference attribute
34
40
  def attribute_statements(holder)
35
41
  holder.attributes.filter_map { |attribute| attribute_statement(holder, attribute) }
36
42
  end
37
43
 
38
- def attribute_statement(holder, attribute)
39
- subject = "#{article(holder.hecks_name)} #{holder.hecks_name}"
40
- return "#{subject} has many #{attribute.name}." if attribute.list?
41
-
42
- target = attribute.relationship && attribute.type.target_name
43
- target_phrase = target && "#{article(target).downcase} #{target}"
44
- case attribute.relationship
45
- when "has_one" then "#{subject} has #{target_phrase}."
46
- when "belongs_to" then "#{subject} belongs to #{target_phrase}."
47
- when "reference_to" then "#{subject} references #{target_phrase}."
48
- end
49
- end
50
-
51
- # Vowel-letter heuristic is safe: construct names are plain words, never initialisms.
52
- # @param word [String] a construct name to prefix
53
- # @return [String] `"An"` if `word` starts with a vowel letter, `"A"` otherwise
54
- def article(word) = word.to_s.match?(/\A[AEIOUaeiou]/) ? "An" : "A"
44
+ # @return [String, nil] the attribute's sentence, or nil for a plain field
45
+ def attribute_statement(holder, attribute) = Relationships.sentence(holder, attribute)
55
46
 
56
47
  # Invariants sit on the holder and on each nested value object; both are read.
57
48
  # @param holder [Bluebook::Aggregate, Bluebook::Entity] the construct whose
@@ -0,0 +1,112 @@
1
+ require "json"
2
+
3
+ module Hecks
4
+ module Projector
5
+ module Exporter
6
+ # The declared `.adapter` constructs as plain Hashes, in the shape
7
+ # `hecks-parse adapter <file>...` prints as JSON, so the two parsers can be compared byte
8
+ # for byte.
9
+ module Adapters
10
+ # Every adapter declared in a registry, in declaration order.
11
+ #
12
+ # @param registry [Runtime::Registry] the registry the `.adapter` files were loaded into
13
+ # @return [Array<Hash{String => Object}>] each adapter as `adapter_hash` builds it
14
+ def adapters(registry)
15
+ registry.adapters.values.map { |adapter| adapter_hash(adapter) }
16
+ end
17
+
18
+ # Exports one adapter with string keys in the fixed order name, port, fields, secrets,
19
+ # defaults.
20
+ #
21
+ # @param adapter [Bluebook::Hecksagon::Adapter] the adapter to export
22
+ # @return [Hash{String => Object}] `"name"` (String), `"port"` (String, nil when never
23
+ # declared), `"fields"`/`"secrets"` (`Array<String>`), `"defaults"` (field name to its
24
+ # JSON-native default, in first-declaration order)
25
+ def adapter_hash(adapter)
26
+ {
27
+ "name" => adapter.name,
28
+ "port" => adapter.port,
29
+ "fields" => (adapter.fields || []).map(&:to_s),
30
+ "secrets" => (adapter.secrets || []).map(&:to_s),
31
+ "defaults" => adapter.declared_defaults.to_h { |field, value| [field.to_s, value] }
32
+ }
33
+ end
34
+
35
+ # Loads each `.adapter` file into its own fresh registry and exports what it declared,
36
+ # in file order then declaration order.
37
+ #
38
+ # @param paths [Array<String>] `.adapter` file paths
39
+ # @return [Array<Hash{String => Object}>] `adapters` of each loaded registry, concatenated
40
+ def adapters_in_files(paths)
41
+ paths.flat_map { |path| adapters(load_adapter_file(path)) }
42
+ end
43
+
44
+ # The adapters of the given files as the pretty JSON `hecks-parse adapter` prints for them,
45
+ # byte for byte: the bundled json's `JSON.pretty_generate`, whose spacing of an empty
46
+ # collection `hecks-parse` reproduces.
47
+ #
48
+ # @param paths [Array<String>] `.adapter` file paths
49
+ # @return [String] the JSON `hecks-parse adapter` prints for the same files
50
+ def adapters_json(paths)
51
+ JSON.pretty_generate(adapters_in_files(paths))
52
+ end
53
+
54
+ # Directories that hold copies or build output, never a source `.adapter`; the Rust
55
+ # builder skips the same names (and any dot-directory).
56
+ SKIPPED_DIRS = %w[target vendor node_modules tmp .git].freeze
57
+
58
+ # Every `.adapter` file under the given directories, for the IR's `adapters` key. The
59
+ # Rust builder scans the same directories the same way.
60
+ #
61
+ # @param directories [Array<String>] directories to scan recursively
62
+ # @return [Array<String>] absolute paths, unique and in byte order, none beneath a
63
+ # skipped or hidden directory (relative to the directory scanned)
64
+ def adapter_files_under(directories)
65
+ directories.flat_map do |directory|
66
+ base = File.expand_path(directory)
67
+ Dir.glob(File.join(base, "**", "*.adapter")).reject do |path|
68
+ path.delete_prefix("#{base}/").split("/")[0...-1].any? { |part| SKIPPED_DIRS.include?(part) }
69
+ end
70
+ end.uniq.sort_by(&:b)
71
+ end
72
+
73
+ # The declarations of the adapters a domain binds, the IR's `adapters` key: one entry
74
+ # per distinct bound name that some `.adapter` file declares, in name byte order. Only
75
+ # declarations: never a world's setting values. A bound name nothing declares is
76
+ # omitted, for the host to refuse. Of two files declaring one name, the first path wins.
77
+ # A file is loaded only if it quotes a bound name, which cannot change the result.
78
+ #
79
+ # @param persistence [Hash{Symbol => Object}] `persistence`'s `:aggregates` with `:adapter`
80
+ # @param paths [Array<String>] candidate `.adapter` files, in precedence order
81
+ # @return [Array<Hash{String => Object}>] each as `adapter_hash` builds it
82
+ def persistence_adapters(persistence, paths)
83
+ wanted = bound_adapter_names(persistence)
84
+ declared = adapters_in_files(paths.select { |path| quotes_any?(File.read(path), wanted) })
85
+ declared.select { |adapter| wanted.include?(adapter["name"]) }.uniq { |a| a["name"] }
86
+ .sort_by { |adapter| adapter["name"].b }
87
+ end
88
+
89
+ # @param persistence [Hash{Symbol => Object}] `persistence`'s result
90
+ # @return [Array<String>] the distinct adapter names its aggregates bind
91
+ def bound_adapter_names(persistence) = persistence.fetch(:aggregates).map { |row| row.fetch(:adapter) }.uniq
92
+
93
+ # @param text [String] a file's text
94
+ # @param names [Array<String>] adapter names
95
+ # @return [Boolean] whether the text quotes any of the names
96
+ def quotes_any?(text, names) = names.any? { |name| text.include?("\"#{name}\"") }
97
+
98
+ # Loads one `.adapter` file into a fresh registry. The file's own `require "hecks/..."`
99
+ # line loads the implementing class and needs `lib/` on the load path, as it is under
100
+ # bundler.
101
+ #
102
+ # @param path [String] an `.adapter` file path
103
+ # @return [Runtime::Registry] a registry holding only that file's adapters
104
+ def load_adapter_file(path)
105
+ registry = Runtime::Registry.new
106
+ Hecks.with_registry(registry) { Kernel.load(path) }
107
+ registry
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -38,33 +38,15 @@ module Hecks
38
38
  JSON.pretty_generate(translations(registry))
39
39
  end
40
40
 
41
- # The digest-relevant shape `ApprovalDigest.edge_digest` hashes — declared rules only,
42
- # never the compiled SQL, so a compiler-output change can't invalidate an approval.
43
- #
44
- # @param translation [Bluebook::Translation] the translation to digest
45
- # @return [Hash{Symbol => Object}] `:domain` (String), `:from`/`:to` (the era
46
- # identifiers as declared), `:retired` (`Array<String>`), and `:aggregates`
47
- # (each `translation_aggregate`'s own Hash)
48
- def translation_hash(translation)
49
- {
50
- domain: translation.domain,
51
- from: translation.from,
52
- to: translation.to,
53
- retired: translation.retired,
54
- aggregates: translation.aggregates.map { |aggregate| translation_aggregate(aggregate) }
55
- }
56
- end
57
-
58
- # Digests one aggregate's own declared translation rules.
41
+ # One aggregate's declared translation rules as plain data.
59
42
  #
60
43
  # @param aggregate [Bluebook::TranslationAggregate] the aggregate's own
61
- # translation rules to digest
44
+ # translation rules
62
45
  # @return [Hash{Symbol => Object}] `:name` (String), `:was` (String, nil),
63
- # `:renames` (`Hash{String => String}`), `:moves`/`:converts`/`:retypes`/
64
- # `:computes`/`:rekeys`/`:backfills` (each an `Array<Hash>`), `:drops`
65
- # (`Array<String>`)
46
+ # `:renames` (`Hash{String => String}`), `:moves`/`:converts`/`:retypes`/`:backfills`
47
+ # (each an `Array<Hash>`), `:drops` (`Array<String>`)
66
48
  def translation_aggregate(aggregate)
67
- structural_rules(aggregate).merge(typed_rules(aggregate)).merge(computed_rules(aggregate))
49
+ structural_rules(aggregate).merge(typed_rules(aggregate)).merge(backfill_rules(aggregate))
68
50
  end
69
51
 
70
52
  # `translation_aggregate`'s fields, plus precompiled SQL from the same
@@ -78,7 +60,7 @@ module Hecks
78
60
  def compiled_translation_aggregate(aggregate)
79
61
  return translation_aggregate(aggregate) unless Ports::Persistence.plugin?(:era)
80
62
 
81
- translation_aggregate(aggregate).merge(
63
+ translation_aggregate(aggregate).merge(derived_rules(aggregate)).merge(
82
64
  compiled_state_expression: Translation::RuleCompiler.compile_rules(aggregate),
83
65
  compiled_id_expression: (if Translation::RuleCompiler.rekeyed?(aggregate)
84
66
  Translation::RuleCompiler.compile_id_expression(aggregate)
@@ -109,17 +91,18 @@ module Hecks
109
91
  }
110
92
  end
111
93
 
112
- # The computes, rekeys and backfills of an aggregate.
113
- #
114
- # `rekeys`/`backfills` are digest-relevant too — a rekey with no compute
115
- # would otherwise collide with any other, letting its SQL change silently
116
- # invalidate nothing.
117
- def computed_rules(aggregate)
118
- {
119
- computes: aggregate.computes.map { |compute| { from: compute.from, to: compute.to, sql: compute.sql } },
120
- rekeys: aggregate.rekeys.map { |rekey| { sql: rekey.sql } },
121
- backfills: aggregate.backfills.map { |backfill| { name: backfill.name.to_s, default: backfill.default } }
122
- }
94
+ # The derives and the `rekey_to` of an aggregate, in the export of compiled rules only. An
95
+ # aggregate with none adds no key.
96
+ def derived_rules(aggregate)
97
+ return {} if aggregate.derives.empty?
98
+
99
+ rules = { derives: aggregate.derives.map { |derive| { to: derive.to, expression: derive.expression } } }
100
+ aggregate.rekeys.empty? ? rules : rules.merge(rekey_to: aggregate.rekeys.first.to)
101
+ end
102
+
103
+ # The backfills of an aggregate.
104
+ def backfill_rules(aggregate)
105
+ { backfills: aggregate.backfills.map { |backfill| { name: backfill.name.to_s, default: backfill.default } } }
123
106
  end
124
107
  end
125
108
  end
@@ -0,0 +1,103 @@
1
+ require "json"
2
+
3
+ module Hecks
4
+ module Projector
5
+ module Exporter
6
+ # The declared `.world` constructs as plain Hashes, in the shape `hecks-parse world
7
+ # <file>...` prints as JSON, so the two parsers can be compared byte for byte. Only names are
8
+ # exported: a bind's setting values (connection strings, keys) never leave the file, and the
9
+ # default database is reported as a presence flag.
10
+ module Worlds
11
+ # Every world declared in a registry, one per domain, in first-declaration order.
12
+ #
13
+ # @param registry [Runtime::Registry] the registry the `.world` files were loaded into
14
+ # @return [Array<Hash{String => Object}>] each world as `world_hash` builds it
15
+ def worlds(registry)
16
+ registry.worlds.values.map { |world| world_hash(world) }
17
+ end
18
+
19
+ # Exports one world with string keys in the fixed order domain, realm, latest,
20
+ # default_adapter, default_database, bindings.
21
+ #
22
+ # @param world [Bluebook::World] the world to export
23
+ # @return [Hash{String => Object}] `"domain"` (String); `"realm"`, `"latest"` and
24
+ # `"default_adapter"` (String, nil when unset or empty); `"default_database"` (true
25
+ # only when a non-empty connection is declared, never the connection itself);
26
+ # `"bindings"` (see `world_bindings`)
27
+ def world_hash(world)
28
+ {
29
+ "domain" => world.domain,
30
+ "realm" => present_string(world.realm),
31
+ "latest" => present_string(world.latest),
32
+ "default_adapter" => present_string(world.default_adapter),
33
+ "default_database" => !present_string(world.default_database).nil?,
34
+ "bindings" => world_bindings(world)
35
+ }
36
+ end
37
+
38
+ # The adapter binds of a world, from the qualified `"verb:adapter"` settings entries only
39
+ # (the bare verb entry repeats the last of them), in the order they were declared.
40
+ #
41
+ # @param world [Bluebook::World] the world whose settings to read
42
+ # @return [Array<Hash{String => Object}>] each `"verb"` (as written), `"adapter"` and
43
+ # `"settings"` (the setting names, never their values, in declaration order)
44
+ def world_bindings(world)
45
+ world.settings.filter_map do |key, value|
46
+ next unless key.include?(":")
47
+
48
+ adapter = value[:adapter].to_s
49
+ { "verb" => key.delete_suffix(":#{adapter.downcase}"), "adapter" => adapter,
50
+ "settings" => value.keys.reject { |name| name == :adapter }.map(&:to_s) }
51
+ end
52
+ end
53
+
54
+ # Loads every given `.world` file, in order, into one fresh registry, so worlds of one
55
+ # domain merge as they do at boot, and exports what it holds.
56
+ #
57
+ # @param paths [Array<String>] `.world` file paths
58
+ # @return [Array<Hash{String => Object}>] `worlds` of that registry
59
+ def worlds_in_files(paths)
60
+ registry = Runtime::Registry.new
61
+ Hecks.with_registry(registry) { paths.each { |path| Kernel.load(path) } }
62
+ worlds(registry)
63
+ end
64
+
65
+ # The worlds of the given files as the pretty JSON `hecks-parse world` prints for them,
66
+ # byte for byte: the bundled json's `JSON.pretty_generate`.
67
+ #
68
+ # @param paths [Array<String>] `.world` file paths
69
+ # @return [String] the JSON `hecks-parse world` prints for the same files
70
+ def worlds_json(paths)
71
+ JSON.pretty_generate(worlds_in_files(paths))
72
+ end
73
+
74
+ # The IR's `world` key: what a domain's world says about persistence, names only. A missing
75
+ # file, or a file with no world for the domain, gives the empty default.
76
+ #
77
+ # @param path [String] the `.world` file the domain sits beside (it need not exist)
78
+ # @param domain [String] the target domain's name
79
+ # @return [Hash{String => Object}] `"default_adapter"` (String or nil), `"default_database"`
80
+ # (Boolean) and `"persistence"` (each `persisted_by` bind as `"adapter"` and the
81
+ # `"settings"` names)
82
+ def persistence_world(path, domain)
83
+ world = (File.file?(path) ? worlds_in_files([path]) : []).find { |entry| entry["domain"] == domain }
84
+ return { "default_adapter" => nil, "default_database" => false, "persistence" => [] } unless world
85
+
86
+ { "default_adapter" => world["default_adapter"], "default_database" => world["default_database"],
87
+ "persistence" => persisted_by_binds(world["bindings"]) }
88
+ end
89
+
90
+ # @param bindings [Array<Hash{String => Object}>] a world's `"bindings"`
91
+ # @return [Array<Hash{String => Object}>] the `persisted_by` ones, adapter and setting names
92
+ def persisted_by_binds(bindings)
93
+ bindings.select { |bind| bind["verb"] == "persisted_by" }
94
+ .map { |bind| { "adapter" => bind["adapter"], "settings" => bind["settings"] } }
95
+ end
96
+
97
+ # @param value [String, nil] a scalar a world may leave unset
98
+ # @return [String, nil] the value, or nil when it is nil or empty
99
+ def present_string(value) = value.nil? || value.empty? ? nil : value
100
+ end
101
+ end
102
+ end
103
+ end
@@ -1,7 +1,9 @@
1
1
  require "json"
2
2
  require_relative "../ports/persistence"
3
+ require_relative "exporter/adapters"
3
4
  require_relative "exporter/bindings"
4
5
  require_relative "exporter/translations"
6
+ require_relative "exporter/worlds"
5
7
 
6
8
  module Hecks
7
9
  module Projector
@@ -9,7 +11,7 @@ module Hecks
9
11
  # binding facts, and translation edges (declared and compiled).
10
12
  #
11
13
  # The capability bindings (`authorization`, `membership`, ...) are `Exporter::Bindings` and
12
- # the translation edges (`translations`, `translation_hash`, ...) are `Exporter::Translations`;
14
+ # the translation edges (`translations`, `translations_json`, ...) are `Exporter::Translations`;
13
15
  # both are reachable as methods of `Exporter` itself.
14
16
  module Exporter
15
17
  module_function
@@ -72,8 +74,10 @@ module Hecks
72
74
  { aggregates: aggregates }
73
75
  end
74
76
 
77
+ extend Adapters
75
78
  extend Bindings
76
79
  extend Translations
80
+ extend Worlds
77
81
  end
78
82
  end
79
83
  end