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
@@ -62,7 +62,7 @@ module Hecks
62
62
 
63
63
  # Permission rules are a feature of `claude`: any other command would run unconfined.
64
64
  def refuse_unconfinable!(words, profile)
65
- return if profile.nil? || profile.sandboxed? || words == default_command(profile)
65
+ return if profile.nil? || words == default_command(profile)
66
66
 
67
67
  raise Failed, "permission confinement applies only to the default claude command"
68
68
  end
@@ -79,15 +79,14 @@ module Hecks
79
79
  %w[claude -p --permission-mode] + [profile.permission_mode] + profile.tool_flags
80
80
  end
81
81
 
82
- # Runs `words` under the profile's sandbox with only the environment it names, in its own
83
- # process group so a timeout takes the agent's children with it.
82
+ # Runs `words` with only the environment the profile names, in its own process group so a
83
+ # timeout takes the agent's children with it. The reader starts first so an agent that
84
+ # writes before it has read its whole prompt cannot block both sides on a full pipe.
84
85
  def run_confined(words, prompt, chdir, profile)
85
- raise Failed, "no sandbox here; refusing to run an agent unconfined" unless profile.available?
86
-
87
- Open3.popen2e(profile.environment, *profile.confine(words),
86
+ Open3.popen2e(profile.environment, *words,
88
87
  chdir: chdir, unsetenv_others: true, pgroup: true) do |stdin, out, waiter|
89
- feed(stdin, prompt)
90
88
  reader = Thread.new { out.read }
89
+ feed(stdin, prompt)
91
90
  finish(waiter, reader, profile.timeout)
92
91
  end
93
92
  end
@@ -1,93 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "tmpdir"
4
-
5
3
  module Hecks
6
4
  module Adapters
7
- # What one `Agent` run may do: which tools it holds, where it may write, what it may reach on
8
- # the network, which environment variables it sees and how long it may run.
5
+ # What one `Agent` run may do: which tools it holds, where it may write, which environment
6
+ # variables it sees and how long it may run.
9
7
  #
10
- # Confinement uses the macOS sandbox. Where there is none, a confined run refuses to start
11
- # rather than run unconfined.
8
+ # Confinement is the permission rules of the `claude` command itself, so a run keeps the
9
+ # user's own login. Only the default `claude` command understands them; `Agent` refuses to
10
+ # run any other command under a profile.
12
11
  class AgentProfile
13
- # What the network rule lets through: nothing, outbound HTTPS and name lookups, or anything.
14
- NETWORKS = %i[none https any].freeze
15
-
16
- # How a run is confined: under the operating system's sandbox, or by the permission rules
17
- # of the `claude` command itself. Only the second leaves the user's own login usable, since
18
- # the sandbox refuses the keychain and `~/.claude`.
19
- CONFINEMENTS = %i[sandbox permissions].freeze
20
-
21
12
  # The variables every confined agent sees besides the ones its profile names.
22
13
  BASE_ENV = %w[PATH HOME USER LOGNAME SHELL LANG LC_ALL TERM TMPDIR].freeze
23
14
 
24
- # The tools that write files; a permissions-confined run holds them only through path rules.
15
+ # The tools that write files; a confined run holds them only through path rules.
25
16
  WRITING_TOOLS = %w[Write Edit MultiEdit NotebookEdit].freeze
26
17
 
27
- # Home-relative places an agent may not read: credentials and the harness's own state.
28
- CREDENTIAL_PATHS = %w[
29
- .ssh .aws .gnupg .claude .config/gh .config/op .gem/credentials .netrc Library/Keychains
30
- ].freeze
31
-
32
- SANDBOX = "/usr/bin/sandbox-exec"
33
-
34
- # The keywords `new` takes besides the four lists, with what each defaults to.
35
- LIMIT_DEFAULTS = { timeout: nil, budget: nil, confinement: :sandbox }.freeze
36
-
37
18
  # @return [Array<String>] the tools the agent is given
38
19
  attr_reader :tools
39
- # @return [Array<String>] the directories the agent may write, besides the temp directory
20
+ # @return [Array<String>] the directories the agent may write
40
21
  attr_reader :writable
41
- # @return [Symbol] one of `NETWORKS`
42
- attr_reader :network
43
22
  # @return [Array<String>] the environment variable names the agent may see besides `BASE_ENV`
44
23
  attr_reader :env
45
24
  # @return [Integer, nil] seconds the run may take; nil for no limit
46
25
  attr_reader :timeout
47
26
  # @return [Float, nil] the most the run may spend, in dollars, for an agent that takes one
48
27
  attr_reader :budget
49
- # @return [Symbol] one of `CONFINEMENTS`
50
- attr_reader :confinement
51
28
 
52
29
  # @param tools [Array<String>] the tools the agent is given
53
30
  # @param writable [Array<String>] directories the agent may write
54
- # @param network [Symbol] one of `NETWORKS`
55
31
  # @param env [Array<String>] environment variable names to pass through
56
- # @param limits [Hash] `timeout:` (Integer, nil) seconds the run may take, `budget:`
57
- # (Float, nil) dollars it may spend, `confinement:` one of `CONFINEMENTS` (`network`
58
- # applies to the sandbox only)
59
- # @raise [ArgumentError] when `network` or `confinement` is not one it knows
60
- def initialize(tools: [], writable: [], network: :none, env: [], **limits)
61
- limits = LIMIT_DEFAULTS.merge(reject_unknown(limits))
62
- ensure_known(:network, NETWORKS, network)
63
- ensure_known(:confinement, CONFINEMENTS, limits[:confinement])
32
+ # @param timeout [Integer, nil] seconds the run may take
33
+ # @param budget [Float, nil] dollars it may spend
34
+ def initialize(tools: [], writable: [], env: [], timeout: nil, budget: nil)
64
35
  @tools = tools.dup.freeze
65
36
  @writable = writable.dup.freeze
66
- @network = network
67
37
  @env = env.dup.freeze
68
- @timeout, @budget, @confinement = limits.values_at(:timeout, :budget, :confinement)
69
- end
70
-
71
- # @return [Boolean] whether this profile is enforced by the operating system's sandbox
72
- def sandboxed?
73
- confinement == :sandbox
74
- end
75
-
76
- # @return [Boolean] whether this machine can apply this profile's confinement
77
- def available?
78
- !sandboxed? || File.executable?(SANDBOX)
79
- end
80
-
81
- # @param words [Array<String>] the agent's command
82
- # @return [Array<String>] `words`, run under the sandbox with this profile's policy; the
83
- # words unchanged when the confinement is the command's own permission rules
84
- def confine(words)
85
- sandboxed? ? [SANDBOX, "-p", policy, *words] : words
38
+ @timeout = timeout
39
+ @budget = budget
86
40
  end
87
41
 
88
42
  # @return [String] the `claude` permission mode this profile runs under
89
43
  def permission_mode
90
- sandboxed? ? "acceptEdits" : "dontAsk"
44
+ "dontAsk"
91
45
  end
92
46
 
93
47
  # @param source [Hash{String => String}] the environment to take values from
@@ -96,79 +50,27 @@ module Hecks
96
50
  source.to_h.slice(*BASE_ENV, *env)
97
51
  end
98
52
 
99
- # The `claude` flags this profile's tools and budget become.
53
+ # The `claude` flags this profile's tools and budget become. The writing tools are held only
54
+ # through an `Edit` rule per writable directory, which every file-writing tool answers to.
100
55
  #
101
56
  # @return [Array<String>]
102
57
  def tool_flags
103
58
  flags = tools.empty? ? [] : ["--tools", tools.join(","), "--allowedTools", *allowed_tools]
104
- flags += ["--strict-mcp-config"] unless sandboxed?
59
+ flags += ["--strict-mcp-config"]
105
60
  flags + (budget ? ["--max-budget-usd", budget.to_s] : [])
106
61
  end
107
62
 
108
- # @return [String] the sandbox policy: reads open but for credentials, writes where allowed
109
- def policy
110
- [
111
- "(version 1)", "(allow default)",
112
- "(deny file-write*)", write_rule, "(deny file-read*#{paths_clause(credential_paths)})",
113
- *network_rules
114
- ].join("\n")
115
- end
116
-
117
63
  private
118
64
 
119
- # The tools allowed without asking. A permissions-confined run holds the writing tools only
120
- # through an `Edit` rule per writable directory, which every file-writing tool answers to.
121
65
  def allowed_tools
122
- return [tools.join(",")] if sandboxed?
123
-
124
66
  tools - WRITING_TOOLS + writable.map { |path| "Edit(/#{real(path)}/**)" }
125
67
  end
126
68
 
127
- def write_rule
128
- allowed = [real(Dir.tmpdir), *writable.map { |path| real(path) }]
129
- devices = '(literal "/dev/null") (literal "/dev/tty") (regex #"^/dev/fd/")'
130
- "(allow file-write*#{paths_clause(allowed)} #{devices})"
131
- end
132
-
133
- def credential_paths
134
- CREDENTIAL_PATHS.map { |path| File.join(Dir.home, path) }
135
- end
136
-
137
- def paths_clause(paths)
138
- paths.map { |path| " (subpath #{quote(path)})" }.join
139
- end
140
-
141
- def network_rules
142
- case network
143
- when :any then []
144
- when :https then ["(deny network*)", '(allow network-outbound (remote tcp "*:443") (remote udp "*:53"))',
145
- '(allow network-outbound (remote unix-socket (path-literal "/private/var/run/mDNSResponder")))']
146
- else ["(deny network*)"]
147
- end
148
- end
149
-
150
- def reject_unknown(limits)
151
- unknown = limits.keys - LIMIT_DEFAULTS.keys
152
- return limits if unknown.empty?
153
-
154
- raise ArgumentError, "unknown keyword#{"s" if unknown.size > 1}: #{unknown.map(&:inspect).join(", ")}"
155
- end
156
-
157
- def ensure_known(name, allowed, value)
158
- return if allowed.include?(value)
159
-
160
- raise ArgumentError, "#{name} must be one of #{allowed.inspect}, not #{value.inspect}"
161
- end
162
-
163
69
  def real(path)
164
70
  File.realpath(path)
165
71
  rescue SystemCallError
166
72
  File.expand_path(path)
167
73
  end
168
-
169
- def quote(path)
170
- %("#{path.gsub("\\", "\\\\\\\\").gsub('"', '\\"')}")
171
- end
172
74
  end
173
75
  end
174
76
  end
@@ -116,7 +116,7 @@ module Hecks
116
116
  # The profile `--confine` runs the agent under: it writes only the candidates directory.
117
117
  def confinement_profile
118
118
  FileUtils.mkdir_p(@out_dir)
119
- Hecks::Adapters::AgentProfile.new(confinement: :permissions, tools: CONFINED_TOOLS, writable: [@out_dir],
119
+ Hecks::Adapters::AgentProfile.new(tools: CONFINED_TOOLS, writable: [@out_dir],
120
120
  timeout: CONFINED_TIMEOUT, budget: CONFINED_BUDGET)
121
121
  end
122
122
 
@@ -43,7 +43,6 @@ module Hecks
43
43
  gates = BootGates.new
44
44
  load_bound_adapters!(registry)
45
45
  Ports::Persistence.each_plugin { |plugin| plugin.contribute_boot_gates(registry, gates) }
46
- check_compute_rules_backstop!(registry)
47
46
 
48
47
  gates.run!(:pre_verify, registry, directory)
49
48
  registry.verify!
@@ -67,25 +66,6 @@ module Hecks
67
66
  (bound + default).each { |name| load_adapter(registry, name) }
68
67
  end
69
68
 
70
- # A backstop only: fires when a translation declares a `computes`/
71
- # `rekeys` rule but no persistence plugin is loaded to interpret it.
72
- # Any loaded plugin's own compute-rules gate refuses earlier, by name,
73
- # whenever one is loaded — this only runs when nothing was.
74
- def check_compute_rules_backstop!(registry)
75
- return if Ports::Persistence.plugins_loaded?
76
-
77
- registry.translations.each do |translation|
78
- translation.aggregates.each do |aggregate|
79
- next if aggregate.computes.empty? && aggregate.rekeys.empty?
80
-
81
- raise WiringError,
82
- "cannot boot #{translation.domain}::#{aggregate.name}: a compute/rekey rule is declared, but no " \
83
- "persistence plugin that can interpret it is loaded (e.g. require " \
84
- "\"hecks/ports/persistence/plugins/era\")"
85
- end
86
- end
87
- end
88
-
89
69
  private
90
70
 
91
71
  def load_adapter(registry, name)
@@ -2,6 +2,7 @@ require "json"
2
2
  require "securerandom"
3
3
  require "time"
4
4
  require_relative "event"
5
+ require_relative "value"
5
6
  require_relative "../naming"
6
7
 
7
8
  module Hecks
@@ -48,19 +49,11 @@ module Hecks
48
49
  name: hash[:name],
49
50
  aggregate: hash[:aggregate],
50
51
  id: hash[:id],
51
- payload: deep_symbolize(hash[:payload] || {}),
52
+ payload: Value.deep_symbolize(hash[:payload] || {}),
52
53
  occurred_at: hash[:occurred_at],
53
54
  correlation: hash[:correlation]
54
55
  ).emit!
55
56
  end
56
-
57
- def deep_symbolize(value)
58
- case value
59
- when Hash then value.to_h { |k, v| [k.to_sym, deep_symbolize(v)] }
60
- when Array then value.map { |element| deep_symbolize(element) }
61
- else value
62
- end
63
- end
64
57
  end
65
58
  end
66
59
  end
@@ -101,15 +101,7 @@ module Hecks
101
101
  def spread(answer)
102
102
  return { answered: answer } unless answer.is_a?(Hash)
103
103
 
104
- answer.to_h { |key, value| [key.to_sym, deep_symbolize(value)] }
105
- end
106
-
107
- def deep_symbolize(value)
108
- case value
109
- when Hash then value.to_h { |k, v| [k.to_sym, deep_symbolize(v)] }
110
- when Array then value.map { |element| deep_symbolize(element) }
111
- else value
112
- end
104
+ Value.deep_symbolize(answer)
113
105
  end
114
106
 
115
107
  # The record, if there is one. A missing record is not an error here: the ask still
@@ -117,8 +117,8 @@ module Hecks
117
117
  [domain, aggregate]
118
118
  end
119
119
 
120
- # Not every aggregate in a routed domain is Lambda-bound (a `compute` rule can
121
- # only run against Postgres). Test the adapter's capability, not its name.
120
+ # Not every aggregate in a routed domain is Lambda-bound (a persistence binding
121
+ # may name another adapter). Test the adapter's capability, not its name.
122
122
  def remote_bound?(domain, aggregate)
123
123
  adapter_name = Ports::Persistence::BindingPolicy.resolve(@registry, domain, aggregate).adapter
124
124
  @registry.adapter_class(adapter_name) <= Ports::Persistence::RemoteRuntime
@@ -0,0 +1,60 @@
1
+ module Hecks
2
+ module Runtime
3
+ class Value
4
+ # The shape a caller is answered with: a single-attribute value object as its bare field,
5
+ # following the declared types down through nested value objects and lists. Extended into
6
+ # `Value` beside `Coercion`; `materialize` keeps the wrapped shape stored state depends on.
7
+ module Unwrapping
8
+ # `materialize`, but a single-attribute value object unwraps to its bare field. What
9
+ # `group_by` rows and `Driving::Json.materialize` answer with.
10
+ #
11
+ # @param value [Object] the value to materialize; anything that is not a `Runtime::Value`,
12
+ # Array or Hash passes through unchanged
13
+ # @return [Object] `value` with every nested `Runtime::Value` replaced by its own sole
14
+ # field's value (recursively unwrapped), or by its own `Hash` of fields when it has
15
+ # more than one
16
+ def materialize_unwrapped(value)
17
+ case value
18
+ when self then unwrap_typed(value.value_object, value.raw_fields)
19
+ when Array then value.map { |item| materialize_unwrapped(item) }
20
+ when Hash then value.transform_values { |item| materialize_unwrapped(item) }
21
+ else value
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ # A value object's fields by its declared attributes. A nested value object is often held
28
+ # as a plain Hash, so its own type is looked up rather than read off the field.
29
+ def unwrap_typed(value_object, fields)
30
+ sole = value_object.sole_attribute
31
+ return unwrap_field(value_object, sole, fields[sole.name]) if sole
32
+
33
+ value_object.attributes.to_h { |attr| [attr.name, unwrap_field(value_object, attr, fields[attr.name])] }
34
+ end
35
+
36
+ def unwrap_field(value_object, attribute, held)
37
+ case held
38
+ when Array then held.map { |item| unwrap_field(value_object, attribute, item) }
39
+ when Hash then unwrap_held_hash(value_object, attribute, held)
40
+ else materialize_unwrapped(held)
41
+ end
42
+ end
43
+
44
+ def unwrap_held_hash(value_object, attribute, held)
45
+ nested = nested_value_object(value_object, attribute.type)
46
+ return materialize_unwrapped(held) unless nested
47
+
48
+ unwrap_typed(nested, held.transform_keys(&:to_sym))
49
+ end
50
+
51
+ # The value object a field's type names, resolved against the aggregate that declares
52
+ # `value_object`; nil for a primitive, or when the owner is not known.
53
+ def nested_value_object(value_object, type)
54
+ owner = value_object.respond_to?(:hecks_owner) ? value_object.hecks_owner : nil
55
+ owner.respond_to?(:value_object) ? value_object_for(owner, type) : nil
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -9,12 +9,13 @@ require_relative "value/flags"
9
9
  require_relative "value/identifiers"
10
10
  require_relative "value/entity_list_coercion"
11
11
  require_relative "value/admission"
12
+ require_relative "value/unwrapping"
12
13
 
13
14
  module Hecks
14
15
  module Runtime
15
16
  # A typed value object: frozen fields, read by name.
16
17
  # Construction lives in value/coercion.rb, field_checks.rb, entity_list_coercion.rb and
17
- # admission.rb.
18
+ # admission.rb; the shape a caller is answered with in unwrapping.rb.
18
19
  class Value
19
20
  extend FieldChecks
20
21
  extend Coercion
@@ -24,6 +25,7 @@ module Hecks
24
25
  extend Identifiers
25
26
  extend EntityListCoercion
26
27
  extend Admission
28
+ extend Unwrapping
27
29
 
28
30
  attr_reader :value_object
29
31
 
@@ -102,32 +104,19 @@ module Hecks
102
104
  end
103
105
  end
104
106
 
105
- # `materialize`, but a single-attribute value object unwraps to its bare field.
106
- # Opt-in for `group_by` rows; `materialize` keeps the wrapped shape callers depend on.
107
+ # Converts every Hash key to a Symbol, at every depth of parsed JSON.
107
108
  #
108
- # @param value [Object] the value to materialize; anything that is not a `Runtime::Value`,
109
- # Array or Hash passes through unchanged
110
- # @return [Object] `value` with every nested `Runtime::Value` replaced by its own sole
111
- # field's value (recursively unwrapped), or by its own `Hash` of fields when it has
112
- # more than one
113
- def self.materialize_unwrapped(value)
109
+ # @param value [Hash, Array, Object] a Hash or Array is walked; any other value is a leaf
110
+ # @return [Hash{Symbol => Object}, Array, Object] a new structure of the same shape with
111
+ # Symbol keys; a leaf is returned as it came
112
+ def self.deep_symbolize(value)
114
113
  case value
115
- when self then unwrap_value_object(value)
116
- when Array then value.map { |item| materialize_unwrapped(item) }
117
- when Hash then value.transform_values { |item| materialize_unwrapped(item) }
114
+ when Hash then value.to_h { |key, item| [key.to_sym, deep_symbolize(item)] }
115
+ when Array then value.map { |item| deep_symbolize(item) }
118
116
  else value
119
117
  end
120
118
  end
121
119
 
122
- def self.unwrap_value_object(value)
123
- sole = value.value_object.sole_attribute
124
- return materialize_unwrapped(value[sole.name]) if sole
125
-
126
- # Not `value.to_h`: it materializes nested values to Hashes, ending the recursion.
127
- value.value_object.attributes.to_h { |attr| [attr.name, materialize_unwrapped(value[attr.name])] }
128
- end
129
- private_class_method :unwrap_value_object
130
-
131
120
  # Reduces an append-only `list_of` sub-log to its current state: the latest row per `key`.
132
121
  # Grouping only; what counts as removed, and the ordering, belong to the caller.
133
122
  #
@@ -36,6 +36,7 @@ module Hecks
36
36
  ir = shaped(exporter.call(@registry).fetch(@domain_name))
37
37
  ir[:lineage] = shaped(exporter.lineage(@registry, @domain_name))
38
38
  ir[:persistence] = shaped(exporter.persistence(@registry, @domain_name))
39
+ add_persistence_wiring(ir, exporter)
39
40
  add_seams(ir, exporter)
40
41
  add_edges_and_source(ir)
41
42
  ir
@@ -43,8 +44,28 @@ module Hecks
43
44
 
44
45
  private
45
46
 
47
+ # The adapters and the world that the host checks the persistence against.
48
+ def add_persistence_wiring(document, exporter)
49
+ document[:adapters] = adapters(exporter, document[:persistence])
50
+ document[:world] = world(exporter)
51
+ end
52
+
46
53
  def shaped(payload) = self.class.json_shaped(payload)
47
54
 
55
+ # The bound persistence adapters' declarations, from the `.adapter` files under the
56
+ # source root's `lib/` and the domain's own directory, so the host can look them up.
57
+ def adapters(exporter, persistence)
58
+ files = exporter.adapter_files_under([File.join(RustBuild::ROOT, "lib"), File.dirname(@bluebook_dir)])
59
+ shaped(exporter.persistence_adapters(persistence, files))
60
+ end
61
+
62
+ # The target's world persistence, from `<domain directory name>.world` beside the bluebooks
63
+ # (names only), so the host can check what the world binds against the adapters.
64
+ def world(exporter)
65
+ path = File.join(@bluebook_dir, "#{File.basename(File.dirname(@bluebook_dir))}.world")
66
+ shaped(exporter.persistence_world(path, @domain_name))
67
+ end
68
+
48
69
  def add_seams(document, exporter)
49
70
  seams = SEAMS.to_h { |seam| [seam, exporter.public_send(seam, @registry, @domain_name)] }
50
71
  seams.each { |seam, value| document[seam] = shaped(value) unless value.empty? }
@@ -56,14 +77,12 @@ module Hecks
56
77
  "hecksagon, not a deploy-time guess, names the identity verbs."
57
78
  end
58
79
 
59
- # Edges carry their own precompiled SQL, so a boot-time mint only executes it. Committed
60
- # approvals sit beside the edges. The source text is verbatim: the era's integrity digest is
80
+ # Edges carry their own precompiled SQL, so a boot-time mint only executes it. The source
81
+ # text is verbatim: the era's integrity digest is
61
82
  # the SHA256 of it, not of anything re-derived from the parsed IR.
62
83
  def add_edges_and_source(document)
63
84
  edges = Hecks::Projector::Exporter.translations(@registry).select { |edge| edge[:domain] == @domain_name }
64
85
  document[:translations] = shaped(edges)
65
- approvals = Hecks::Translation::ApprovalFile.read_all(@bluebook_dir)
66
- document[:approvals] = shaped(approvals) unless approvals.empty?
67
86
  document[:source_text] = Hecks::Runtime::EraCheck.source_text_for(
68
87
  @registry.bluebooks.fetch(@domain_name), @bluebook_dir, registry: @registry
69
88
  )
@@ -31,7 +31,6 @@ module Hecks
31
31
  # @raise [Failure] when the target is not installed or a step fails
32
32
  def call(argv)
33
33
  domain = argv.first or raise Failure, "usage: hecks build_wasm <domain>"
34
- return build_with_rust(domain) if ENV["HECKS_PARSER"] == "rust" && ENV["HECKS_CODEGEN"] == "rust"
35
34
 
36
35
  require_target!
37
36
  rust_dir = RustBuild.rust_dir
@@ -48,17 +47,6 @@ module Hecks
48
47
  raise Failure, "project_rust failed for #{domain}" unless status.zero?
49
48
  end
50
49
 
51
- # With the Rust parser and codegen, `hecks-build` runs the same steps and writes the same
52
- # outputs.
53
- def build_with_rust(domain)
54
- build_dir = File.join(ROOT, "rust", "build")
55
- puts "== cargo build (hecks-build) =="
56
- RustBuild.command!("cargo", "build", chdir: build_dir)
57
- puts "== hecks-build #{domain} --wasm =="
58
- RustBuild.command!(File.join(build_dir, "target", "debug", "hecks-build"), domain, "--wasm")
59
- 0
60
- end
61
-
62
50
  def require_target!
63
51
  return if RustBuild.target_installed?(TARGET)
64
52
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fileutils"
4
+ require_relative "../rust_format"
4
5
 
5
6
  module Hecks
6
7
  module RustBuild
@@ -9,17 +10,8 @@ module Hecks
9
10
  module WriteIfChanged
10
11
  module_function
11
12
 
12
- # Records every path written or confirmed unchanged inside the block, then deletes
13
- # files left in `dir` that were never touched (orphans of removed aggregates).
14
- # Every write is recorded against every open ancestor directory.
15
- # `push_directory`/`pop_and_prune` are the same operation split for non-adjacent writes.
16
- def track_directory(dir)
17
- push_directory(dir)
18
- yield
19
- ensure
20
- pop_and_prune(dir)
21
- end
22
-
13
+ # Starts recording every path written or confirmed unchanged inside `dir`, against every open
14
+ # ancestor directory too; `pop_and_prune` closes it and deletes the files never touched.
23
15
  def push_directory(dir)
24
16
  FileUtils.mkdir_p(dir)
25
17
  stack.push([File.expand_path(dir), Set.new])
@@ -56,6 +48,7 @@ module Hecks
56
48
  # Returns whether it wrote.
57
49
  def call(path, content)
58
50
  record_touch(path)
51
+ content = RustFormat.call(content) if RustFormat.rust?(path)
59
52
  # `.b` on both sides: String#== is encoding-aware, so UTF-8 vs ASCII-8BIT
60
53
  # content can compare unequal even when the bytes are identical.
61
54
  return false if File.exist?(path) && File.binread(path) == content.b
@@ -64,13 +57,20 @@ module Hecks
64
57
  true
65
58
  end
66
59
 
60
+ # Fills `tmp` from the block, then lays it out with rustfmt when `path` is Rust source.
61
+ def write_formatted(tmp, path, &)
62
+ File.open(tmp, "w", &)
63
+ File.write(tmp, RustFormat.call(File.read(tmp))) if RustFormat.rust?(path)
64
+ end
65
+ private_class_method :write_formatted
66
+
67
67
  # Like `call`, for callers that build content by writing to an IO. Yields a sibling
68
68
  # temp file, then renames it into place only if the bytes differ. Returns whether it wrote.
69
69
  def block(path, &)
70
70
  record_touch(path)
71
71
  tmp = "#{path}.tmp-#{Process.pid}-#{rand(1_000_000)}"
72
72
  begin
73
- File.open(tmp, "w", &)
73
+ write_formatted(tmp, path, &)
74
74
  changed = !(File.exist?(path) && FileUtils.compare_file(tmp, path))
75
75
  FileUtils.mv(tmp, path) if changed
76
76
  changed
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Hecks
6
+ # Runs generated Rust through rustfmt, so a generator writes what `cargo fmt --check` accepts.
7
+ #
8
+ # Every generator that writes a `.rs` file formats its text here before comparing it with the tree
9
+ # or writing it, so a drift check and a write see the same bytes. The edition is pinned in the
10
+ # call, not read from a `rustfmt.toml`, because a generator may run from any directory.
11
+ module RustFormat
12
+ # Raised when the rustfmt program cannot be found.
13
+ class Missing < StandardError; end
14
+
15
+ # Raised when rustfmt fails for a reason that is not the text: a toolchain that will not
16
+ # install, say. Falling back to the unformatted text would write the wrong bytes without a word.
17
+ class Failed < StandardError; end
18
+
19
+ EDITION = "2021"
20
+
21
+ # The most passes `call` makes; a text still changing after them keeps the last result.
22
+ PASSES = 3
23
+
24
+ module_function
25
+
26
+ # @param path [String] where the text will be written
27
+ # @return [Boolean] whether the path is Rust source
28
+ def rust?(path)
29
+ path.to_s.end_with?(".rs")
30
+ end
31
+
32
+ # Formats one Rust source text.
33
+ #
34
+ # Text rustfmt cannot parse is returned as it came, with a warning on stderr: the compiler will
35
+ # name the real fault, and a generator should not be the thing that hides it. Any other failure
36
+ # of rustfmt raises, since the text is not the problem.
37
+ #
38
+ # @param text [String] Rust source
39
+ # @return [String] the same source as rustfmt lays it out
40
+ # @raise [Missing] when rustfmt is not installed
41
+ # @raise [Failed] when rustfmt fails for a reason other than the text
42
+ def call(text)
43
+ PASSES.times do
44
+ formatted = pass(text)
45
+ return formatted if formatted == text
46
+
47
+ text = formatted
48
+ end
49
+ text
50
+ end
51
+
52
+ # One rustfmt run. rustfmt does not always reach a fixed point in one pass, so `call` repeats it
53
+ # until the text stops changing, as a later `cargo fmt --check` would have it.
54
+ #
55
+ # @param text [String] Rust source
56
+ # @return [String] the source after one pass, or unchanged when rustfmt cannot parse it
57
+ # @raise [Failed] when rustfmt fails for a reason other than the text
58
+ def pass(text)
59
+ out, err, status = Open3.capture3("rustfmt", "--edition", EDITION, "--emit", "stdout", stdin_data: text)
60
+ return out if status.success?
61
+ raise Failed, "rustfmt failed: #{err.lines.first&.strip}" unless err.include?("<stdin>")
62
+
63
+ warn "hecks: rustfmt could not format generated Rust, leaving it as generated: #{err.lines.first&.strip}"
64
+ text
65
+ rescue Errno::ENOENT
66
+ raise Missing, "rustfmt is not installed: generated Rust is formatted with it; run `rustup component add rustfmt`"
67
+ end
68
+
69
+ # Formats the Rust texts of a path-to-text map and leaves the others alone.
70
+ #
71
+ # @param content [Hash{String => String}] absolute path to text
72
+ # @return [Hash{String => String}] the same map with each `.rs` text formatted
73
+ def files(content)
74
+ content.to_h { |path, text| [path, rust?(path) ? call(text) : text] }
75
+ end
76
+ end
77
+ end