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
@@ -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
@@ -102,6 +102,19 @@ module Hecks
102
102
  end
103
103
  end
104
104
 
105
+ # Converts every Hash key to a Symbol, at every depth of parsed JSON.
106
+ #
107
+ # @param value [Hash, Array, Object] a Hash or Array is walked; any other value is a leaf
108
+ # @return [Hash{Symbol => Object}, Array, Object] a new structure of the same shape with
109
+ # Symbol keys; a leaf is returned as it came
110
+ def self.deep_symbolize(value)
111
+ case value
112
+ when Hash then value.to_h { |key, item| [key.to_sym, deep_symbolize(item)] }
113
+ when Array then value.map { |item| deep_symbolize(item) }
114
+ else value
115
+ end
116
+ end
117
+
105
118
  # `materialize`, but a single-attribute value object unwraps to its bare field.
106
119
  # Opt-in for `group_by` rows; `materialize` keeps the wrapped shape callers depend on.
107
120
  #
@@ -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
@@ -48,19 +48,13 @@ module Hecks
48
48
  failure[:message].split("; ").all? { |chunk| matchers.any? { |regex| regex.match?(chunk) } }
49
49
  end
50
50
 
51
- # Skips only exact, already-tracked wiring gaps; any other WiringError is re-raised.
51
+ # Skips only exact, already-tracked wiring gaps (`SKIPPED_DOMAIN_REASONS`); none is tracked
52
+ # now, so any WiringError is re-raised.
52
53
  #
53
54
  # @param error [Hecks::Runtime::WiringError] what generating a sequence raised
54
55
  # @return [Symbol] a key of `SKIPPED_DOMAIN_REASONS`
55
56
  def known_unfuzzable_wiring_gap(error)
56
- case error.message
57
- when %r{a compute/rekey rule is declared}
58
- # compute/rekey needs an era/Postgres migration, so it cannot boot under IsolatedBoot's
59
- # Memory rebind. Real coverage lives in spec/adapters/driven/postgres_era/*_spec.rb.
60
- :skip_compute_rekey
61
- else
62
- raise error
63
- end
57
+ raise error
64
58
  end
65
59
 
66
60
  # Shrinks steps, then arguments, keeping a removal only while the same finding reproduces.
@@ -29,9 +29,7 @@ module Hecks
29
29
  KNOWN_FUZZ_FINDINGS = {}.freeze
30
30
 
31
31
  # Why a domain is skipped, by the reason `known_unfuzzable_wiring_gap` answers.
32
- SKIPPED_DOMAIN_REASONS = {
33
- skip_compute_rekey: "declares compute/rekey, which only an era/Postgres-bound boot can interpret"
34
- }.freeze
32
+ SKIPPED_DOMAIN_REASONS = {}.freeze
35
33
 
36
34
  # The persistence an ephemeral boot may use.
37
35
  ADAPTERS = %i[memory sqlite postgres].freeze
@@ -30,17 +30,14 @@ module Hecks
30
30
  end
31
31
 
32
32
  # @return [Integer] the exit status
33
- def conclude(db, lineage, edge, failed, approve)
33
+ def conclude(db, failed)
34
+ db.close
34
35
  if failed
35
- db.close
36
36
  puts "AUDIT REFUSED."
37
37
  return 1
38
38
  end
39
39
 
40
- needs_approval = edge.aggregates.any? { |declared| !declared.computes.empty? || !declared.rekeys.empty? }
41
- record_approval(lineage, edge) if needs_approval && approve
42
- db.close
43
- print_passed(needs_approval, approve)
40
+ puts "AUDIT PASSED — review the samples above; intent is yours to approve."
44
41
  0
45
42
  end
46
43
 
@@ -79,28 +76,6 @@ module Hecks
79
76
  puts " before: #{JSON.generate(sample[:before])}" if sample[:before]
80
77
  puts " after: #{JSON.generate(sample[:after])}" if sample[:after]
81
78
  end
82
-
83
- # Binds to what was reviewed — this edge and the journal as it stands now — so it must
84
- # live in the database, not the repo.
85
- def record_approval(lineage, edge)
86
- lineage.record_approval!(
87
- from: edge.from, to: edge.to,
88
- edge_digest: Hecks::Translation::Audit.edge_digest(edge)
89
- )
90
- end
91
-
92
- def print_passed(needs_approval, approve)
93
- if needs_approval && approve
94
- puts "AUDIT PASSED — approval recorded in the database, bound to this edge and this journal."
95
- puts "A journal that advances before the mint invalidates it; re-run with --approve if that happens."
96
- elsif needs_approval
97
- puts "AUDIT PASSED — this edge carries a compute or rekey rule, and the samples above are " \
98
- "its ONLY verification."
99
- puts "If they show what you intended, run again with --approve; the mint refuses until you do."
100
- else
101
- puts "AUDIT PASSED — review the samples above; intent is yours to approve."
102
- end
103
- end
104
79
  end
105
80
  end
106
81
  end