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
@@ -16,13 +16,16 @@ module Hecks
16
16
  # The attribute kept its name, so `rename`/`move` cannot express it.
17
17
  TranslationRetype = Struct.new(:from, :to)
18
18
 
19
- # A computed transform whose only implementation is a SQL expression run inside
20
- # the compiled Postgres head; an aggregate carrying one refuses to boot elsewhere.
21
- TranslationCompute = Struct.new(:from, :to, :sql)
19
+ # A new field computed from the record's own text by an expression, read before any other
20
+ # rule runs. The fields the expression reads stay where they are; `drop` removes them.
21
+ # `expression` is the source text and `tree` its parsed plan form.
22
+ TranslationDerive = Struct.new(:to, :expression) do
23
+ def tree = TranslationExpression.parse(expression)
24
+ end
22
25
 
23
- # The aggregate's own key is recomputed by a SQL expression; stored state is untouched.
24
- # Postgres-only like `compute`, and it has no `from:`/`to:` path.
25
- TranslationRekey = Struct.new(:sql)
26
+ # The aggregate's id is the value of the field a `derive` writes, for every record where that
27
+ # derive has a value; a record where it has none keeps its id.
28
+ TranslationRekey = Struct.new(:to)
26
29
 
27
30
  # A newly added, required attribute with no source in old data.
28
31
  # `default` is what an existing record reads until a command writes a real value;
@@ -32,12 +35,12 @@ module Hecks
32
35
  # One aggregate's part of a translation between eras.
33
36
  # `drops` makes data loss a declared decision.
34
37
  class TranslationAggregate
35
- attr_reader :name, :was, :renames, :moves, :converts, :drops, :retypes, :computes, :rekeys, :backfills
38
+ attr_reader :name, :was, :renames, :moves, :converts, :drops, :retypes, :derives, :rekeys, :backfills
36
39
 
37
40
  # Every optional field and what it holds when the declaration omits it.
38
41
  FIELD_DEFAULTS = {
39
42
  was: nil, renames: {}, moves: [], converts: [], drops: [], retypes: [],
40
- computes: [], rekeys: [], backfills: []
43
+ derives: [], rekeys: [], backfills: []
41
44
  }.freeze
42
45
 
43
46
  # @param name [String, Symbol] the aggregate's name in the destination era
@@ -47,8 +50,8 @@ module Hecks
47
50
  # @param converts [Array<Bluebook::TranslationConvert>] fields remapped via a lookup table
48
51
  # @param drops [Array<Symbol>] fields deliberately not carried forward
49
52
  # @param retypes [Array<Bluebook::TranslationRetype>] value object or entity type changes
50
- # @param computes [Array<Bluebook::TranslationCompute>] fields computed by Postgres-only SQL
51
- # @param rekeys [Array<Bluebook::TranslationRekey>] SQL recomputing the aggregate's identity
53
+ # @param derives [Array<Bluebook::TranslationDerive>] fields computed from the record's text
54
+ # @param rekeys [Array<Bluebook::TranslationRekey>] the aggregate's identity, recomputed
52
55
  # @param backfills [Array<Bluebook::TranslationBackfill>] new required fields, no old source
53
56
  def initialize(name:, **given)
54
57
  KeywordFields.assign(self, KeywordFields.fill(given, FIELD_DEFAULTS))
@@ -83,6 +86,11 @@ module Hecks
83
86
  # @return [Bluebook::TranslationAggregate, nil] the aggregate's translation, or
84
87
  # `nil` if `name` carries no translation rules
85
88
  def for_aggregate(name) = @aggregates.find { |aggregate| aggregate.name == name.to_s }
89
+
90
+ # This edge as plain data: each aggregate's rules as an ordered list of ops.
91
+ #
92
+ # @return [Hash] see `Bluebook::TranslationPlan`
93
+ def to_plan = TranslationPlan.build(self)
86
94
  end
87
95
  end
88
96
  end
