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
@@ -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
  #
@@ -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
@@ -0,0 +1,113 @@
1
+ require_relative "../runtime/registry"
2
+
3
+ module Hecks
4
+ module Composer
5
+ # The agent the Composer asks, on the hecksagon side: one port, `composer_agent`, resolved to
6
+ # the single adapter a registry holds for it. The caller never sees the adapter, only answers
7
+ # this module has checked, so a spec can stand a scripted adapter in for the real one.
8
+ #
9
+ # Two of the asks change the workspace on disk and answer with a note about what changed:
10
+ # `draft` edits a bluebook, `wire` edits the `.hecksagon` and `.world` files that bind it. The
11
+ # third, `classify`, only names what a prompt is asking for.
12
+ module Agent
13
+ NAME = "composer_agent".freeze
14
+ PORT = File.expand_path("ports/composer_agent.port", __dir__).freeze
15
+ ADAPTERS = File.expand_path("adapters", __dir__).freeze
16
+
17
+ # What a prompt can be asking for.
18
+ INTENTS = %w[edit wire question new_domain].freeze
19
+
20
+ # The adapter's answer arrived but was unusable; asking again will not help.
21
+ class ValidationError < StandardError
22
+ end
23
+
24
+ # Nothing answered (missing binary, dead subprocess, timeout); retry or fall back.
25
+ class Unavailable < StandardError
26
+ end
27
+
28
+ module_function
29
+
30
+ # Asks the agent to change a workspace's bluebooks as the prompt says.
31
+ #
32
+ # @param registry [Runtime::Registry] a registry holding the port and one adapter for it
33
+ # @param prompt [String] what the author asked for
34
+ # @param dir [String] the workspace directory, whose files the agent edits in place
35
+ # @return [String] the agent's one-line account of what it changed
36
+ # @raise [ValidationError] unless the answer is a Hash with a non-blank `"note"`
37
+ # @raise [Unavailable] if the adapter cannot answer
38
+ # @raise [Runtime::WiringError] unless exactly one adapter implements the port
39
+ def draft(registry, prompt:, dir:)
40
+ note(adapter(registry).draft(prompt: prompt, dir: dir))
41
+ end
42
+
43
+ # Asks the agent to change how a workspace's chapter is wired, never what it declares.
44
+ #
45
+ # @param registry [Runtime::Registry] a registry holding the port and one adapter for it
46
+ # @param prompt [String] what the author asked for
47
+ # @param dir [String] the workspace directory, whose `.hecksagon` and `.world` files the
48
+ # agent edits in place
49
+ # @return [String] the agent's one-line account of what it changed
50
+ # @raise [ValidationError] unless the answer is a Hash with a non-blank `"note"`
51
+ # @raise [Unavailable] if the adapter cannot answer
52
+ def wire(registry, prompt:, dir:)
53
+ note(adapter(registry).wire(prompt: prompt, dir: dir))
54
+ end
55
+
56
+ # Names what a prompt is asking for.
57
+ #
58
+ # @param registry [Runtime::Registry] a registry holding the port and one adapter for it
59
+ # @param prompt [String] what the author typed
60
+ # @return [String] one of `Composer::Agent::INTENTS`
61
+ # @raise [ValidationError] unless the answer is a Hash whose `"intent"` is one of those
62
+ # @raise [Unavailable] if the adapter cannot answer
63
+ def classify(registry, prompt:)
64
+ answer = adapter(registry).classify(prompt: prompt)
65
+ intent = answer["intent"] if answer.is_a?(Hash)
66
+ return intent if INTENTS.include?(intent)
67
+
68
+ raise ValidationError, "an intent is one of #{INTENTS.join(", ")}, got #{intent.inspect}"
69
+ end
70
+
71
+ # Builds a registry in which exactly one adapter implements the port.
72
+ #
73
+ # @param adapter_file [String] the adapter's `.adapter` declaration
74
+ # @return [Runtime::Registry] a registry holding the port and that adapter
75
+ def registry_with(adapter_file)
76
+ registry = Runtime::Registry.new
77
+ Hecks.with_registry(registry) do
78
+ Kernel.load(PORT)
79
+ Kernel.load(adapter_file)
80
+ end
81
+ registry
82
+ end
83
+
84
+ # Finds the single adapter bound to the port.
85
+ #
86
+ # @param registry [Runtime::Registry] the registry to search
87
+ # @return [Module] the adapter implementing the port
88
+ # @raise [Runtime::WiringError] if none, or more than one, implements the port
89
+ def adapter(registry)
90
+ implementations = registry.adapters.values.select { |found| found.port == NAME }
91
+ return registry.adapter_class(implementations.first.name) if implementations.size == 1
92
+
93
+ raise Runtime::WiringError, wiring_refusal(implementations)
94
+ end
95
+
96
+ # @api private
97
+ def note(answer)
98
+ text = answer["note"].to_s.strip if answer.is_a?(Hash)
99
+ return text unless text.nil? || text.empty?
100
+
101
+ raise ValidationError, "an agent's answer carries a non-blank \"note\", got #{answer.inspect}"
102
+ end
103
+
104
+ # @api private
105
+ def wiring_refusal(implementations)
106
+ return "no adapter implements the #{NAME} port — nothing can edit the draft" if implementations.empty?
107
+
108
+ names = implementations.map(&:name).sort.join(", ")
109
+ "#{implementations.size} adapters implement the #{NAME} port (#{names}) — the runtime will not choose for you"
110
+ end
111
+ end
112
+ end
113
+ end