hecks 3.14.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 (419) 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/lib/hecks/adapters/driven/prism.rb +0 -2
  9. data/lib/hecks/adapters/driven.rb +0 -2
  10. data/lib/hecks/adapters/driving/json.rb +1 -7
  11. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  12. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  13. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  14. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  15. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  16. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  17. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  18. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  19. data/lib/hecks/bluebook/translation.rb +18 -10
  20. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  21. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  22. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  23. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  24. data/lib/hecks/bluebook.rb +2 -0
  25. data/lib/hecks/chapters.rb +3 -3
  26. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  27. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  28. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  29. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  30. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  31. data/lib/hecks/composer/adapters/jev.rb +66 -0
  32. data/lib/hecks/composer/agent.rb +113 -0
  33. data/lib/hecks/composer/assistant.rb +110 -0
  34. data/lib/hecks/composer/behavior_run.rb +53 -0
  35. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  36. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  37. data/lib/hecks/composer/board.rb +65 -0
  38. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  39. data/lib/hecks/composer/board_edit/source.rb +84 -0
  40. data/lib/hecks/composer/board_edit.rb +144 -0
  41. data/lib/hecks/composer/check.rb +89 -0
  42. data/lib/hecks/composer/domains.rb +81 -0
  43. data/lib/hecks/composer/jobs.rb +134 -0
  44. data/lib/hecks/composer/layout.rb +37 -0
  45. data/lib/hecks/composer/live.rb +150 -0
  46. data/lib/hecks/composer/outline.rb +48 -0
  47. data/lib/hecks/composer/page.html +989 -0
  48. data/lib/hecks/composer/panels.rb +142 -0
  49. data/lib/hecks/composer/parse.rb +83 -0
  50. data/lib/hecks/composer/plain.rb +28 -0
  51. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  52. data/lib/hecks/composer/session.rb +141 -0
  53. data/lib/hecks/composer/toolbox.rb +166 -0
  54. data/lib/hecks/composer/web.rb +156 -0
  55. data/lib/hecks/composer/workbench.rb +51 -0
  56. data/lib/hecks/composer/workspace.rb +81 -0
  57. data/lib/hecks/composer.rb +17 -0
  58. data/lib/hecks/corpus/adapters.rb +22 -0
  59. data/lib/hecks/corpus/rust.rb +1 -1
  60. data/lib/hecks/corpus/worlds.rb +19 -0
  61. data/lib/hecks/corpus.rb +5 -0
  62. data/lib/hecks/forms/app/commands.rb +2 -3
  63. data/lib/hecks/forms/app/queries.rb +2 -3
  64. data/lib/hecks/forms/app/records.rb +17 -5
  65. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  66. data/lib/hecks/forms/field_controls.rb +2 -2
  67. data/lib/hecks/forms/field_renderer.rb +1 -1
  68. data/lib/hecks/forms/field_shape.rb +36 -17
  69. data/lib/hecks/forms/index_renderer.rb +2 -1
  70. data/lib/hecks/forms/page.rb +1 -1
  71. data/lib/hecks/forms/params.rb +0 -13
  72. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  73. data/lib/hecks/forms/record_renderer.rb +11 -7
  74. data/lib/hecks/forms/rule_words.rb +100 -0
  75. data/lib/hecks/forms/value_object_shape.rb +2 -1
  76. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  77. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  78. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  79. data/lib/hecks/gate/stages.yml +25 -0
  80. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  81. data/lib/hecks/grammar/evolve.rb +0 -3
  82. data/lib/hecks/grammar/translation.bluebook +1 -1
  83. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  84. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  85. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  86. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  87. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  88. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  89. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  90. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  91. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  92. data/lib/hecks/hecks/codebase.behaviors +6 -0
  93. data/lib/hecks/hecks/codebase.bluebook +21 -0
  94. data/lib/hecks/hecks/composer.behaviors +68 -0
  95. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  96. data/lib/hecks/hecks/custodian.behaviors +0 -6
  97. data/lib/hecks/hecks/custodian.bluebook +3 -80
  98. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  99. data/lib/hecks/hecks/hecks.world +1 -1
  100. data/lib/hecks/hecks/rehearsal.bluebook +7 -13
  101. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  102. data/lib/hecks/kernel_capabilities.rb +1 -1
  103. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  104. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  105. data/lib/hecks/ports/access_control.rb +2 -2
  106. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  107. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  108. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  109. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  110. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  111. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  112. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  113. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  114. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  115. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  116. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  117. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  118. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  119. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  120. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  121. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  122. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  123. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  124. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  125. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  126. data/lib/hecks/ports.rb +0 -1
  127. data/lib/hecks/projection_files.rb +3 -1
  128. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  129. data/lib/hecks/projections/glossary/entries.rb +10 -4
  130. data/lib/hecks/projections/glossary/groups.rb +42 -0
  131. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  132. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  133. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  134. data/lib/hecks/projections/glossary/html.rb +1 -2
  135. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  136. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  137. data/lib/hecks/projections/glossary/page.css +143 -153
  138. data/lib/hecks/projections/glossary/page.js +63 -3
  139. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  140. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  141. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  142. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  143. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  144. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  145. data/lib/hecks/projections/glossary.rb +2 -1
  146. data/lib/hecks/projections/plain_english.rb +97 -0
  147. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  148. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  149. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  150. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  151. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  152. data/lib/hecks/projections/statements/actions.rb +99 -0
  153. data/lib/hecks/projections/statements/relationships.rb +86 -0
  154. data/lib/hecks/projections/statements.rb +15 -24
  155. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  156. data/lib/hecks/projector/exporter/translations.rb +18 -35
  157. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  158. data/lib/hecks/projector/exporter.rb +5 -1
  159. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  160. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  161. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  162. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  163. data/lib/hecks/runtime/outbox.rb +2 -9
  164. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  165. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  166. data/lib/hecks/runtime/value.rb +13 -0
  167. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  168. data/lib/hecks/rust_build/wasm.rb +0 -12
  169. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  170. data/lib/hecks/rust_format.rb +77 -0
  171. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  172. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  173. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  174. data/lib/hecks/tools/translation_audit.rb +8 -10
  175. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  176. data/lib/hecks/version.rb +1 -1
  177. data/lib/hecks/vocabulary.rb +2 -1
  178. data/lib/hecks.rb +3 -0
  179. data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
  180. data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
  181. data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json} +1 -1
  182. data/rust/Cargo.toml +40 -0
  183. data/rust/build/Cargo.toml +36 -0
  184. data/rust/build/clippy.toml +4 -0
  185. data/rust/build/src/adapters_pass.rs +196 -0
  186. data/rust/build/src/binding_pass.rs +54 -13
  187. data/rust/build/src/build_artifact.rs +23 -5
  188. data/rust/build/src/cargo_sync.rs +69 -26
  189. data/rust/build/src/lineage_pass.rs +101 -87
  190. data/rust/build/src/main.rs +11 -2
  191. data/rust/build/src/pipeline.rs +80 -19
  192. data/rust/build/src/reserved_names.rs +6 -50
  193. data/rust/build/src/resolve.rs +82 -35
  194. data/rust/build/src/rustfmt.rs +100 -0
  195. data/rust/build/src/seams_pass.rs +85 -16
  196. data/rust/build/src/subprocess.rs +30 -6
  197. data/rust/build/src/tmp.rs +5 -2
  198. data/rust/build/src/translation_pass.rs +72 -22
  199. data/rust/build/src/world_pass.rs +109 -0
  200. data/rust/clippy.toml +4 -0
  201. data/rust/codegen/Cargo.toml +36 -0
  202. data/rust/codegen/clippy.toml +4 -0
  203. data/rust/codegen/src/asks.rs +40 -14
  204. data/rust/codegen/src/attr.rs +2 -2
  205. data/rust/codegen/src/bridging.rs +676 -117
  206. data/rust/codegen/src/commands.rs +979 -373
  207. data/rust/codegen/src/constraints.rs +88 -25
  208. data/rust/codegen/src/dependency_planning.rs +128 -34
  209. data/rust/codegen/src/domain_generator.rs +300 -170
  210. data/rust/codegen/src/exemplar.rs +87 -32
  211. data/rust/codegen/src/expr_emitter.rs +54 -6
  212. data/rust/codegen/src/fielded.rs +193 -41
  213. data/rust/codegen/src/hecks_naming.rs +124 -16
  214. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  215. data/rust/codegen/src/json.rs +80 -44
  216. data/rust/codegen/src/json_codec.rs +585 -149
  217. data/rust/codegen/src/literal.rs +104 -138
  218. data/rust/codegen/src/main.rs +145 -52
  219. data/rust/codegen/src/manifest.rs +76 -14
  220. data/rust/codegen/src/mutations.rs +1212 -239
  221. data/rust/codegen/src/naming.rs +111 -60
  222. data/rust/codegen/src/optional_pass.rs +90 -28
  223. data/rust/codegen/src/ports.rs +5 -4
  224. data/rust/codegen/src/prelude.rs +139 -49
  225. data/rust/codegen/src/queries.rs +601 -226
  226. data/rust/codegen/src/reactions.rs +268 -74
  227. data/rust/codegen/src/read_models.rs +606 -205
  228. data/rust/codegen/src/reference_specs.rs +16 -3
  229. data/rust/codegen/src/registry.rs +128 -79
  230. data/rust/codegen/src/reserved_names.rs +6 -50
  231. data/rust/codegen/src/shared.rs +71 -22
  232. data/rust/codegen/src/sidecars.rs +15 -5
  233. data/rust/codegen/src/skip_reason.rs +8 -2
  234. data/rust/codegen/src/types.rs +306 -108
  235. data/rust/host/Cargo.lock +1 -0
  236. data/rust/host/Cargo.toml +40 -4
  237. data/rust/host/HECKS_RELEASE +1 -1
  238. data/rust/host/clippy.toml +4 -0
  239. data/rust/host/src/api.rs +698 -218
  240. data/rust/host/src/auth.rs +491 -214
  241. data/rust/host/src/b64.rs +91 -0
  242. data/rust/host/src/bin/expr_harness.rs +6 -2
  243. data/rust/host/src/bin/lineage_harness.rs +47 -11
  244. data/rust/host/src/bin/mint_harness.rs +70 -37
  245. data/rust/host/src/bin/validate_harness.rs +16 -8
  246. data/rust/host/src/boot.rs +144 -68
  247. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  248. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  249. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  250. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  251. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  252. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  253. data/rust/host/src/checkout.rs +78 -32
  254. data/rust/host/src/clock.rs +8 -0
  255. data/rust/host/src/commerce_ir.rs +333 -95
  256. data/rust/host/src/crypto.rs +33 -0
  257. data/rust/host/src/dispatch.rs +990 -331
  258. data/rust/host/src/expr_json.rs +1021 -273
  259. data/rust/host/src/extension.rs +24 -4
  260. data/rust/host/src/field_hints.rs +11 -7
  261. data/rust/host/src/fuzz_support.rs +85 -13
  262. data/rust/host/src/host.rs +100 -0
  263. data/rust/host/src/http.rs +22 -0
  264. data/rust/host/src/ir.rs +326 -16
  265. data/rust/host/src/journal.rs +277 -176
  266. data/rust/host/src/lambda_client.rs +219 -53
  267. data/rust/host/src/lib.rs +16 -4
  268. data/rust/host/src/log.rs +62 -11
  269. data/rust/host/src/mint.rs +1153 -326
  270. data/rust/host/src/needs.rs +185 -44
  271. data/rust/host/src/payments/keystore.rs +197 -45
  272. data/rust/host/src/payments/roles.rs +39 -7
  273. data/rust/host/src/payments/tests.rs +1427 -282
  274. data/rust/host/src/payments.rs +391 -112
  275. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  276. data/rust/host/src/platform_signin/config.rs +11 -11
  277. data/rust/host/src/platform_signin/cookie.rs +2 -2
  278. data/rust/host/src/platform_signin/extension.rs +2 -1
  279. data/rust/host/src/platform_signin/handler.rs +9 -8
  280. data/rust/host/src/platform_signin/jti.rs +4 -2
  281. data/rust/host/src/platform_signin/jwks.rs +2 -1
  282. data/rust/host/src/platform_signin/jws.rs +2 -37
  283. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  284. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  285. data/rust/host/src/platform_signin/pages.rs +20 -20
  286. data/rust/host/src/platform_signin/session.rs +8 -1
  287. data/rust/host/src/platform_signin/settings.rs +6 -6
  288. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  289. data/rust/host/src/platform_signin/verify.rs +41 -41
  290. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  291. data/rust/host/src/presentation.rs +207 -63
  292. data/rust/host/src/presentation_write.rs +502 -187
  293. data/rust/host/src/query_step.rs +69 -18
  294. data/rust/host/src/rate_limit.rs +541 -142
  295. data/rust/host/src/reference_transform.rs +77 -25
  296. data/rust/host/src/reference_validate.rs +443 -104
  297. data/rust/host/src/resend.rs +190 -55
  298. data/rust/host/src/secrets.rs +116 -21
  299. data/rust/host/src/server.rs +483 -104
  300. data/rust/host/src/storage_shape.rs +88 -21
  301. data/rust/host/src/test_pg.rs +17 -7
  302. data/rust/host/src/ui_schema.rs +495 -207
  303. data/rust/host/src/wasm_runner.rs +21 -6
  304. data/rust/host/src/web/commerce.rs +101 -23
  305. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  306. data/rust/host/src/web/newsletter/template.rs +26 -7
  307. data/rust/host/src/web/newsletter/tests.rs +112 -23
  308. data/rust/host/src/web/newsletter.rs +209 -59
  309. data/rust/host/src/web/newsletter_send.rs +251 -79
  310. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  311. data/rust/host/src/web/registration_receipt.rs +100 -24
  312. data/rust/host/src/web/registrations.rs +1384 -332
  313. data/rust/host/src/web/resend_webhook.rs +138 -80
  314. data/rust/host/src/web.rs +2416 -508
  315. data/rust/lsp/Cargo.toml +36 -0
  316. data/rust/lsp/clippy.toml +4 -0
  317. data/rust/lsp/src/json.rs +29 -22
  318. data/rust/lsp/src/main.rs +81 -31
  319. data/rust/lsp/src/outline.rs +23 -7
  320. data/rust/lsp/src/rpc.rs +17 -4
  321. data/rust/parser/Cargo.toml +36 -0
  322. data/rust/parser/clippy.toml +4 -0
  323. data/rust/parser/src/build/closed_sets.rs +6 -1
  324. data/rust/parser/src/build/identity.rs +4 -3
  325. data/rust/parser/src/build/mod.rs +1 -1
  326. data/rust/parser/src/build/named_pattern.rs +4 -1
  327. data/rust/parser/src/build/naming.rs +2 -80
  328. data/rust/parser/src/build/pattern_subset.rs +6 -8
  329. data/rust/parser/src/build/query_derive.rs +3 -1
  330. data/rust/parser/src/build/query_inference.rs +14 -5
  331. data/rust/parser/src/build/read_model.rs +4 -1
  332. data/rust/parser/src/build/references.rs +4 -3
  333. data/rust/parser/src/canonical.rs +34 -15
  334. data/rust/parser/src/emit.rs +187 -105
  335. data/rust/parser/src/expr/ast_json.rs +134 -42
  336. data/rust/parser/src/expr/evaluator.rs +27 -9
  337. data/rust/parser/src/expr/mod.rs +54 -13
  338. data/rust/parser/src/expr/resolver.rs +299 -92
  339. data/rust/parser/src/ir.rs +35 -3
  340. data/rust/parser/src/keywords.rs +7168 -516
  341. data/rust/parser/src/lex.rs +11 -25
  342. data/rust/parser/src/lib.rs +1 -0
  343. data/rust/parser/src/main.rs +65 -6
  344. data/rust/parser/src/parse/adapter.rs +157 -0
  345. data/rust/parser/src/parse/aggregate.rs +19 -10
  346. data/rust/parser/src/parse/chapter.rs +16 -17
  347. data/rust/parser/src/parse/command.rs +35 -16
  348. data/rust/parser/src/parse/domain_port.rs +4 -7
  349. data/rust/parser/src/parse/entity.rs +21 -11
  350. data/rust/parser/src/parse/hecksagon.rs +10 -4
  351. data/rust/parser/src/parse/lifecycle.rs +9 -5
  352. data/rust/parser/src/parse/mod.rs +22 -91
  353. data/rust/parser/src/parse/needs.rs +21 -12
  354. data/rust/parser/src/parse/policy.rs +2 -2
  355. data/rust/parser/src/parse/process_manager.rs +9 -11
  356. data/rust/parser/src/parse/query.rs +8 -4
  357. data/rust/parser/src/parse/read_model.rs +57 -16
  358. data/rust/parser/src/parse/value_object.rs +7 -2
  359. data/rust/parser/src/parse/world.rs +322 -0
  360. data/rust/parser/src/ruby_value.rs +7 -6
  361. data/rust/src/exemplar/json.rs +4 -4
  362. data/rust/src/exemplar/mutations.rs +18 -0
  363. data/rust/src/exemplar/read_models.rs +1 -10
  364. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  365. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  366. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  367. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  368. data/rust/src/kernel/cli.rs +229 -112
  369. data/rust/src/kernel/dispatch.rs +323 -115
  370. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  371. data/rust/src/kernel/expr.rs +203 -86
  372. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  373. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  374. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  375. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  376. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  377. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  378. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  379. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  380. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  381. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  382. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  383. data/rust/src/kernel/expression_operators/text.rs +120 -27
  384. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  385. data/rust/src/kernel/json.rs +230 -148
  386. data/rust/src/kernel/mod.rs +46 -33
  387. data/rust/src/kernel/named_query.rs +63 -15
  388. data/rust/src/kernel/needs.rs +112 -25
  389. data/rust/src/kernel/orchestrate.rs +980 -581
  390. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  391. data/rust/src/kernel/pattern.rs +107 -34
  392. data/rust/src/kernel/query_comparators.rs +118 -41
  393. data/rust/src/kernel/query_ordering.rs +13 -5
  394. data/rust/src/kernel/read_model.rs +479 -181
  395. data/rust/src/kernel/reference_lookup.rs +122 -26
  396. data/rust/src/kernel/repository.rs +293 -66
  397. data/rust/src/kernel/routing.rs +58 -42
  398. data/rust/src/kernel/ruby_float.rs +139 -0
  399. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  400. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  401. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  402. data/rust/src/kernel/vocab/mod.rs +6 -6
  403. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  404. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  405. data/rust/src/lib.rs +50 -0
  406. data/rust/src/main.rs +4 -1
  407. data/rust/web/Cargo.toml +36 -0
  408. data/rust/web/clippy.toml +4 -0
  409. metadata +82 -15
  410. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  411. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  412. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  413. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  414. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  415. data/lib/hecks/ports/authentication.port +0 -6
  416. data/lib/hecks/ports/authentication.rb +0 -67
  417. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  418. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  419. data/rust/host/src/approval.rs +0 -652
