hecks 3.13.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 (440) 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.bluebook +4 -4
  7. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  8. data/examples/pizzas/config.ru +22 -0
  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 +1 -7
  12. data/lib/hecks/bluebook/dsl/attribute_collector.rb +15 -5
  13. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  14. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  15. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  16. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  17. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  18. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  19. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  20. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  21. data/lib/hecks/bluebook/named_pattern.rb +33 -0
  22. data/lib/hecks/bluebook/translation.rb +18 -10
  23. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  24. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  25. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  26. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  27. data/lib/hecks/bluebook.rb +3 -0
  28. data/lib/hecks/chapters.rb +3 -3
  29. data/lib/hecks/cli/domain_stub.rb +1 -1
  30. data/lib/hecks/cli/interview_aggregate.rb +4 -4
  31. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  32. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  33. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  34. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  35. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  36. data/lib/hecks/composer/adapters/jev.rb +66 -0
  37. data/lib/hecks/composer/agent.rb +113 -0
  38. data/lib/hecks/composer/assistant.rb +110 -0
  39. data/lib/hecks/composer/behavior_run.rb +53 -0
  40. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  41. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  42. data/lib/hecks/composer/board.rb +65 -0
  43. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  44. data/lib/hecks/composer/board_edit/source.rb +84 -0
  45. data/lib/hecks/composer/board_edit.rb +144 -0
  46. data/lib/hecks/composer/check.rb +89 -0
  47. data/lib/hecks/composer/domains.rb +81 -0
  48. data/lib/hecks/composer/jobs.rb +134 -0
  49. data/lib/hecks/composer/layout.rb +37 -0
  50. data/lib/hecks/composer/live.rb +150 -0
  51. data/lib/hecks/composer/outline.rb +48 -0
  52. data/lib/hecks/composer/page.html +989 -0
  53. data/lib/hecks/composer/panels.rb +142 -0
  54. data/lib/hecks/composer/parse.rb +83 -0
  55. data/lib/hecks/composer/plain.rb +28 -0
  56. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  57. data/lib/hecks/composer/session.rb +141 -0
  58. data/lib/hecks/composer/toolbox.rb +166 -0
  59. data/lib/hecks/composer/web.rb +156 -0
  60. data/lib/hecks/composer/workbench.rb +51 -0
  61. data/lib/hecks/composer/workspace.rb +81 -0
  62. data/lib/hecks/composer.rb +17 -0
  63. data/lib/hecks/corpus/adapters.rb +22 -0
  64. data/lib/hecks/corpus/rust.rb +1 -1
  65. data/lib/hecks/corpus/worlds.rb +19 -0
  66. data/lib/hecks/corpus.rb +5 -0
  67. data/lib/hecks/deploy/bluebook/deploy.bluebook +34 -34
  68. data/lib/hecks/forms/app/commands.rb +2 -3
  69. data/lib/hecks/forms/app/queries.rb +2 -3
  70. data/lib/hecks/forms/app/records.rb +17 -5
  71. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  72. data/lib/hecks/forms/field_controls.rb +2 -2
  73. data/lib/hecks/forms/field_renderer.rb +1 -1
  74. data/lib/hecks/forms/field_shape.rb +36 -17
  75. data/lib/hecks/forms/index_renderer.rb +2 -1
  76. data/lib/hecks/forms/page.rb +1 -1
  77. data/lib/hecks/forms/params.rb +0 -13
  78. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  79. data/lib/hecks/forms/record_renderer.rb +11 -7
  80. data/lib/hecks/forms/rule_words.rb +100 -0
  81. data/lib/hecks/forms/value_object_shape.rb +2 -1
  82. data/lib/hecks/framework/bluebook/change.bluebook +2 -2
  83. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  84. data/lib/hecks/fuzzing/domain_generator/block_rendering.rb +1 -1
  85. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  86. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  87. data/lib/hecks/gate/stages.yml +25 -0
  88. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  89. data/lib/hecks/grammar/evolve.rb +0 -3
  90. data/lib/hecks/grammar/translation.bluebook +1 -1
  91. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  92. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  93. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  94. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  95. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  96. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  97. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  98. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  99. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  100. data/lib/hecks/hecks/codebase.behaviors +6 -0
  101. data/lib/hecks/hecks/codebase.bluebook +58 -37
  102. data/lib/hecks/hecks/composer.behaviors +68 -0
  103. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  104. data/lib/hecks/hecks/custodian.behaviors +0 -6
  105. data/lib/hecks/hecks/custodian.bluebook +15 -92
  106. data/lib/hecks/hecks/hecks.bluebook +2 -2
  107. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  108. data/lib/hecks/hecks/hecks.world +1 -1
  109. data/lib/hecks/hecks/rehearsal.bluebook +10 -16
  110. data/lib/hecks/hecks/tooling.bluebook +22 -22
  111. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  112. data/lib/hecks/kernel_capabilities.rb +1 -1
  113. data/lib/hecks/language/bluebook/aggregate.bluebook +1 -0
  114. data/lib/hecks/language/bluebook/command.bluebook +1 -0
  115. data/lib/hecks/language/bluebook/entity.bluebook +1 -0
  116. data/lib/hecks/language/bluebook/query.bluebook +1 -0
  117. data/lib/hecks/language/bluebook/shape.bluebook +8 -7
  118. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  119. data/lib/hecks/language/hecksagon/port_operation.bluebook +1 -0
  120. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  121. data/lib/hecks/ports/access_control.rb +2 -2
  122. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  123. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  124. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  125. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  126. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  127. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  128. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  129. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  130. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  131. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  132. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  133. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  134. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  135. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  136. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  137. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  138. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  139. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  140. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  141. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  142. data/lib/hecks/ports.rb +0 -1
  143. data/lib/hecks/projection_files.rb +3 -1
  144. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  145. data/lib/hecks/projections/glossary/entries.rb +10 -4
  146. data/lib/hecks/projections/glossary/groups.rb +42 -0
  147. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  148. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  149. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  150. data/lib/hecks/projections/glossary/html.rb +1 -2
  151. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  152. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  153. data/lib/hecks/projections/glossary/page.css +143 -153
  154. data/lib/hecks/projections/glossary/page.js +63 -3
  155. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  156. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  157. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  158. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  159. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  160. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  161. data/lib/hecks/projections/glossary.rb +2 -1
  162. data/lib/hecks/projections/plain_english.rb +97 -0
  163. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  164. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  165. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  166. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  167. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  168. data/lib/hecks/projections/statements/actions.rb +99 -0
  169. data/lib/hecks/projections/statements/relationships.rb +86 -0
  170. data/lib/hecks/projections/statements.rb +15 -24
  171. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  172. data/lib/hecks/projector/exporter/translations.rb +18 -35
  173. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  174. data/lib/hecks/projector/exporter.rb +5 -1
  175. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  176. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  177. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  178. data/lib/hecks/quality_control/quality_control.bluebook +51 -51
  179. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  180. data/lib/hecks/runtime/outbox.rb +2 -9
  181. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  182. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  183. data/lib/hecks/runtime/value.rb +13 -0
  184. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  185. data/lib/hecks/rust_build/wasm.rb +0 -12
  186. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  187. data/lib/hecks/rust_format.rb +77 -0
  188. data/lib/hecks/site/bluebook/site.bluebook +5 -5
  189. data/lib/hecks/sme/bluebook/sme.bluebook +12 -12
  190. data/lib/hecks/tickets/bluebook/tickets.bluebook +3 -3
  191. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  192. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  193. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  194. data/lib/hecks/tools/translation_audit.rb +8 -10
  195. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  196. data/lib/hecks/version.rb +1 -1
  197. data/lib/hecks/vocabulary.rb +2 -1
  198. data/lib/hecks.rb +3 -0
  199. data/prebuilt/{58208399bada10b0db99928c48e38789288fc58f7b71a72b21bfa386680e32ed.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
  200. data/prebuilt/{2b59737fbdbd5f44edda0fddce2611f45d76087b3a7c782d1f1a17ea58c77948.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
  201. data/prebuilt/verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json +1 -0
  202. data/rust/Cargo.toml +40 -0
  203. data/rust/build/Cargo.toml +36 -0
  204. data/rust/build/clippy.toml +4 -0
  205. data/rust/build/src/adapters_pass.rs +196 -0
  206. data/rust/build/src/binding_pass.rs +54 -13
  207. data/rust/build/src/build_artifact.rs +23 -5
  208. data/rust/build/src/cargo_sync.rs +69 -26
  209. data/rust/build/src/lineage_pass.rs +101 -87
  210. data/rust/build/src/main.rs +11 -2
  211. data/rust/build/src/pipeline.rs +80 -19
  212. data/rust/build/src/reserved_names.rs +6 -50
  213. data/rust/build/src/resolve.rs +82 -35
  214. data/rust/build/src/rustfmt.rs +100 -0
  215. data/rust/build/src/seams_pass.rs +85 -16
  216. data/rust/build/src/subprocess.rs +30 -6
  217. data/rust/build/src/tmp.rs +5 -2
  218. data/rust/build/src/translation_pass.rs +72 -22
  219. data/rust/build/src/world_pass.rs +109 -0
  220. data/rust/clippy.toml +4 -0
  221. data/rust/codegen/Cargo.toml +36 -0
  222. data/rust/codegen/clippy.toml +4 -0
  223. data/rust/codegen/src/asks.rs +40 -14
  224. data/rust/codegen/src/attr.rs +2 -2
  225. data/rust/codegen/src/bridging.rs +676 -117
  226. data/rust/codegen/src/commands.rs +979 -373
  227. data/rust/codegen/src/constraints.rs +88 -25
  228. data/rust/codegen/src/dependency_planning.rs +128 -34
  229. data/rust/codegen/src/domain_generator.rs +300 -170
  230. data/rust/codegen/src/exemplar.rs +87 -32
  231. data/rust/codegen/src/expr_emitter.rs +54 -6
  232. data/rust/codegen/src/fielded.rs +193 -41
  233. data/rust/codegen/src/hecks_naming.rs +124 -16
  234. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  235. data/rust/codegen/src/json.rs +80 -44
  236. data/rust/codegen/src/json_codec.rs +585 -149
  237. data/rust/codegen/src/literal.rs +104 -138
  238. data/rust/codegen/src/main.rs +145 -52
  239. data/rust/codegen/src/manifest.rs +76 -14
  240. data/rust/codegen/src/mutations.rs +1212 -239
  241. data/rust/codegen/src/naming.rs +111 -60
  242. data/rust/codegen/src/optional_pass.rs +90 -28
  243. data/rust/codegen/src/ports.rs +5 -4
  244. data/rust/codegen/src/prelude.rs +139 -49
  245. data/rust/codegen/src/queries.rs +601 -226
  246. data/rust/codegen/src/reactions.rs +268 -74
  247. data/rust/codegen/src/read_models.rs +606 -205
  248. data/rust/codegen/src/reference_specs.rs +16 -3
  249. data/rust/codegen/src/registry.rs +128 -79
  250. data/rust/codegen/src/reserved_names.rs +6 -50
  251. data/rust/codegen/src/shared.rs +71 -22
  252. data/rust/codegen/src/sidecars.rs +15 -5
  253. data/rust/codegen/src/skip_reason.rs +8 -2
  254. data/rust/codegen/src/types.rs +306 -108
  255. data/rust/host/Cargo.lock +1 -0
  256. data/rust/host/Cargo.toml +40 -4
  257. data/rust/host/HECKS_RELEASE +1 -1
  258. data/rust/host/clippy.toml +4 -0
  259. data/rust/host/src/api.rs +698 -218
  260. data/rust/host/src/auth.rs +491 -214
  261. data/rust/host/src/b64.rs +91 -0
  262. data/rust/host/src/bin/expr_harness.rs +6 -2
  263. data/rust/host/src/bin/lineage_harness.rs +47 -11
  264. data/rust/host/src/bin/mint_harness.rs +70 -37
  265. data/rust/host/src/bin/validate_harness.rs +16 -8
  266. data/rust/host/src/boot.rs +144 -68
  267. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  268. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  269. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  270. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  271. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  272. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  273. data/rust/host/src/checkout.rs +78 -32
  274. data/rust/host/src/clock.rs +8 -0
  275. data/rust/host/src/commerce_ir.rs +333 -95
  276. data/rust/host/src/crypto.rs +33 -0
  277. data/rust/host/src/dispatch.rs +990 -331
  278. data/rust/host/src/expr_json.rs +1021 -273
  279. data/rust/host/src/extension.rs +24 -4
  280. data/rust/host/src/field_hints.rs +11 -7
  281. data/rust/host/src/fuzz_support.rs +85 -13
  282. data/rust/host/src/host.rs +100 -0
  283. data/rust/host/src/http.rs +22 -0
  284. data/rust/host/src/ir.rs +326 -16
  285. data/rust/host/src/journal.rs +277 -176
  286. data/rust/host/src/lambda_client.rs +219 -53
  287. data/rust/host/src/lib.rs +16 -4
  288. data/rust/host/src/log.rs +62 -11
  289. data/rust/host/src/mint.rs +1153 -326
  290. data/rust/host/src/needs.rs +185 -44
  291. data/rust/host/src/payments/keystore.rs +197 -45
  292. data/rust/host/src/payments/roles.rs +191 -0
  293. data/rust/host/src/payments/tests.rs +1445 -275
  294. data/rust/host/src/payments.rs +409 -121
  295. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  296. data/rust/host/src/platform_signin/config.rs +11 -11
  297. data/rust/host/src/platform_signin/cookie.rs +2 -2
  298. data/rust/host/src/platform_signin/extension.rs +2 -1
  299. data/rust/host/src/platform_signin/handler.rs +9 -8
  300. data/rust/host/src/platform_signin/jti.rs +4 -2
  301. data/rust/host/src/platform_signin/jwks.rs +2 -1
  302. data/rust/host/src/platform_signin/jws.rs +2 -37
  303. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  304. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  305. data/rust/host/src/platform_signin/pages.rs +20 -20
  306. data/rust/host/src/platform_signin/session.rs +8 -1
  307. data/rust/host/src/platform_signin/settings.rs +6 -6
  308. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  309. data/rust/host/src/platform_signin/verify.rs +41 -41
  310. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  311. data/rust/host/src/presentation.rs +207 -63
  312. data/rust/host/src/presentation_write.rs +502 -187
  313. data/rust/host/src/query_step.rs +69 -18
  314. data/rust/host/src/rate_limit.rs +541 -142
  315. data/rust/host/src/reference_transform.rs +77 -25
  316. data/rust/host/src/reference_validate.rs +443 -104
  317. data/rust/host/src/resend.rs +190 -55
  318. data/rust/host/src/secrets.rs +116 -21
  319. data/rust/host/src/server.rs +483 -104
  320. data/rust/host/src/storage_shape.rs +88 -21
  321. data/rust/host/src/test_pg.rs +17 -7
  322. data/rust/host/src/ui_schema.rs +495 -207
  323. data/rust/host/src/wasm_runner.rs +21 -6
  324. data/rust/host/src/web/commerce.rs +101 -23
  325. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  326. data/rust/host/src/web/newsletter/template.rs +26 -7
  327. data/rust/host/src/web/newsletter/tests.rs +112 -23
  328. data/rust/host/src/web/newsletter.rs +209 -59
  329. data/rust/host/src/web/newsletter_send.rs +251 -79
  330. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  331. data/rust/host/src/web/registration_receipt.rs +100 -24
  332. data/rust/host/src/web/registrations.rs +1384 -332
  333. data/rust/host/src/web/resend_webhook.rs +138 -80
  334. data/rust/host/src/web.rs +2416 -508
  335. data/rust/lsp/Cargo.toml +36 -0
  336. data/rust/lsp/clippy.toml +4 -0
  337. data/rust/lsp/src/json.rs +29 -22
  338. data/rust/lsp/src/main.rs +81 -31
  339. data/rust/lsp/src/outline.rs +23 -7
  340. data/rust/lsp/src/rpc.rs +17 -4
  341. data/rust/parser/Cargo.toml +36 -0
  342. data/rust/parser/clippy.toml +4 -0
  343. data/rust/parser/src/build/closed_sets.rs +6 -1
  344. data/rust/parser/src/build/identity.rs +4 -3
  345. data/rust/parser/src/build/mod.rs +1 -0
  346. data/rust/parser/src/build/named_pattern.rs +26 -0
  347. data/rust/parser/src/build/naming.rs +2 -80
  348. data/rust/parser/src/build/pattern_subset.rs +6 -8
  349. data/rust/parser/src/build/query_derive.rs +3 -1
  350. data/rust/parser/src/build/query_inference.rs +14 -5
  351. data/rust/parser/src/build/read_model.rs +4 -1
  352. data/rust/parser/src/build/references.rs +4 -3
  353. data/rust/parser/src/canonical.rs +34 -15
  354. data/rust/parser/src/emit.rs +187 -105
  355. data/rust/parser/src/expr/ast_json.rs +134 -42
  356. data/rust/parser/src/expr/evaluator.rs +27 -9
  357. data/rust/parser/src/expr/mod.rs +54 -13
  358. data/rust/parser/src/expr/resolver.rs +299 -92
  359. data/rust/parser/src/ir.rs +35 -3
  360. data/rust/parser/src/keywords.rs +7168 -510
  361. data/rust/parser/src/lex.rs +11 -25
  362. data/rust/parser/src/lib.rs +1 -0
  363. data/rust/parser/src/main.rs +65 -6
  364. data/rust/parser/src/parse/adapter.rs +157 -0
  365. data/rust/parser/src/parse/aggregate.rs +19 -10
  366. data/rust/parser/src/parse/chapter.rs +16 -17
  367. data/rust/parser/src/parse/command.rs +35 -16
  368. data/rust/parser/src/parse/domain_port.rs +4 -7
  369. data/rust/parser/src/parse/entity.rs +21 -11
  370. data/rust/parser/src/parse/hecksagon.rs +10 -4
  371. data/rust/parser/src/parse/lifecycle.rs +9 -5
  372. data/rust/parser/src/parse/mod.rs +43 -91
  373. data/rust/parser/src/parse/needs.rs +21 -12
  374. data/rust/parser/src/parse/policy.rs +2 -2
  375. data/rust/parser/src/parse/process_manager.rs +9 -11
  376. data/rust/parser/src/parse/query.rs +8 -4
  377. data/rust/parser/src/parse/read_model.rs +57 -16
  378. data/rust/parser/src/parse/value_object.rs +7 -2
  379. data/rust/parser/src/parse/world.rs +322 -0
  380. data/rust/parser/src/ruby_value.rs +7 -6
  381. data/rust/src/exemplar/json.rs +4 -4
  382. data/rust/src/exemplar/mutations.rs +18 -0
  383. data/rust/src/exemplar/read_models.rs +1 -10
  384. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  385. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  386. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  387. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  388. data/rust/src/kernel/cli.rs +229 -112
  389. data/rust/src/kernel/dispatch.rs +323 -115
  390. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  391. data/rust/src/kernel/expr.rs +203 -86
  392. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  393. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  394. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  395. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  396. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  397. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  398. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  399. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  400. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  401. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  402. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  403. data/rust/src/kernel/expression_operators/text.rs +120 -27
  404. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  405. data/rust/src/kernel/json.rs +230 -148
  406. data/rust/src/kernel/mod.rs +46 -33
  407. data/rust/src/kernel/named_query.rs +63 -15
  408. data/rust/src/kernel/needs.rs +112 -25
  409. data/rust/src/kernel/orchestrate.rs +980 -581
  410. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  411. data/rust/src/kernel/pattern.rs +107 -34
  412. data/rust/src/kernel/query_comparators.rs +118 -41
  413. data/rust/src/kernel/query_ordering.rs +13 -5
  414. data/rust/src/kernel/read_model.rs +479 -181
  415. data/rust/src/kernel/reference_lookup.rs +122 -26
  416. data/rust/src/kernel/repository.rs +293 -66
  417. data/rust/src/kernel/routing.rs +58 -42
  418. data/rust/src/kernel/ruby_float.rs +139 -0
  419. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  420. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  421. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  422. data/rust/src/kernel/vocab/mod.rs +6 -6
  423. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  424. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  425. data/rust/src/lib.rs +50 -0
  426. data/rust/src/main.rs +4 -1
  427. data/rust/web/Cargo.toml +36 -0
  428. data/rust/web/clippy.toml +4 -0
  429. metadata +85 -15
  430. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  431. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  432. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  433. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  434. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  435. data/lib/hecks/ports/authentication.port +0 -6
  436. data/lib/hecks/ports/authentication.rb +0 -67
  437. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  438. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  439. data/prebuilt/verdicts-48318ca172edd21443b1ad02801c697fff100b746e828f09db987647641a5df2.json +0 -1
  440. data/rust/host/src/approval.rs +0 -652
@@ -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
@@ -0,0 +1,110 @@
1
+ require_relative "agent"
2
+ require_relative "check"
3
+ require_relative "layout"
4
+ require_relative "session"
5
+
6
+ module Hecks
7
+ module Composer
8
+ # Carries one request from the author to the agent and back, on the hecksagon side: records it
9
+ # in the draft's conversation, has the prompt classified, asks the agent to change the
10
+ # workspace's files, judges what it left, asks once more when the check refused the result, and
11
+ # records the agent's reply and the verdict.
12
+ #
13
+ # The agent edits the workspace on disk, so the files it changes are read before and after to
14
+ # show the author what moved.
15
+ class Assistant
16
+ # How many times the agent may be asked about one request: the request, then a repair.
17
+ ROUNDS = 2
18
+
19
+ QUESTION_NOTE = "That reads as a question, so nothing was edited.".freeze
20
+
21
+ # @param workspace [String] the domain directory the agent edits
22
+ # @param agents [Runtime::Registry] a registry holding the `composer_agent` port and one
23
+ # adapter
24
+ # @param session [Session] the editor's in-memory draft bookkeeping
25
+ def initialize(workspace:, agents:, session:)
26
+ @workspace = workspace
27
+ @agents = agents
28
+ @session = session
29
+ end
30
+
31
+ # @param prompt [String] what the author asked for
32
+ # @return [Hash] `intent`, `note` (the agent's one-line account), `rounds` (how many times it
33
+ # was asked), `changed` (`{path => {before:, after:}}` for each file that differs), `check`
34
+ # (the whole check on what is now on disk) and `layout` (the panels to open, see {Layout})
35
+ def ask(prompt)
36
+ slug = File.basename(@workspace)
37
+ before = snapshot
38
+ open_draft(slug, before)
39
+ @session.ask(slug: slug, said: prompt)
40
+ intent = classify(prompt)
41
+ note, rounds = edit(intent, prompt)
42
+ outcome(slug, intent, note, rounds, before)
43
+ end
44
+
45
+ private
46
+
47
+ def open_draft(slug, files)
48
+ chapter = Parse.bluebooks(@workspace).keys.first || "Workspace"
49
+ @session.open(slug: slug, chapter_name: chapter, text: files.values.first.to_s)
50
+ end
51
+
52
+ # The prompt's intent; a classifier that cannot answer leaves it an edit.
53
+ def classify(prompt)
54
+ Agent.classify(@agents, prompt: prompt)
55
+ rescue Agent::Unavailable
56
+ "edit"
57
+ end
58
+
59
+ def edit(intent, prompt)
60
+ return [QUESTION_NOTE, 0] if intent == "question"
61
+
62
+ note = ask_agent(intent, prompt)
63
+ refusal = Check.call(dir: @workspace)[:diagnostic]
64
+ return [note, 1] unless refusal
65
+
66
+ [ask_agent(intent, repair(prompt, refusal)), ROUNDS]
67
+ end
68
+
69
+ def ask_agent(intent, prompt)
70
+ return Agent.wire(@agents, prompt: prompt, dir: @workspace) if intent == "wire"
71
+
72
+ Agent.draft(@agents, prompt: prompt, dir: @workspace)
73
+ end
74
+
75
+ def repair(prompt, refusal)
76
+ where = refusal[:line] ? "#{refusal[:file]} line #{refusal[:line]}: " : ""
77
+ expected = refusal[:expected].to_a.empty? ? "" : " (expected one of: #{refusal[:expected].join(", ")})"
78
+ "#{prompt}\n\nYour last edit was refused by the check, so fix it: #{where}#{refusal[:message]}#{expected}"
79
+ end
80
+
81
+ def outcome(slug, intent, note, rounds, before)
82
+ after = snapshot
83
+ check = Check.call(dir: @workspace)
84
+ @session.answer(slug: slug, said: note, text: after.values.first.to_s)
85
+ @session.judge(slug: slug, diagnostic: check[:diagnostic])
86
+ moved = changed(before, after)
87
+ record(moved)
88
+ { intent: intent, note: note, rounds: rounds, changed: moved, check: check,
89
+ layout: Layout.propose(intent: intent, check: check, changed: moved) }
90
+ end
91
+
92
+ # Keeps each file the agent changed in the timeline, as a save would.
93
+ def record(moved)
94
+ moved.each { |path, text| @session.record(path: path, text: text[:after]) if text[:after] }
95
+ end
96
+
97
+ def changed(before, after)
98
+ (before.keys | after.keys).sort.each_with_object({}) do |path, moved|
99
+ next if before[path] == after[path]
100
+
101
+ moved[path] = { before: before[path], after: after[path] }
102
+ end
103
+ end
104
+
105
+ def snapshot
106
+ Workspace.files(@workspace).to_h { |path| [path, File.read(File.join(@workspace, path))] }
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,53 @@
1
+ require "hecks"
2
+ require "hecks/behaviors"
3
+ require_relative "workspace"
4
+
5
+ module Hecks
6
+ module Composer
7
+ # Runs a workspace's `.behaviors` files as they stand on screen and reports each test as plain
8
+ # data. Like the other composer files it sits on the hecksagon side: it stages the text, calls
9
+ # the behaviors runner and changes nothing on disk.
10
+ module BehaviorRun
11
+ module_function
12
+
13
+ # @param dir [String] the workspace directory
14
+ # @param buffers [Hash{String => String}] unsaved text by path relative to `dir`
15
+ # @return [Hash] `files` (`{path:, parse_error:, runs: [{status:, description:, message:}]}`
16
+ # per behaviors file, paths relative to the workspace, status "pass", "fail" or "error")
17
+ # and `summary` (`total`, `passed`, `failed`, `errored` and `parse_errors` counts)
18
+ # @raise [Workspace::OutsideWorkspace] if a buffer's path leaves the workspace
19
+ def call(dir:, buffers: {})
20
+ Workspace.stage(dir: dir, buffers: buffers) do |stage|
21
+ files = Hecks::Behaviors.run_all(stage).files.map { |file| report(stage, file) }
22
+ { files: files, summary: tally(files) }
23
+ end
24
+ end
25
+
26
+ # @api private
27
+ def report(stage, file)
28
+ {
29
+ path: file.path.to_s.delete_prefix("#{File.realpath(stage)}/").delete_prefix("#{stage}/"),
30
+ parse_error: file.parse_error,
31
+ runs: file.runs.map { |run| run_hash(run) }
32
+ }
33
+ end
34
+
35
+ # @api private
36
+ def run_hash(run)
37
+ { status: run.status.to_s, description: run.description, message: run.message }
38
+ end
39
+
40
+ # @api private
41
+ def tally(files)
42
+ runs = files.flat_map { |file| file[:runs] }
43
+ {
44
+ total: runs.size,
45
+ passed: runs.count { |run| run[:status] == "pass" },
46
+ failed: runs.count { |run| run[:status] == "fail" },
47
+ errored: runs.count { |run| run[:status] == "error" },
48
+ parse_errors: files.count { |file| file[:parse_error] }
49
+ }
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,178 @@
1
+ # The Composer chapter: a bluebook being written in an editor, by hand or by asking an agent.
2
+ #
3
+ # A Draft is the editor's working copy of one chapter: the text on screen, the conversation that
4
+ # shaped it, and whether the last check of it passed. The files on disk stay the truth; a Draft
5
+ # only holds what is being changed and what was said about it. The check itself (parse, load,
6
+ # project, model-check) and the agent that rewrites the text are the outside, reached through the
7
+ # Composer's hecksagon, so nothing here knows how either is done.
8
+ Hecks.bluebook "Composer" do
9
+ vision "Write a bluebook in an editor and watch the domain draw itself, with an agent that can change the draft when asked."
10
+
11
+ aggregate "Draft" do
12
+ description "One chapter being edited: its text, the conversation about it, and whether its last check passed."
13
+
14
+ attribute :slug, DraftSlug
15
+ attribute :chapter_name, ChapterName
16
+ attribute :text, SourceText
17
+ attribute :note, Note, optional: true
18
+ attribute :turns, list_of(Turn)
19
+
20
+ identified_by :slug
21
+
22
+ value_object "DraftSlug" do
23
+ attribute :value, String, pattern: '[^ \t\n\r]'
24
+
25
+ invariant("a draft is named") { !value.to_s.empty? }
26
+ end
27
+
28
+ # The name a chapter's `Hecks.bluebook "Name"` header declares, which is also the name the
29
+ # check loads it under.
30
+ value_object "ChapterName" do
31
+ attribute :value, String, pattern: '[A-Z][A-Za-z0-9]*'
32
+
33
+ invariant("a chapter is named") { !value.to_s.empty? }
34
+ end
35
+
36
+ # What is on screen. A draft may be empty or half-written; the check judges it, not this.
37
+ value_object "SourceText" do
38
+ attribute :value, String
39
+ end
40
+
41
+ # The check's last word on the draft, or the agent's one-line account of what it changed.
42
+ value_object "Note" do
43
+ attribute :value, String
44
+ end
45
+
46
+ value_object "TurnSequence" do
47
+ attribute :value, Integer
48
+
49
+ invariant("a turn sequence is positive") { value.positive? }
50
+ end
51
+
52
+ value_object "Speaker" do
53
+ attribute :value, String, one_of: ["author", "agent"]
54
+ end
55
+
56
+ value_object "Utterance" do
57
+ attribute :value, String
58
+
59
+ invariant("a turn says something") { !value.to_s.empty? }
60
+ end
61
+
62
+ # The sequence is supplied by the caller, who already knows how many turns are on screen, so
63
+ # the order of a conversation is not a fact this chapter invents on write.
64
+ entity "Turn" do
65
+ description "One thing said in the conversation about the draft, by the author or by the agent."
66
+
67
+ attribute :sequence, TurnSequence
68
+
69
+ identified_by :sequence
70
+
71
+ attribute :speaker, Speaker
72
+ attribute :said, Utterance
73
+ end
74
+
75
+ lifecycle :status, default: "drafting" do
76
+ transition "Accept" => "valid", from: %w[drafting valid invalid]
77
+ transition "Reject" => "invalid", from: %w[drafting valid invalid]
78
+ transition "Revise" => "drafting", from: %w[drafting valid invalid]
79
+ end
80
+
81
+ command "Begin" do
82
+ role "Author"
83
+ goal "Open a chapter for editing, with the text it starts from"
84
+
85
+ attribute :slug, DraftSlug
86
+ attribute :chapter_name, ChapterName
87
+ attribute :text, SourceText
88
+
89
+ sets :slug
90
+ sets :chapter_name
91
+ sets :text
92
+
93
+ emits "DraftBegun"
94
+ end
95
+
96
+ command "Revise" do
97
+ role "Author"
98
+ goal "Replace the draft's text with what is now on screen"
99
+
100
+ reference_to Draft
101
+ attribute :text, SourceText
102
+
103
+ sets :text
104
+
105
+ emits "DraftRevised"
106
+ end
107
+
108
+ command "Accept" do
109
+ role "System"
110
+ goal "Record that the draft's text passed its check"
111
+
112
+ reference_to Draft
113
+ attribute :note, Note
114
+
115
+ sets :note
116
+
117
+ emits "DraftAccepted"
118
+ end
119
+
120
+ command "Reject" do
121
+ role "System"
122
+ goal "Record that the draft's text failed its check, and why"
123
+
124
+ reference_to Draft
125
+ attribute :note, Note
126
+
127
+ sets :note
128
+
129
+ emits "DraftRejected"
130
+ end
131
+
132
+ command "Ask" do
133
+ role "Author"
134
+ goal "Put a request for the agent to the draft's conversation"
135
+
136
+ reference_to Draft
137
+ attribute :sequence, TurnSequence
138
+ attribute :said, Utterance
139
+
140
+ sets :turns, append: { sequence: :sequence, speaker: { value: "author" }, said: :said }
141
+
142
+ emits "TurnAsked"
143
+ end
144
+
145
+ command "Answer" do
146
+ role "System"
147
+ goal "Record the agent's reply and the text it left the draft with"
148
+
149
+ reference_to Draft
150
+ attribute :sequence, TurnSequence
151
+ attribute :said, Utterance
152
+ attribute :text, SourceText
153
+
154
+ sets :text
155
+ sets :turns, append: { sequence: :sequence, speaker: { value: "agent" }, said: :said }
156
+
157
+ emits "TurnAnswered"
158
+ end
159
+
160
+ query "Drafting" do
161
+ description "Every draft still being edited."
162
+ where(status: "drafting")
163
+ order_by :slug
164
+ end
165
+
166
+ query "Valid" do
167
+ description "Every draft whose last check passed."
168
+ where(status: "valid")
169
+ order_by :slug
170
+ end
171
+
172
+ query "Invalid" do
173
+ description "Every draft whose last check failed."
174
+ where(status: "invalid")
175
+ order_by :slug
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,16 @@
1
+ # The Composer domain, bound so it boots standing alone (a fuzz or model_check boot of this
2
+ # directory). Drafts are working state; the files on disk are the truth, so nothing here outlives
3
+ # the process.
4
+ Hecks.hecksagon "Composer" do
5
+ attaches "Governance"
6
+ persisted_by "Memory"
7
+ # The editor reaches this domain through Web, and every tool that boots a corpus domain (fuzz,
8
+ # model_check, the Composer's own Session) through Ruby; the launcher, the forms and MCP do not.
9
+ driven_by "Web"
10
+ driven_by "Ruby"
11
+ end
12
+
13
+ # The anti-corruption layer for Governance, which the chapter's roles are checked against.
14
+ Hecks.hecksagon "Governance" do
15
+ persisted_by "Memory"
16
+ end
@@ -0,0 +1,65 @@
1
+ module Hecks
2
+ module Composer
3
+ # The event-storming board of a chapter, on the hecksagon side: its aggregates with their
4
+ # commands, the events each command emits, and the policies that make one event trigger another
5
+ # command. It is read off the booted chapter, so it always shows what the text declares; the
6
+ # board's own edits go through {BoardEdit}, which rewrites the text.
7
+ module Board
8
+ module_function
9
+
10
+ # @param chapter [Bluebook::Chapter] the booted chapter
11
+ # @param outline [Array<Hash>] the text outline (see {Outline}), which says where each note
12
+ # is declared
13
+ # @return [Hash] `aggregates` (each with `name`, `at` and its `commands`, each command with
14
+ # `name`, `role`, `goal`, `emits` and `at`) and `policies` (`name`, `on`, `trigger`, `at`),
15
+ # where `at` is `{file:, line:}` or nil, and `on` and `trigger` read `Aggregate.Name`
16
+ def call(chapter, outline:)
17
+ places = Places.new(outline)
18
+ { aggregates: chapter.aggregates.map { |aggregate| aggregate_note(aggregate, places) },
19
+ policies: chapter.policies.map { |policy| policy_note(policy, places) } }
20
+ end
21
+
22
+ # @api private
23
+ def aggregate_note(aggregate, places)
24
+ name = aggregate.hecks_name
25
+ { name: name, at: places.of("aggregate", name),
26
+ commands: aggregate.commands.map { |command| command_note(name, command, places) } }
27
+ end
28
+
29
+ # @api private
30
+ def command_note(owner, command, places)
31
+ name = command.hecks_name
32
+ { name: name, role: command.role, goal: command.goal, emits: Array(command.emits).map(&:to_s),
33
+ at: places.of("command", name, owner: owner) }
34
+ end
35
+
36
+ # @api private
37
+ def policy_note(policy, places)
38
+ { name: policy.name, on: policy.on_event, trigger: policy.trigger_command,
39
+ at: places.of("policy", policy.name) }
40
+ end
41
+
42
+ # Where each declaration sits in the text, found by kind and name, and for a command by the
43
+ # aggregate that holds it.
44
+ class Places
45
+ # @param outline [Array<Hash>] see {Outline}
46
+ def initialize(outline)
47
+ owner = nil
48
+ @entries = outline.map do |entry|
49
+ owner = entry[:name] if entry[:kind] == "aggregate"
50
+ entry.merge(owner: entry[:kind] == "command" ? owner : nil)
51
+ end
52
+ end
53
+
54
+ # @param kind [String] `aggregate`, `command` or `policy`
55
+ # @param name [String] the declared name
56
+ # @param owner [String, nil] the aggregate, for a command
57
+ # @return [Hash, nil] `{file:, line:}` of the first match, or nil when the text has none
58
+ def of(kind, name, owner: nil)
59
+ found = @entries.find { |e| e[:kind] == kind && e[:name] == name && e[:owner] == owner }
60
+ found && { file: found[:file], line: found[:line] }
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,39 @@
1
+ module Hecks
2
+ module Composer
3
+ module BoardEdit
4
+ # The text of a command the board adds. The first command of an aggregate creates it, setting
5
+ # the key the aggregate is known by; every later one acts on a record of it. A command with an
6
+ # event says so on a line of its own.
7
+ module CommandText
8
+ module_function
9
+
10
+ # @param block [Array<String>] the lines of the aggregate's block, after its declaration
11
+ # @param indent [Integer] how far the aggregate's declaration is indented
12
+ # @param aggregate [String] the aggregate's name
13
+ # @param name [String] the new command's name
14
+ # @param event [String] the event it emits, or an empty string for none
15
+ # @return [Array<String>] the command's lines, indented one step inside the aggregate
16
+ # @raise [Refused] if the aggregate's key cannot be read, for a command that must set it
17
+ def lines(block:, indent:, aggregate:, name:, event:)
18
+ pad = " " * (indent + 2)
19
+ body = acts_on_a_record?(block, indent) ? ["reference_to #{aggregate}"] : creates(block, aggregate)
20
+ inner = [%(goal "Describe #{name}."), *body, (event.empty? ? nil : "emits #{event}")].compact
21
+ ["", %(#{pad}command "#{name}" do), *inner.map { |line| "#{pad} #{line}" }, "#{pad}end"]
22
+ end
23
+
24
+ # @api private
25
+ def acts_on_a_record?(block, indent) = block.any? { |line| line.match?(/\A {#{indent + 2}}command\s+"/) }
26
+
27
+ # The attribute and `sets` that give a new record its key.
28
+ # @api private
29
+ def creates(block, aggregate)
30
+ field = block.filter_map { |line| line[/\A\s*identified_by\s+:(\w+)\s*\z/, 1] }.first
31
+ type = field && block.filter_map { |line| line[/\A\s*attribute\s+:#{field},\s*([A-Za-z:]+)/, 1] }.first
32
+ raise Refused, "#{aggregate} has no key the board can read; write its first command in the text" unless type
33
+
34
+ ["attribute :#{field}, #{type}", "sets :#{field}"]
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end