@@ -0,0 +1,63 @@
1
+ module Hecks
2
+ module Bluebook
3
+ module TranslationExpression
4
+ # Computes a parsed expression for one record's state. The reference semantics the
5
+ # compiled Postgres form is held to.
6
+ module Evaluator
7
+ module_function
8
+
9
+ # @param tree [Array] a tree `TranslationExpression.parse` returned
10
+ # @param state [Hash] the record's stored state
11
+ # @return [String, nil] the computed text; nil when the expression has no value
12
+ def call(tree, state)
13
+ case tree.first
14
+ when "lit" then tree[1]
15
+ when "null" then nil
16
+ when "ref" then reference(tree[1], state)
17
+ else apply(tree.first, tree.drop(1), state)
18
+ end
19
+ end
20
+
21
+ # Reads a dotted path out of a record; only text counts as a value.
22
+ def reference(path, state)
23
+ value = path.split(".").inject(state) { |held, key| held.is_a?(Hash) ? member(held, key) : nil }
24
+ value.is_a?(String) ? value : nil
25
+ end
26
+
27
+ # A record's top-level keys are symbols, a value object's are strings.
28
+ def member(hash, key) = hash.key?(key.to_sym) ? hash[key.to_sym] : hash[key]
29
+
30
+ def apply(name, arguments, state)
31
+ return lookup(arguments, state) if name == "lookup"
32
+ return replace(arguments, state) if name == "replace"
33
+
34
+ values = arguments.map { |argument| call(argument, state) }
35
+ return nil if values.any?(&:nil?)
36
+
37
+ fold(name, values)
38
+ end
39
+
40
+ def fold(name, values)
41
+ case name
42
+ when "lower" then values.first.downcase(:ascii)
43
+ when "upper" then values.first.upcase(:ascii)
44
+ else values.join
45
+ end
46
+ end
47
+
48
+ def replace(arguments, state)
49
+ target, from, to = arguments
50
+ call(target, state)&.gsub(from.last) { to.last }
51
+ end
52
+
53
+ def lookup(arguments, state)
54
+ key, table, fallback = arguments
55
+ value = call(key, state)
56
+ return nil if value.nil?
57
+
58
+ table.fetch(value) { call(fallback, state) }
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,127 @@
1
+ require "json"
2
+ require "strscan"
3
+
4
+ module Hecks
5
+ module Bluebook
6
+ module TranslationExpression
7
+ # Reads the text of an expression into its tree, refusing anything outside the language.
8
+ class Parser
9
+ NAME = /[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)*/
10
+ STRING = /"(?:[^"\\]|\\.)*"/
11
+
12
+ # @param text [String] the expression's source text
13
+ def initialize(text)
14
+ @text = text.to_s
15
+ @scan = StringScanner.new(@text)
16
+ end
17
+
18
+ # @return [Array] the expression tree
19
+ # @raise [TranslationExpression::Refused] if the text is not a well-formed expression
20
+ def call
21
+ tree = expression
22
+ skip
23
+ refuse("unexpected #{@scan.rest[0, 12].inspect} after the expression") unless @scan.eos?
24
+ tree
25
+ end
26
+
27
+ private
28
+
29
+ def expression
30
+ skip
31
+ return ["lit", string] if @scan.check(/"/)
32
+
33
+ name = @scan.scan(NAME) || refuse("expected a field, a string or a function")
34
+ return function(name) if @scan.check(/\s*\(/)
35
+
36
+ name == "null" ? ["null"] : ["ref", name]
37
+ end
38
+
39
+ def function(name)
40
+ refuse("#{name} is not a function (#{FUNCTIONS.keys.join(", ")})") unless FUNCTIONS.key?(name)
41
+ punct("(")
42
+ arguments = list { argument(name, _1) }
43
+ punct(")")
44
+ check_arguments(name, arguments)
45
+ [name, *arguments]
46
+ end
47
+
48
+ def argument(name, index)
49
+ skip
50
+ return table if name == "lookup" && index == 1
51
+
52
+ expression
53
+ end
54
+
55
+ def list
56
+ items = []
57
+ loop do
58
+ items << yield(items.size)
59
+ skip
60
+ break unless @scan.skip(/,/)
61
+ end
62
+ items
63
+ end
64
+
65
+ def table
66
+ punct("{")
67
+ pairs = list { pair }
68
+ punct("}")
69
+ refuse("a lookup table repeats a key") unless pairs.map(&:first).uniq.size == pairs.size
70
+ pairs.to_h
71
+ end
72
+
73
+ def pair
74
+ skip
75
+ key = string
76
+ punct(":")
77
+ skip
78
+ [key, string]
79
+ end
80
+
81
+ def string
82
+ token = @scan.scan(STRING) || refuse("expected a string")
83
+ JSON.parse(token, quirks_mode: true)
84
+ rescue JSON::ParserError
85
+ refuse("#{token} is not a well-formed string")
86
+ end
87
+
88
+ def punct(mark)
89
+ skip
90
+ @scan.skip(Regexp.new(Regexp.escape(mark))) || refuse("expected #{mark.inspect}")
91
+ end
92
+
93
+ def skip = @scan.skip(/\s+/)
94
+
95
+ def check_arguments(name, arguments)
96
+ arity = FUNCTIONS.fetch(name)
97
+ refuse("#{name} takes #{arity} expressions, not #{arguments.size}") if arity && arguments.size != arity
98
+ refuse("concat takes two or more expressions") if arity.nil? && arguments.size < 2
99
+ check_literals(name, arguments)
100
+ end
101
+
102
+ def check_literals(name, arguments)
103
+ case name
104
+ when "replace" then check_replace(arguments)
105
+ when "lookup" then check_lookup(arguments)
106
+ end
107
+ end
108
+
109
+ def check_replace(arguments)
110
+ _target, from, to = arguments
111
+ refuse("replace needs string literals for what to find and what to put") unless [from, to].all? { _1.first == "lit" }
112
+ refuse("replace cannot look for an empty string") if from.last.empty?
113
+ end
114
+
115
+ def check_lookup(arguments)
116
+ _key, table, fallback = arguments
117
+ refuse("lookup needs a table of at least one pair") unless table.is_a?(Hash) && !table.empty?
118
+ refuse("lookup needs a string literal or null as its fallback") unless %w[lit null].include?(fallback.first)
119
+ end
120
+
121
+ def refuse(why)
122
+ raise Refused, "#{why} (in #{@text.inspect} at offset #{@scan.pos})"
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,39 @@
1
+ require_relative "translation_expression/parser"
2
+ require_relative "translation_expression/evaluator"
3
+
4
+ module Hecks
5
+ module Bluebook
6
+ # The expression a `derive` rule computes a new field from: text functions over the record's
7
+ # stored text, with no storage engine in sight. It parses to a tree of plain arrays, which
8
+ # is also its plan form:
9
+ #
10
+ # ["ref", "name.value"] ["lit", "text"] ["null"]
11
+ # ["lower", x] ["upper", x] ["concat", x, y, ...]
12
+ # ["replace", x, ["lit", from], ["lit", to]]
13
+ # ["lookup", x, {"key" => "value"}, ["lit", default] | ["null"]]
14
+ #
15
+ # A reference that is missing or not text has no value, and a function with a valueless
16
+ # argument has none either.
17
+ module TranslationExpression
18
+ # Raised when the text of an expression is not one the language accepts.
19
+ class Refused < StandardError; end
20
+
21
+ # The functions and how many expressions each takes; `nil` means "two or more".
22
+ FUNCTIONS = { "lower" => 1, "upper" => 1, "concat" => nil, "replace" => 3, "lookup" => 3 }.freeze
23
+
24
+ # Parses an expression's text into its tree.
25
+ #
26
+ # @param text [String] the expression, e.g. `lower(name.value)`
27
+ # @return [Array] the expression tree
28
+ # @raise [Refused] if the text is not a well-formed expression
29
+ def self.parse(text) = Parser.new(text).call
30
+
31
+ # Computes an expression's value for one record's state.
32
+ #
33
+ # @param tree [Array] a tree `parse` returned
34
+ # @param state [Hash] the record's stored state
35
+ # @return [String, nil] the text it computes; nil when it has no value
36
+ def self.evaluate(tree, state) = Evaluator.call(tree, state)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,70 @@
1
+ module Hecks
2
+ module Bluebook
3
+ # A translation as data: each aggregate's rules as an ordered list of ops any runtime can
4
+ # interpret, with nothing in them that names a storage engine. In the order they apply to a
5
+ # record, each op has its name under "op" plus:
6
+ #
7
+ # derive "to" "expression" (a tree) rename "from" "to" move "from" "to" (dotted paths)
8
+ # convert "from" "to" "values" ([old, new] pairs) drop "path" retype "from" "to"
9
+ # backfill "path" "default" rekey_to "to"
10
+ module TranslationPlan
11
+ VERSION = 1
12
+
13
+ # The op names, in the order they apply; one per rule list of a `TranslationAggregate`.
14
+ OPS = %w[derive rename move convert drop retype backfill rekey_to].freeze
15
+
16
+ module_function
17
+
18
+ # @param translation [Bluebook::Translation] a declared translation edge
19
+ # @return [Hash] the edge as plain data, ready for `JSON.generate`
20
+ def build(translation)
21
+ {
22
+ "version" => VERSION, "domain" => translation.domain, "from" => translation.from.to_s,
23
+ "to" => translation.to.to_s, "retired" => translation.retired.map(&:to_s),
24
+ "aggregates" => translation.aggregates.map { |aggregate| aggregate_plan(aggregate) }
25
+ }
26
+ end
27
+
28
+ def aggregate_plan(aggregate)
29
+ { "name" => aggregate.name, "was" => aggregate.was, "ops" => ops(aggregate) }
30
+ end
31
+
32
+ def ops(aggregate)
33
+ [*derives(aggregate), *renames(aggregate), *moves(aggregate), *converts(aggregate), *drops(aggregate),
34
+ *retypes(aggregate), *backfills(aggregate), *rekeys(aggregate)]
35
+ end
36
+
37
+ def derives(aggregate)
38
+ aggregate.derives.map { |d| { "op" => "derive", "to" => d.to, "expression" => d.tree } }
39
+ end
40
+
41
+ def renames(aggregate)
42
+ aggregate.renames.map { |from, to| { "op" => "rename", "from" => from.to_s, "to" => to.to_s } }
43
+ end
44
+
45
+ def moves(aggregate)
46
+ aggregate.moves.map { |m| { "op" => "move", "from" => m.from, "to" => m.to } }
47
+ end
48
+
49
+ def converts(aggregate)
50
+ aggregate.converts.map do |c|
51
+ { "op" => "convert", "from" => c.from, "to" => c.to, "values" => c.values.map { |old, new| [old, new] } }
52
+ end
53
+ end
54
+
55
+ def drops(aggregate) = aggregate.drops.map { |path| { "op" => "drop", "path" => path.to_s } }
56
+
57
+ def retypes(aggregate)
58
+ aggregate.retypes.map { |r| { "op" => "retype", "from" => r.from, "to" => r.to } }
59
+ end
60
+
61
+ def backfills(aggregate)
62
+ aggregate.backfills.map { |b| { "op" => "backfill", "path" => b.name.to_s, "default" => b.default } }
63
+ end
64
+
65
+ def rekeys(aggregate)
66
+ aggregate.rekeys.map { |rekey| { "op" => "rekey_to", "to" => rekey.to } }
67
+ end
68
+ end
69
+ end
70
+ end
@@ -29,7 +29,9 @@ require_relative "bluebook/ask_resolution"
29
29
  require_relative "bluebook/process_manager"
30
30
  require_relative "bluebook/aggregate"
31
31
  require_relative "bluebook/hecksagon"
32
+ require_relative "bluebook/translation_expression"
32
33
  require_relative "bluebook/translation"
34
+ require_relative "bluebook/translation_plan"
33
35
 
34
36
  require_relative "bluebook/assembly/contract"
35
37
  require_relative "bluebook/assembly/contracts"
@@ -5,8 +5,8 @@ require_relative "bluebook/meta_validator"
5
5
 
6
6
  module Hecks
7
7
  # The chapters the gem carries that a hecksagon can attach by name (ADR 0080): the language
8
- # declared in itself, Expression, Tenancy, Deploy, Site, Tickets and QualityControl. Framework
9
- # members stay in `Framework`; `table` and `attach!` find a name across both.
8
+ # declared in itself, Expression, Tenancy, Deploy, Site, Tickets, Composer and QualityControl.
9
+ # Framework members stay in `Framework`; `table` and `attach!` find a name across both.
10
10
  #
11
11
  # A chapter is named by the `Hecks.bluebook "Name"` header of its files, and may span several.
12
12
  # Beside its bluebook a chapter may carry what every hecksagon attaching it needs, whatever
@@ -16,7 +16,7 @@ module Hecks
16
16
  # Where attachable chapters live, relative to `lib/hecks/`.
17
17
  GLOBS = %w[language/**/*.bluebook grammar/expression.bluebook tenancy/bluebook/*.bluebook
18
18
  deploy/bluebook/*.bluebook site/bluebook/*.bluebook tickets/bluebook/*.bluebook
19
- quality_control/*.bluebook].freeze
19
+ composer/bluebook/*.bluebook quality_control/*.bluebook].freeze
20
20
 
21
21
  # Every attachable chapter, by name, with the files that declare it.
22
22
  #
@@ -36,7 +36,7 @@ module Hecks
36
36
  if MEMORY_COMMANDS.include?(ARGV.first.to_s.chomp("!"))
37
37
  why = begin
38
38
  require "json"
39
- JSON.parse(text).dig("state", "refusal", "value")
39
+ JSON.parse(text).dig("state", "refusal")
40
40
  rescue JSON::ParserError
41
41
  nil
42
42
  end
@@ -0,0 +1,3 @@
1
+ Hecks.adapter "ClaudeComposer" do
2
+ port "composer_agent"
3
+ end
@@ -0,0 +1,83 @@
1
+ require "json"
2
+ require "open3"
3
+ require "timeout"
4
+
5
+ require_relative "../agent"
6
+ require_relative "jev"
7
+
8
+ module Hecks
9
+ module Adapters
10
+ # Real `composer_agent` fulfillment for `draft` and `wire`: the developer's own `claude` run
11
+ # as the `hecks-composer` agent, which edits the workspace's files in place. Transport only;
12
+ # whether the edit is any good is the Composer's own check to judge.
13
+ #
14
+ # The agent's write guard confines it to domain files inside the git checkout, so the
15
+ # workspace must sit inside one.
16
+ module ClaudeComposer
17
+ TIMEOUT_SECONDS = 300
18
+
19
+ AGENT = "hecks-composer".freeze
20
+
21
+ DRAFT = "Edit the domain files in %<dir>s as the request below says. Change only what it asks " \
22
+ "for, write the bluebook first, then any hecksagon wiring it needs. Reply with one line " \
23
+ "saying what you changed.".freeze
24
+
25
+ WIRE = "Change how the chapter in %<dir>s is wired, as the request below says: edit only its " \
26
+ ".hecksagon and .world files, and bind adapters that already exist. Reply with one line " \
27
+ "saying what you changed.".freeze
28
+
29
+ module_function
30
+
31
+ # @param prompt [String] what the author asked for
32
+ # @param dir [String] the workspace directory
33
+ # @return [Hash] `"note"` the agent's account of what it changed
34
+ # @raise [Ports::Agent::Unavailable] see {Composer::Agent::Unavailable}
35
+ def draft(prompt:, dir:) = call(DRAFT, prompt, dir)
36
+
37
+ # @param prompt [String] what the author asked for
38
+ # @param dir [String] the workspace directory
39
+ # @return [Hash] `"note"` the agent's account of what it changed
40
+ def wire(prompt:, dir:) = call(WIRE, prompt, dir)
41
+
42
+ # Naming what a prompt asks for is the `jev` categorizer's work, which this adapter lends.
43
+ #
44
+ # @param prompt [String] what the author typed
45
+ # @return [Hash] `"intent"` one of {Composer::Agent::INTENTS}
46
+ # @raise [Composer::Agent::Unavailable] if `jev` cannot answer
47
+ def classify(prompt:) = Jev.classify(prompt: prompt)
48
+
49
+ # @api private
50
+ def call(task, prompt, dir)
51
+ root = checkout_of(dir)
52
+ message = "#{format(task, dir: dir)}\n\nRequest: #{prompt}"
53
+ stdout, status = run(root, message)
54
+ raise Composer::Agent::Unavailable, "claude exited #{status.exitstatus}: #{stdout}" unless status.success?
55
+
56
+ { "note" => JSON.parse(stdout).fetch("result").to_s.lines.first.to_s.strip }
57
+ rescue JSON::ParserError, KeyError => e
58
+ raise Composer::Agent::ValidationError, "claude's reply was not the expected envelope: #{e.message}"
59
+ end
60
+
61
+ # Argv spawn, never a shell, so a `claude` shell alias cannot apply.
62
+ # @api private
63
+ def run(root, message)
64
+ Timeout.timeout(TIMEOUT_SECONDS) do
65
+ Open3.capture2("claude", "-p", "--agent", AGENT, "--output-format", "json",
66
+ "--permission-mode", "acceptEdits", stdin_data: message, chdir: root)
67
+ end
68
+ rescue Timeout::Error
69
+ raise Composer::Agent::Unavailable, "claude did not answer within #{TIMEOUT_SECONDS}s"
70
+ rescue Errno::ENOENT => e
71
+ raise Composer::Agent::Unavailable, "claude is not on PATH: #{e.message}"
72
+ end
73
+
74
+ # @api private
75
+ def checkout_of(dir)
76
+ top, status = Open3.capture2("git", "-C", dir, "rev-parse", "--show-toplevel")
77
+ raise Composer::Agent::Unavailable, "#{dir} is not inside a git checkout" unless status.success?
78
+
79
+ top.strip
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,3 @@
1
+ Hecks.adapter "FakeAgent" do
2
+ port "composer_agent"
3
+ end
@@ -0,0 +1,55 @@
1
+ require_relative "../agent"
2
+
3
+ module Hecks
4
+ module Adapters
5
+ # A scripted `composer_agent`, for specs and for trying the editor with no model behind it.
6
+ # Each ask is recorded; `draft` and `wire` run a script that may rewrite the workspace's files,
7
+ # so a test sees the same thing the real agent leaves behind.
8
+ module FakeAgent
9
+ class << self
10
+ # @return [Array<Hash>] every ask, as `{ask:, prompt:, dir:}` in the order made
11
+ def calls = @calls ||= []
12
+
13
+ # @return [#call, nil] takes `(prompt, dir)` and edits files under `dir`; may return a note
14
+ attr_accessor :edit
15
+
16
+ # @return [#call, nil] takes `prompt` and answers an intent
17
+ attr_accessor :intent
18
+
19
+ # Forgets the calls and both scripts.
20
+ # @return [void]
21
+ def reset!
22
+ @calls = []
23
+ @edit = nil
24
+ @intent = nil
25
+ end
26
+ end
27
+
28
+ module_function
29
+
30
+ # @param prompt [String] what the author asked for
31
+ # @param dir [String] the workspace directory
32
+ # @return [Hash] `"note"` what the script reported
33
+ def draft(prompt:, dir:) = edited(:draft, prompt, dir)
34
+
35
+ # @param prompt [String] what the author asked for
36
+ # @param dir [String] the workspace directory
37
+ # @return [Hash] `"note"` what the script reported
38
+ def wire(prompt:, dir:) = edited(:wire, prompt, dir)
39
+
40
+ # @param prompt [String] what the author typed
41
+ # @return [Hash] `"intent"` what the script answered, `"edit"` without one
42
+ def classify(prompt:)
43
+ FakeAgent.calls << { ask: :classify, prompt: prompt, dir: nil }
44
+ { "intent" => FakeAgent.intent ? FakeAgent.intent.call(prompt) : "edit" }
45
+ end
46
+
47
+ # @api private
48
+ def edited(ask, prompt, dir)
49
+ FakeAgent.calls << { ask: ask, prompt: prompt, dir: dir }
50
+ note = FakeAgent.edit&.call(prompt, dir)
51
+ { "note" => note.is_a?(String) ? note : "changed nothing" }
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,3 @@
1
+ Hecks.adapter "Jev" do
2
+ port "composer_agent"
3
+ end
@@ -0,0 +1,66 @@
1
+ require "json"
2
+ require "open3"
3
+ require "tmpdir"
4
+
5
+ require_relative "../agent"
6
+
7
+ module Hecks
8
+ module Adapters
9
+ # `composer_agent` fulfillment for `classify` alone: the `jev` categorizer, which answers a
10
+ # multiple-choice question about a piece of text. It carries no key of its own; the script
11
+ # reads one from the environment or from 1Password, so nothing secret is written here.
12
+ #
13
+ # `draft` and `wire` are not this adapter's work.
14
+ module Jev
15
+ QUESTIONS = {
16
+ "intent" => {
17
+ "type" => "choice",
18
+ "instructions" => "What is the author of a domain bluebook asking an editor to do?",
19
+ "criteria" => {
20
+ "edit" => "change what the domain declares: an aggregate, command, attribute, rule or lifecycle",
21
+ "wire" => "change how the domain is wired to the outside: persistence, adapters, ports, deployment",
22
+ "question" => "explain or answer something about the domain, changing nothing",
23
+ "new_domain" => "start a new domain from a description of a business"
24
+ }
25
+ }
26
+ }.freeze
27
+
28
+ module_function
29
+
30
+ # @param prompt [String] what the author typed
31
+ # @return [Hash] `"intent"` one of {Composer::Agent::INTENTS}
32
+ # @raise [Composer::Agent::Unavailable] if `jev` is missing, fails or answers nothing
33
+ def classify(prompt:)
34
+ answers = ask(prompt)
35
+ { "intent" => choice(answers["intent"]) }
36
+ end
37
+
38
+ # @api private
39
+ def ask(prompt)
40
+ Dir.mktmpdir("hecks-jev") { |dir| JSON.parse(run(prompt, File.join(dir, "questions.json"))) }
41
+ rescue Errno::ENOENT => e
42
+ raise Composer::Agent::Unavailable, "jev is not on PATH: #{e.message}"
43
+ rescue JSON::ParserError => e
44
+ raise Composer::Agent::Unavailable, "jev's answer was not JSON: #{e.message}"
45
+ end
46
+
47
+ # Runs `jev` on the prompt, with the questions written to `path` beside it.
48
+ # @api private
49
+ def run(prompt, path)
50
+ File.write(path, JSON.generate(QUESTIONS))
51
+ out, err, status = Open3.capture3(ENV.fetch("HECKS_JEV", "jev"), path, "-", stdin_data: prompt)
52
+ return out if status.success?
53
+
54
+ raise Composer::Agent::Unavailable, "jev exited #{status.exitstatus}: #{err.lines.first}"
55
+ end
56
+
57
+ # `jev` names a choice by its key; accept the bare key or a row that carries it.
58
+ # @api private
59
+ def choice(answer)
60
+ return answer unless answer.is_a?(Hash)
61
+
62
+ answer.values_at("choice", "answer", "value", "label").compact.first
63
+ end
64
+ end
65
+ end
66
+ end