@@ -9,7 +9,7 @@ module Hecks
9
9
  # Checks one aggregate's records across the edge for id conservation and, where a
10
10
  # per-record comparison is possible, agreement with the reference transform.
11
11
  #
12
- # Compute paths and rekeyed aggregates are exempt from the comparison.
12
+ # Rekeyed aggregates are exempt from the comparison.
13
13
  #
14
14
  # @param violations [Array<String>] collector this method appends messages to
15
15
  # @param aggregate [Bluebook::Aggregate] the current era's IR for the aggregate
@@ -24,7 +24,7 @@ module Hecks
24
24
  check_id_conservation!(violations, aggregate, before, after, rekeyed: rekeyed)
25
25
 
26
26
  return unless declared
27
- # A rekey's SQL is its only implementation and `after` cannot be looked up by old id.
27
+ # A rekeyed record's `after` cannot be looked up by its old id.
28
28
  return if rekeyed
29
29
 
30
30
  check_value_preservation!(violations, aggregate, declared, before, after)
@@ -56,12 +56,11 @@ module Hecks
56
56
  # @return [void]
57
57
  def check_value_preservation!(violations, aggregate, declared, before, after)
58
58
  rules = Ports::Persistence::Lineage.from_declared(declared, aggregate.name)
59
- compute_paths = compute_paths_of(declared)
60
59
 
61
60
  before.each do |id, state|
62
61
  next unless after.key?(id)
63
62
 
64
- diverged = divergence(rules, id, state, after[id], compute_paths)
63
+ diverged = divergence(rules, id, state, after[id])
65
64
  next if diverged.empty?
66
65
 
67
66
  violations << "#{aggregate.name}##{id}: the translated state diverges from the reference " \
@@ -75,28 +74,6 @@ module Hecks
75
74
  # @return [Hash{String => Object}, nil] a deep copy with String keys at every depth
76
75
  def normalize(state) = JSON.parse(JSON.generate(state))
77
76
 
78
- # Removes the paths a compute owns from a normalized state, in place.
79
- #
80
- # A dotted path removes only its own member, never the whole top-level key, so sibling
81
- # members stay under the equivalence check and silent loss there is still caught.
82
- #
83
- # @param state [Hash{String => Object}] a normalized state; mutated
84
- # @param paths [Array<String>] bare (`"price_cents"`) or dotted (`"price.cents"`)
85
- # compute paths
86
- # @return [Hash{String => Object}] `state` itself, with those paths deleted
87
- def strip_compute_paths(state, paths)
88
- paths.each do |path|
89
- segments = path.split(".")
90
- if segments.length == 1
91
- state.delete(segments.first)
92
- else
93
- parent = segments[0..-2].reduce(state) { |node, segment| node.is_a?(Hash) ? node[segment] : nil }
94
- parent.delete(segments.last) if parent.is_a?(Hash)
95
- end
96
- end
97
- state
98
- end
99
-
100
77
  private
101
78
 
102
79
  # The message for a rekeying edge whose record count changed, or nil.
@@ -118,15 +95,11 @@ module Hecks
118
95
  "(lost #{lost.sort.inspect}, gained #{gained.sort.inspect})"
119
96
  end
120
97
 
121
- def compute_paths_of(declared)
122
- declared.computes.flat_map { |compute| [compute.from, compute.to] }.map(&:to_s)
123
- end
124
-
125
98
  # The keys at which one record's translated state differs from the reference transform.
126
- def divergence(rules, id, state, translated, compute_paths)
99
+ def divergence(rules, id, state, translated)
127
100
  entry = Ports::Persistence::Entry.new(operation: "save", id: id, state: state.transform_keys(&:to_sym))
128
- expected = strip_compute_paths(normalize(rules.translate(entry).state), compute_paths)
129
- actual = strip_compute_paths(normalize(translated), compute_paths)
101
+ expected = normalize(rules.translate(entry).state)
102
+ actual = normalize(translated)
130
103
  (expected.keys | actual.keys).reject { |key| expected[key] == actual[key] }
131
104
  end
132
105
  end
@@ -8,7 +8,7 @@ module Hecks
8
8
  #
9
9
  # @param aggregate [Bluebook::Aggregate] the current era's IR for the aggregate
10
10
  # @param declared [Bluebook::TranslationAggregate, nil] this edge's rules; the
11
- # destinations of its renames, moves, converts and computes count as fed
11
+ # destinations of its renames, moves, converts and derives count as fed
12
12
  # @param after [Hash{String => Hash}] translated state per record id
13
13
  # @return [Array<String>] names of unfed attributes, in declaration order; `[]` when
14
14
  # every attribute is fed or `after` holds no records
@@ -25,12 +25,12 @@ module Hecks
25
25
  # The top-level attribute names an edge's destinations feed.
26
26
  #
27
27
  # @param declared [Bluebook::TranslationAggregate, nil] this edge's rules
28
- # @return [Array<String>] the destinations of renames, moves, converts and computes, cut to
28
+ # @return [Array<String>] the destinations of renames, moves, converts and derives, cut to
29
29
  # their top-level name; `[]` when there is no edge
30
30
  def fed_names(declared)
31
31
  return [] unless declared
32
32
 
33
- rules = [declared.moves, declared.converts, declared.computes].flat_map { |list| list.map(&:to) }
33
+ rules = [declared.moves, declared.converts, declared.derives].flat_map { |list| list.map(&:to) }
34
34
  (declared.renames.values.map(&:to_s) + rules).map { |path| path.to_s.split(".").first }
35
35
  end
36
36
 
@@ -3,12 +3,11 @@ require "json"
3
3
  require_relative "audit/layer_one"
4
4
  require_relative "audit/layer_two"
5
5
  require_relative "audit/unfed_report"
6
- require_relative "audit/approval_digest"
7
6
 
8
7
  module Hecks
9
8
  module Translation
10
- # Derives the audit's assertions per layer (audit/layer_one, layer_two, unfed_report,
11
- # approval_digest); Layer 3, the human-approved sample, is assembled in `check`.
9
+ # Derives the audit's assertions per layer (audit/layer_one, layer_two, unfed_report);
10
+ # Layer 3, the human-approved sample, is assembled in `check`.
12
11
  module Audit
13
12
  Verdict = Struct.new(:violations, :dropped, :unfed, :samples, keyword_init: true) do
14
13
  # Reports whether the mechanical layers found nothing to refuse.
@@ -23,7 +22,6 @@ module Hecks
23
22
  extend LayerOne
24
23
  extend LayerTwo
25
24
  extend UnfedReport
26
- extend ApprovalDigest
27
25
 
28
26
  module_function
29
27
 
@@ -0,0 +1,52 @@
1
+ module Hecks
2
+ module Translation
3
+ # Compiles a `Bluebook::TranslationExpression` tree into a Postgres text expression over a
4
+ # jsonb record. The compiled form yields NULL wherever the expression has no value, and
5
+ # agrees with `TranslationExpression.evaluate` on every record.
6
+ module ExpressionSql
7
+ UPPER = ("A".."Z").to_a.join.freeze
8
+ LOWER = UPPER.downcase.freeze
9
+
10
+ module_function
11
+
12
+ # @param tree [Array] a tree `Bluebook::TranslationExpression.parse` returned
13
+ # @param record [String] the SQL that names the jsonb record the expression reads
14
+ # @return [String] a SQL expression of type text
15
+ def compile(tree, record)
16
+ case tree.first
17
+ when "lit" then text(tree[1])
18
+ when "null" then "NULL::text"
19
+ when "ref" then reference(tree[1], record)
20
+ else function(tree.first, tree.drop(1), record)
21
+ end
22
+ end
23
+
24
+ def function(name, arguments, record)
25
+ return lookup(arguments, record) if name == "lookup"
26
+
27
+ parts = arguments.map { |argument| compile(argument, record) }
28
+ case name
29
+ when "lower" then "translate(#{parts.first}, #{text(UPPER)}, #{text(LOWER)})"
30
+ when "upper" then "translate(#{parts.first}, #{text(LOWER)}, #{text(UPPER)})"
31
+ when "concat" then "(#{parts.join(" || ")})"
32
+ else "replace(#{parts.join(", ")})"
33
+ end
34
+ end
35
+
36
+ # Only text counts as a value: a number, object, array or null reads as no value.
37
+ def reference(path, record)
38
+ at = RuleCompiler.path_literal(path)
39
+ "(CASE WHEN jsonb_typeof(#{record} #> #{at}) = 'string' THEN #{record} #>> #{at} END)"
40
+ end
41
+
42
+ def lookup(arguments, record)
43
+ key, table, fallback = arguments
44
+ branches = table.map { |from, to| "WHEN #{text(from)} THEN #{text(to)}" }.join(" ")
45
+ "(SELECT CASE WHEN __v IS NULL THEN NULL ELSE CASE __v #{branches} ELSE #{compile(fallback, record)} END END " \
46
+ "FROM (SELECT #{compile(key, record)} AS __v) __lookup)"
47
+ end
48
+
49
+ def text(value) = "#{RuleCompiler.text_literal(value)}::text"
50
+ end
51
+ end
52
+ end
@@ -1,4 +1,5 @@
1
1
  require "json"
2
+ require_relative "expression_sql"
2
3
 
3
4
  module Hecks
4
5
  module Translation
@@ -7,19 +8,34 @@ module Hecks
7
8
  module RuleCompiler
8
9
  module_function
9
10
 
10
- # Compiles one edge's rename/move/convert/drop/compute/backfill rules into a nested
11
+ # Compiles one edge's derive/rename/move/convert/drop/backfill rules into a nested
11
12
  # SQL expression over `state`, composed innermost-first in that phase order.
12
13
  def compile_rules(declared)
13
- expression = compile_renames("state", declared.renames)
14
+ expression = compile_derives("state", declared.derives)
15
+ expression = compile_renames(expression, declared.renames)
14
16
  expression = compile_moves(expression, declared.moves)
15
17
  expression = compile_converts(expression, declared.converts)
16
18
  expression = compile_drops(expression, declared.drops)
17
- expression = compile_computes(expression, declared.computes)
18
19
  # Backfilled last, same order as `Lineage#translate`'s in-process pass, so the
19
20
  # boot-time mint audit (which reads only this compiled SQL) sees the same result.
20
21
  compile_backfills(expression, declared.backfills)
21
22
  end
22
23
 
24
+ # Wraps `expression` in each derive, every one reading the record as it was stored.
25
+ def compile_derives(expression, derives)
26
+ derives.inject(expression) { |sql, derive| compile_derive(sql, derive) }
27
+ end
28
+
29
+ # Wraps `expression` so a derive's text lands at its destination when the expression has a
30
+ # value; the expression reads the stored `state`, never the rules composed before it.
31
+ def compile_derive(expression, derive)
32
+ value = ExpressionSql.compile(derive.tree, "state")
33
+ "(SELECT CASE WHEN __d IS NULL THEN __s " \
34
+ "ELSE hecks_tr_insert(__s, #{path_literal(derive.to)}, to_jsonb(__d), " \
35
+ "#{text_literal("derive #{derive.to}")}) END " \
36
+ "FROM (SELECT (#{expression}) AS __s) __outer, LATERAL (SELECT #{value} AS __d) __derived)"
37
+ end
38
+
23
39
  # Folds each rule of one kind into `expression`, innermost-first.
24
40
  def compile_renames(expression, renames)
25
41
  renames.inject(expression) { |sql, (old_name, new_name)| compile_rename(sql, old_name, new_name) }
@@ -31,8 +47,6 @@ module Hecks
31
47
 
32
48
  def compile_drops(expression, drops) = drops.inject(expression) { |sql, name| compile_drop(sql, name) }
33
49
 
34
- def compile_computes(expression, computes) = computes.inject(expression) { |sql, compute| compile_compute(sql, compute) }
35
-
36
50
  def compile_backfills(expression, backfills)
37
51
  backfills.inject(expression) { |sql, backfill| compile_backfill(sql, backfill) }
38
52
  end
@@ -71,25 +85,12 @@ module Hecks
71
85
  "CASE WHEN #{guard} THEN #{compile_id_expression(declared)} ELSE aggregate_id END AS aggregate_id"
72
86
  end
73
87
 
74
- # Evaluates a rekey's own SQL against the record's current `state`, read directly
75
- # rather than through `compile_rules`'s progressively-built expression chain.
88
+ # A `rekey_to` id: the value of the derive it names, read from the record's stored `state`,
89
+ # or the old id for a record where the derive has no value.
76
90
  def compile_id_expression(declared)
77
91
  rekey = declared.rekeys.first
78
- "(SELECT (#{rekey.sql}) FROM (SELECT (state) AS __s) __outer)"
79
- end
80
-
81
- # Wraps `expression` so a compute's SQL result lands at its declared destination
82
- # when the source path is present; the whole record stays readable as `__s`.
83
- def compile_compute(expression, compute)
84
- from = compute.from.to_s
85
- to = compute.to.to_s
86
- "(SELECT CASE WHEN (__x).present THEN " \
87
- "hecks_tr_insert((__x).remaining, #{path_literal(to)}, to_jsonb((#{compute.sql})), " \
88
- "#{text_literal("compute #{from} to: #{to}")}) " \
89
- "ELSE __s END " \
90
- "FROM (SELECT (#{expression}) AS __s) __outer, " \
91
- "LATERAL (SELECT hecks_tr_extract(__s, #{path_literal(from)}) AS __x) __extract, " \
92
- "LATERAL (SELECT (__s #>> #{path_literal(from)}) AS #{quote(from)}) __fields)"
92
+ derive = declared.derives.find { |candidate| candidate.to == rekey.to }
93
+ "COALESCE(#{ExpressionSql.compile(derive.tree, "state")}, aggregate_id)"
93
94
  end
94
95
 
95
96
  # Wraps `expression` so a backfill's default lands at its declared path only when
@@ -103,13 +104,6 @@ module Hecks
103
104
  "FROM (SELECT (#{expression}) AS __s) __outer)"
104
105
  end
105
106
 
106
- # Quotes a Postgres identifier; requires "pg" lazily so a non-Postgres-bound
107
- # domain never gains a hard dependency on the gem just by loading this file.
108
- def quote(name)
109
- require "pg"
110
- PG::Connection.quote_ident(name.to_s)
111
- end
112
-
113
107
  # Renders a Ruby value as a single-quoted SQL text literal, escaping embedded quotes.
114
108
  def text_literal(text) = "'#{text.to_s.gsub("'", "''")}'"
115
109
 
@@ -5,7 +5,7 @@ require_relative "scaffold/writer"
5
5
  module Hecks
6
6
  module Translation
7
7
  # Diffs the held era's storage shape against the current one and writes the edge file.
8
- # Ambiguities become parse-refusing `unresolved` constructs; it never proposes compute or drop.
8
+ # Ambiguities become parse-refusing `unresolved` constructs; it never proposes drop or derive.
9
9
  module Scaffold
10
10
  Edge = Struct.new(:domain, :from, :to, :ordinal, :label, :aggregates, :retired, keyword_init: true)
11
11
  ScaffoldedAggregate = Struct.new(:name, :was, :rules, keyword_init: true)
@@ -9,4 +9,3 @@ require_relative "translation/rule_compiler"
9
9
  require_relative "translation/scaffold"
10
10
  require_relative "translation/audit"
11
11
  require_relative "translation/reattest"
12
- require_relative "translation/approval_file"
@@ -18,20 +18,17 @@ module Hecks
18
18
 
19
19
  # Registers this plugin's era gates on one boot's gate list.
20
20
  #
21
- # Two `:pre_verify` gates: `era_compute_rules` always runs; `era_check` (ADR 0031)
22
- # runs only when the registry binds something lineage-capable.
21
+ # One `:pre_verify` gate, `era_check` (ADR 0031), registered only when the registry
22
+ # binds something lineage-capable.
23
23
  #
24
24
  # @param registry [Runtime::Registry] the registry being booted, asked whether any
25
25
  # bluebook's first aggregate is bound to a lineage-capable adapter
26
26
  # @param gates [Runtime::BootGates] this boot's gate list, registered onto in place
27
27
  # @return [Runtime::BootGates, nil] `gates` when `:era_check` was registered; nil when
28
- # the registry binds nothing lineage-capable, so only `:era_compute_rules` was added
28
+ # the registry binds nothing lineage-capable
29
29
  # @raise [Runtime::WiringError] if an aggregate's persistence binding is missing,
30
30
  # ambiguous, or carries an unsupported role
31
31
  def contribute_boot_gates(registry, gates)
32
- gates.register(:era_compute_rules, lambda { |reg, _dir|
33
- Runtime::EraCheck.check_compute_rules_for_registry!(reg)
34
- }, phase: :pre_verify)
35
32
  gates.register(:era_check, Runtime::EraCheck.method(:check_lineage!), phase: :pre_verify) if
36
33
  Runtime::EraCheck.lineage_capable_registry?(registry)
37
34
  end
data/lib/hecks/ports.rb CHANGED
@@ -17,7 +17,6 @@ require_relative "ports/identity_generation"
17
17
  require_relative "ports/key_vault"
18
18
  require_relative "ports/authorization"
19
19
  require_relative "ports/identity_resolution"
20
- require_relative "ports/authentication"
21
20
  require_relative "ports/access_control"
22
21
  require_relative "ports/identity_assignment"
23
22
  require_relative "ports/agent"
@@ -3,6 +3,7 @@
3
3
  require "fileutils"
4
4
  require "json"
5
5
  require "tempfile"
6
+ require_relative "rust_format"
6
7
  require_relative "projection_files/texts"
7
8
  require_relative "projection_files/expression_tables"
8
9
 
@@ -55,7 +56,8 @@ module Hecks
55
56
  def build(name, root: ROOT)
56
57
  raise ArgumentError, "no projection named #{name.inspect}" unless NAMES.include?(name.to_sym)
57
58
 
58
- public_send(name, root)
59
+ result = public_send(name, root)
60
+ Result.new(RustFormat.files(result.content), result.stale)
59
61
  end
60
62
 
61
63
  # Writes one projection, each file through a temporary file and a rename so a reader never sees
@@ -1,6 +1,6 @@
1
1
  #
2
2
  # @@web_logical_id@@ is the ONE exception -- real Google OAuth token
3
- # exchange (hecks's own GoogleAuthentication adapter posting to
3
+ # exchange (the Rust web host's Google sign-in posting to
4
4
  # https://oauth2.googleapis.com/token) is a genuine third-party HTTPS
5
5
  # call with no AWS PrivateLink/VPC Endpoint option (unlike
6
6
  # lambda:InvokeFunction, above), and @@web_logical_id@@ is ALSO
@@ -77,20 +77,26 @@ module Hecks
77
77
  end
78
78
 
79
79
  def verb_entries(bluebook, homes)
80
- holders(bluebook).flat_map { |holder| command_entries(holder, homes) + query_entries(holder, homes) }
80
+ shapes = bluebook.aggregates.to_h { |aggregate| [aggregate.hecks_name, aggregate.value_objects] }
81
+ holders(bluebook).flat_map do |holder|
82
+ command_entries(holder, homes) + query_entries(holder, homes, shapes[homes[holder.hecks_name]])
83
+ end
81
84
  end
82
85
 
83
86
  def command_entries(holder, homes)
84
87
  holder.commands.map do |command|
85
88
  Entry.new(name: command.hecks_name, kind: :command, within: holder.hecks_name,
86
- section: homes[holder.hecks_name], facts: { command: command, holder: holder })
89
+ section: homes[holder.hecks_name],
90
+ facts: { command: command, holder: holder, within: homes[holder.hecks_name] })
87
91
  end
88
92
  end
89
93
 
90
- def query_entries(holder, homes)
94
+ # A query carries its holder and the aggregate's value objects, to speak its fields.
95
+ def query_entries(holder, homes, value_objects)
91
96
  holder.queries.map do |query|
92
97
  Entry.new(name: query.hecks_name, kind: :query, within: holder.hecks_name,
93
- section: homes[holder.hecks_name], facts: { query: query })
98
+ section: homes[holder.hecks_name],
99
+ facts: { query: query, holder: holder, value_objects: Array(value_objects) })
94
100
  end
95
101
  end
96
102
 
@@ -0,0 +1,42 @@
1
+ module Hecks
2
+ module Projections
3
+ module Glossary
4
+ # The groups an aggregate's terms are listed under, so a reader meets what can be done
5
+ # with a thing before the lists and the parts it is made of. Each group is a `###`
6
+ # heading in the Markdown, so GitHub and the page give the terms the same slugs.
7
+ module Groups
8
+ # Kind => the group's heading, in the order the groups are shown.
9
+ LABELS = {
10
+ command: "Actions", event: "What it records", policy: "What happens on its own",
11
+ saga: "What happens on its own", query: "Lists", entity: "What it is made of",
12
+ value_object: "What it is made of"
13
+ }.freeze
14
+
15
+ ORDER = LABELS.values.uniq.freeze
16
+
17
+ module_function
18
+
19
+ # @param entry [Glossary::Entry] a term
20
+ # @return [Integer] where its group sits; terms outside every group come first
21
+ def rank(entry) = ORDER.index(LABELS[entry.kind]) || -1
22
+
23
+ # A section's terms with a group heading before the first term of each group. Only an
24
+ # aggregate's section is grouped.
25
+ #
26
+ # @param section [Glossary::Section] the section, its terms already sorted
27
+ # @return [Array<Array(Symbol, Object)>] `[:group, label]` and `[:term, entry]` pairs
28
+ def walk(section)
29
+ return section.terms.map { |entry| [:term, entry] } unless section.aggregate
30
+
31
+ last = nil
32
+ section.terms.flat_map do |entry|
33
+ label = LABELS[entry.kind]
34
+ heading = label == last ? [] : [[:group, label]]
35
+ last = label
36
+ heading + [[:term, entry]]
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -41,6 +41,7 @@ module Hecks
41
41
  <p class="sub">Glossary</p>
42
42
  <input type="search" placeholder="Find a term" aria-label="Find a term" autocomplete="off">
43
43
  <p class="count"></p>
44
+ <p class="progress" hidden></p>
44
45
  <ol>
45
46
  @@ITEMS@@
46
47
  </ol>
@@ -69,7 +70,7 @@ module Hecks
69
70
  end
70
71
 
71
72
  def rail(title, sections, slugs)
72
- domain = title.split(" — ").first
73
+ domain = Page.domain(title)
73
74
  items = sections.map do |section|
74
75
  heading = section[:heading]
75
76
  %(<li><a href="##{slugs[heading]}">#{Html.inline(heading.text)}</a></li>)
@@ -77,13 +78,17 @@ module Hecks
77
78
  fill(RAIL, "DOMAIN" => Html.escape(domain), "ITEMS" => items.join("\n"))
78
79
  end
79
80
 
81
+ # @param title [String] the document's title, such as "Pizzas glossary"
82
+ # @return [String] the domain's name alone
83
+ def domain(title) = title.delete_suffix(" glossary")
84
+
80
85
  def front_matter(blocks)
81
86
  ["<header>", *blocks.filter_map { |block| front_part(block) }, "</header>"].join("\n")
82
87
  end
83
88
 
84
89
  def front_part(block)
85
90
  case block.type
86
- when :heading then "<h1>#{Html.inline(block.text.split(" — ").first)}</h1>"
91
+ when :heading then "<h1>#{Html.inline(Page.domain(block.text))}</h1>"
87
92
  when :quote then "<p class=\"vision\">#{Html.inline(block.text)}</p>"
88
93
  when :paragraph then "<p class=\"lede\">#{Html.inline(block.text)}</p>"
89
94
  when :mermaid then Html.figure(block, "How it all fits together")
@@ -25,7 +25,7 @@ module Hecks
25
25
 
26
26
  case line
27
27
  when /\A```mermaid/ then fence(lines, index)
28
- when /\A(\#{1,3}) (.+)\z/ then [heading(Regexp.last_match), index + 1]
28
+ when /\A(\#{1,4}) (.+)\z/ then [heading(Regexp.last_match), index + 1]
29
29
  when /\A> / then [Block.new(type: :quote, text: line[2..]), index + 1]
30
30
  when /\A- / then list(lines, index)
31
31
  else paragraph(lines, index)
@@ -4,6 +4,11 @@ module Hecks
4
4
  module Html
5
5
  # Groups parsed blocks into the page's sections and renders each section's terms.
6
6
  module Sections
7
+ # Paragraph openings that mark a rule, and the class that sets each apart on the page.
8
+ LEADS = {
9
+ "Always true:" => "rule", "Allowed only when:" => "rule", "Afterwards, always:" => "rule"
10
+ }.freeze
11
+
7
12
  module_function
8
13
 
9
14
  # GitHub's slugs for these headings, so links the Markdown carries land here too.
@@ -26,14 +31,14 @@ module Hecks
26
31
  sections
27
32
  end
28
33
 
29
- # Each `###` opens an article running to the next. A bold-only paragraph
30
- # captions whatever figure or list follows it.
34
+ # Each `###` opens a group of terms and each `####` a term running to the next. A
35
+ # bold-only paragraph captions whatever figure or list follows it.
31
36
  def section_html(section, slugs)
32
37
  heading = section[:heading]
33
38
  state = { parts: [%(<section id="#{slugs[heading]}">), "<h2>#{Html.inline(heading.text)}</h2>"],
34
- caption: nil, in_terms: false }
39
+ caption: nil, open: nil }
35
40
  section[:blocks].drop(1).each { |block| section_block(block, state, slugs) }
36
- state[:parts] << "</article></div>" if state[:in_terms]
41
+ close(state, nil)
37
42
  state[:parts] << "</section>"
38
43
  state[:parts].join("\n")
39
44
  end
@@ -41,7 +46,7 @@ module Hecks
41
46
  # Renders one block into `state[:parts]`, mutating `state`.
42
47
  def section_block(block, state, slugs)
43
48
  case block.type
44
- when :heading then open_term(block, state, slugs)
49
+ when :heading then block.level == 3 ? open_group(block, state, slugs) : open_term(block, state, slugs)
45
50
  when :mermaid then state[:parts] << Html.figure(block, state.delete(:caption))
46
51
  when :list then list(block, state)
47
52
  when :quote then state[:parts] << "<p class=\"about\">#{Html.inline(block.text)}</p>"
@@ -49,11 +54,26 @@ module Hecks
49
54
  end
50
55
  end
51
56
 
57
+ def open_group(block, state, slugs)
58
+ close(state, nil)
59
+ state[:parts] << %(<h3 class="group" id="#{slugs[block]}">#{Html.inline(block.text)}</h3>)
60
+ end
61
+
52
62
  def open_term(block, state, slugs)
53
- parts = state[:parts]
54
- parts << (state[:in_terms] ? "</article>" : "<div class=\"terms\">")
55
- parts << %(<article class="term" id="#{slugs[block]}">) << "<h3>#{Html.inline(block.text)}</h3>"
56
- state[:in_terms] = true
63
+ close(state, :terms)
64
+ state[:parts] << "<div class=\"terms\">" unless state[:open]
65
+ state[:parts] << %(<article class="term" id="#{slugs[block]}">) << "<h4>#{Html.inline(block.text)}</h4>"
66
+ state[:open] = :article
67
+ end
68
+
69
+ # Closes the open article, and the terms around it unless `keep` is `:terms`.
70
+ def close(state, keep)
71
+ state[:parts] << "</article>" if state[:open] == :article
72
+ state[:open] = :terms if state[:open]
73
+ return if keep == :terms
74
+
75
+ state[:parts] << "</div>" if state[:open]
76
+ state[:open] = nil
57
77
  end
58
78
 
59
79
  def list(block, state)
@@ -67,8 +87,8 @@ module Hecks
67
87
  if text.match?(/\A\*\*[^*]+\*\*\z/)
68
88
  state[:caption] = text.delete("*")
69
89
  else
70
- klass = text.start_with?("Always true:") ? " class=\"rule\"" : ""
71
- state[:parts] << "<p#{klass}>#{Html.inline(text)}</p>"
90
+ klass = LEADS.find { |lead, _class| text.start_with?(lead) }&.last
91
+ state[:parts] << "<p#{%( class="#{klass}") if klass}>#{Html.inline(text)}</p>"
72
92
  end
73
93
  end
74
94
  end
@@ -11,8 +11,7 @@ module Hecks
11
11
  module Html
12
12
  Block = Struct.new(:type, :text, :items, :level, keyword_init: true)
13
13
 
14
- FONTS = "https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;" \
15
- "0,6..72,600;1,6..72,400&family=Instrument+Sans:wght@400;500;600&display=swap".freeze
14
+ FONTS = "https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&display=swap".freeze
16
15
  MERMAID = "https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.9.1/mermaid.min.js".freeze
17
16
 
18
17
  module_function