hecks 3.14.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (427) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/adapters/fake_processor.rb +34 -0
  3. data/examples/pizzas/adapters/payment_signature.rb +66 -0
  4. data/examples/pizzas/adapters/payment_webhook.rb +87 -0
  5. data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
  6. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  7. data/examples/pizzas/config.ru +22 -0
  8. data/exe/hecks +1 -1
  9. data/lib/hecks/adapters/driven/prism.rb +0 -2
  10. data/lib/hecks/adapters/driven.rb +0 -2
  11. data/lib/hecks/adapters/driving/json.rb +9 -16
  12. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  13. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  14. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  15. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  16. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  17. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  18. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  19. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  20. data/lib/hecks/bluebook/translation.rb +18 -10
  21. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  22. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  23. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  24. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  25. data/lib/hecks/bluebook.rb +2 -0
  26. data/lib/hecks/chapters.rb +3 -3
  27. data/lib/hecks/cli/project_cli/launcher_source.rb +1 -1
  28. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  29. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  30. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  31. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  32. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  33. data/lib/hecks/composer/adapters/jev.rb +66 -0
  34. data/lib/hecks/composer/agent.rb +113 -0
  35. data/lib/hecks/composer/assistant.rb +110 -0
  36. data/lib/hecks/composer/behavior_run.rb +53 -0
  37. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  38. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  39. data/lib/hecks/composer/board.rb +65 -0
  40. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  41. data/lib/hecks/composer/board_edit/source.rb +84 -0
  42. data/lib/hecks/composer/board_edit.rb +144 -0
  43. data/lib/hecks/composer/check.rb +89 -0
  44. data/lib/hecks/composer/domains.rb +81 -0
  45. data/lib/hecks/composer/jobs.rb +134 -0
  46. data/lib/hecks/composer/layout.rb +37 -0
  47. data/lib/hecks/composer/live.rb +150 -0
  48. data/lib/hecks/composer/outline.rb +48 -0
  49. data/lib/hecks/composer/page.html +989 -0
  50. data/lib/hecks/composer/panels.rb +142 -0
  51. data/lib/hecks/composer/parse.rb +83 -0
  52. data/lib/hecks/composer/plain.rb +28 -0
  53. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  54. data/lib/hecks/composer/session.rb +141 -0
  55. data/lib/hecks/composer/toolbox.rb +166 -0
  56. data/lib/hecks/composer/web.rb +156 -0
  57. data/lib/hecks/composer/workbench.rb +51 -0
  58. data/lib/hecks/composer/workspace.rb +81 -0
  59. data/lib/hecks/composer.rb +17 -0
  60. data/lib/hecks/corpus/adapters.rb +22 -0
  61. data/lib/hecks/corpus/rust.rb +1 -1
  62. data/lib/hecks/corpus/worlds.rb +19 -0
  63. data/lib/hecks/corpus.rb +5 -0
  64. data/lib/hecks/forms/app/commands.rb +2 -3
  65. data/lib/hecks/forms/app/queries.rb +2 -3
  66. data/lib/hecks/forms/app/records.rb +17 -5
  67. data/lib/hecks/forms/app/responses.rb +3 -1
  68. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  69. data/lib/hecks/forms/field_controls.rb +2 -2
  70. data/lib/hecks/forms/field_renderer.rb +1 -1
  71. data/lib/hecks/forms/field_shape.rb +36 -17
  72. data/lib/hecks/forms/index_renderer.rb +2 -1
  73. data/lib/hecks/forms/page.rb +1 -1
  74. data/lib/hecks/forms/params.rb +0 -13
  75. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  76. data/lib/hecks/forms/record_renderer.rb +11 -7
  77. data/lib/hecks/forms/rule_words.rb +100 -0
  78. data/lib/hecks/forms/value_object_shape.rb +2 -1
  79. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  80. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  81. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  82. data/lib/hecks/gate/stages.yml +25 -0
  83. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  84. data/lib/hecks/grammar/evolve.rb +0 -3
  85. data/lib/hecks/grammar/translation.bluebook +1 -1
  86. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  87. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  88. data/lib/hecks/hecks/adapters/first_admin.rb +3 -3
  89. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  90. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  91. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  92. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  93. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  94. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  95. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  96. data/lib/hecks/hecks/codebase.behaviors +6 -0
  97. data/lib/hecks/hecks/codebase.bluebook +21 -0
  98. data/lib/hecks/hecks/composer.behaviors +68 -0
  99. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  100. data/lib/hecks/hecks/custodian.behaviors +0 -6
  101. data/lib/hecks/hecks/custodian.bluebook +3 -80
  102. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  103. data/lib/hecks/hecks/hecks.world +1 -1
  104. data/lib/hecks/hecks/rehearsal.bluebook +7 -13
  105. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  106. data/lib/hecks/kernel_capabilities.rb +1 -1
  107. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  108. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  109. data/lib/hecks/ports/access_control.rb +2 -2
  110. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  111. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  112. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  113. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  114. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  115. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  116. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  117. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  118. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  119. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  120. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  121. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  122. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  123. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  124. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  125. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  126. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  127. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  128. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  129. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  130. data/lib/hecks/ports.rb +0 -1
  131. data/lib/hecks/projection_files.rb +3 -1
  132. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  133. data/lib/hecks/projections/glossary/entries.rb +10 -4
  134. data/lib/hecks/projections/glossary/groups.rb +42 -0
  135. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  136. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  137. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  138. data/lib/hecks/projections/glossary/html.rb +1 -2
  139. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  140. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  141. data/lib/hecks/projections/glossary/page.css +143 -153
  142. data/lib/hecks/projections/glossary/page.js +63 -3
  143. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  144. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  145. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  146. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  147. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  148. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  149. data/lib/hecks/projections/glossary.rb +2 -1
  150. data/lib/hecks/projections/plain_english.rb +97 -0
  151. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  152. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  153. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  154. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  155. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  156. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +7 -2
  157. data/lib/hecks/projections/statements/actions.rb +99 -0
  158. data/lib/hecks/projections/statements/relationships.rb +86 -0
  159. data/lib/hecks/projections/statements.rb +15 -24
  160. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  161. data/lib/hecks/projector/exporter/translations.rb +18 -35
  162. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  163. data/lib/hecks/projector/exporter.rb +5 -1
  164. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  165. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  166. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  167. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  168. data/lib/hecks/runtime/outbox.rb +2 -9
  169. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  170. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  171. data/lib/hecks/runtime/value/unwrapping.rb +60 -0
  172. data/lib/hecks/runtime/value.rb +10 -21
  173. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  174. data/lib/hecks/rust_build/wasm.rb +0 -12
  175. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  176. data/lib/hecks/rust_format.rb +77 -0
  177. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  178. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  179. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  180. data/lib/hecks/tools/translation_audit.rb +8 -10
  181. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  182. data/lib/hecks/version.rb +1 -1
  183. data/lib/hecks/vocabulary.rb +2 -1
  184. data/lib/hecks.rb +3 -0
  185. data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → ad68f9e06c8068c2a4176b57abf3083d591d5d8980c2b3e897b97331f965b643.marshal} +0 -0
  186. data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → ee22ec8642c4006717ba2b80f46e5efb81de459eef9dfec043e838c0ad4fd720.marshal} +0 -0
  187. data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-b69f5dcbba79c20daa0493476fa7c317004a30aa2bc3d3dc6a722ea70ae2b43b.json} +1 -1
  188. data/rust/Cargo.toml +40 -0
  189. data/rust/build/Cargo.toml +36 -0
  190. data/rust/build/clippy.toml +4 -0
  191. data/rust/build/src/adapters_pass.rs +196 -0
  192. data/rust/build/src/binding_pass.rs +54 -13
  193. data/rust/build/src/build_artifact.rs +23 -5
  194. data/rust/build/src/cargo_sync.rs +69 -26
  195. data/rust/build/src/lineage_pass.rs +101 -87
  196. data/rust/build/src/main.rs +11 -2
  197. data/rust/build/src/pipeline.rs +80 -19
  198. data/rust/build/src/reserved_names.rs +6 -50
  199. data/rust/build/src/resolve.rs +82 -35
  200. data/rust/build/src/rustfmt.rs +100 -0
  201. data/rust/build/src/seams_pass.rs +85 -16
  202. data/rust/build/src/subprocess.rs +30 -6
  203. data/rust/build/src/tmp.rs +5 -2
  204. data/rust/build/src/translation_pass.rs +72 -22
  205. data/rust/build/src/world_pass.rs +109 -0
  206. data/rust/clippy.toml +4 -0
  207. data/rust/codegen/Cargo.toml +36 -0
  208. data/rust/codegen/clippy.toml +4 -0
  209. data/rust/codegen/src/asks.rs +40 -14
  210. data/rust/codegen/src/attr.rs +2 -2
  211. data/rust/codegen/src/bridging.rs +676 -117
  212. data/rust/codegen/src/commands.rs +979 -373
  213. data/rust/codegen/src/constraints.rs +88 -25
  214. data/rust/codegen/src/dependency_planning.rs +128 -34
  215. data/rust/codegen/src/domain_generator.rs +306 -170
  216. data/rust/codegen/src/exemplar.rs +87 -32
  217. data/rust/codegen/src/expr_emitter.rs +54 -6
  218. data/rust/codegen/src/fielded.rs +193 -41
  219. data/rust/codegen/src/hecks_naming.rs +124 -16
  220. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  221. data/rust/codegen/src/json.rs +80 -44
  222. data/rust/codegen/src/json_codec.rs +585 -149
  223. data/rust/codegen/src/literal.rs +104 -138
  224. data/rust/codegen/src/main.rs +151 -52
  225. data/rust/codegen/src/manifest.rs +76 -14
  226. data/rust/codegen/src/mutations.rs +1212 -239
  227. data/rust/codegen/src/naming.rs +111 -60
  228. data/rust/codegen/src/optional_pass.rs +90 -28
  229. data/rust/codegen/src/ports.rs +5 -4
  230. data/rust/codegen/src/prelude.rs +139 -49
  231. data/rust/codegen/src/presenters.rs +335 -0
  232. data/rust/codegen/src/queries.rs +601 -226
  233. data/rust/codegen/src/reactions.rs +268 -74
  234. data/rust/codegen/src/read_models.rs +574 -229
  235. data/rust/codegen/src/reference_specs.rs +16 -3
  236. data/rust/codegen/src/registry.rs +128 -79
  237. data/rust/codegen/src/reserved_names.rs +6 -50
  238. data/rust/codegen/src/shared.rs +71 -22
  239. data/rust/codegen/src/sidecars.rs +15 -5
  240. data/rust/codegen/src/skip_reason.rs +8 -2
  241. data/rust/codegen/src/types.rs +306 -108
  242. data/rust/host/Cargo.lock +1 -0
  243. data/rust/host/Cargo.toml +40 -4
  244. data/rust/host/HECKS_RELEASE +1 -1
  245. data/rust/host/clippy.toml +4 -0
  246. data/rust/host/src/api.rs +698 -218
  247. data/rust/host/src/auth.rs +491 -214
  248. data/rust/host/src/b64.rs +91 -0
  249. data/rust/host/src/bin/expr_harness.rs +6 -2
  250. data/rust/host/src/bin/lineage_harness.rs +47 -11
  251. data/rust/host/src/bin/mint_harness.rs +70 -37
  252. data/rust/host/src/bin/validate_harness.rs +16 -8
  253. data/rust/host/src/boot.rs +144 -68
  254. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  255. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  256. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  257. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  258. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  259. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  260. data/rust/host/src/checkout.rs +78 -32
  261. data/rust/host/src/clock.rs +8 -0
  262. data/rust/host/src/commerce_ir.rs +333 -95
  263. data/rust/host/src/crypto.rs +33 -0
  264. data/rust/host/src/dispatch.rs +1037 -331
  265. data/rust/host/src/expr_json.rs +1021 -273
  266. data/rust/host/src/extension.rs +24 -4
  267. data/rust/host/src/field_hints.rs +11 -7
  268. data/rust/host/src/fuzz_support.rs +85 -13
  269. data/rust/host/src/host.rs +100 -0
  270. data/rust/host/src/http.rs +22 -0
  271. data/rust/host/src/ir.rs +326 -16
  272. data/rust/host/src/journal.rs +277 -176
  273. data/rust/host/src/lambda_client.rs +219 -53
  274. data/rust/host/src/lib.rs +16 -4
  275. data/rust/host/src/log.rs +62 -11
  276. data/rust/host/src/mint.rs +1153 -326
  277. data/rust/host/src/needs.rs +185 -44
  278. data/rust/host/src/payments/keystore.rs +197 -45
  279. data/rust/host/src/payments/roles.rs +39 -7
  280. data/rust/host/src/payments/tests.rs +1427 -282
  281. data/rust/host/src/payments.rs +391 -112
  282. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  283. data/rust/host/src/platform_signin/config.rs +11 -11
  284. data/rust/host/src/platform_signin/cookie.rs +2 -2
  285. data/rust/host/src/platform_signin/extension.rs +2 -1
  286. data/rust/host/src/platform_signin/handler.rs +9 -8
  287. data/rust/host/src/platform_signin/jti.rs +4 -2
  288. data/rust/host/src/platform_signin/jwks.rs +2 -1
  289. data/rust/host/src/platform_signin/jws.rs +2 -37
  290. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  291. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  292. data/rust/host/src/platform_signin/pages.rs +20 -20
  293. data/rust/host/src/platform_signin/session.rs +8 -1
  294. data/rust/host/src/platform_signin/settings.rs +6 -6
  295. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  296. data/rust/host/src/platform_signin/verify.rs +41 -41
  297. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  298. data/rust/host/src/presentation.rs +207 -63
  299. data/rust/host/src/presentation_write.rs +502 -187
  300. data/rust/host/src/query_step.rs +69 -18
  301. data/rust/host/src/rate_limit.rs +541 -142
  302. data/rust/host/src/reference_transform.rs +77 -25
  303. data/rust/host/src/reference_validate.rs +443 -104
  304. data/rust/host/src/resend.rs +190 -55
  305. data/rust/host/src/secrets.rs +116 -21
  306. data/rust/host/src/server.rs +555 -104
  307. data/rust/host/src/storage_shape.rs +88 -21
  308. data/rust/host/src/test_pg.rs +17 -7
  309. data/rust/host/src/ui_schema.rs +495 -207
  310. data/rust/host/src/wasm_runner.rs +21 -6
  311. data/rust/host/src/web/commerce.rs +101 -23
  312. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  313. data/rust/host/src/web/newsletter/template.rs +26 -7
  314. data/rust/host/src/web/newsletter/tests.rs +112 -23
  315. data/rust/host/src/web/newsletter.rs +209 -59
  316. data/rust/host/src/web/newsletter_send.rs +251 -79
  317. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  318. data/rust/host/src/web/registration_receipt.rs +100 -24
  319. data/rust/host/src/web/registrations.rs +1384 -332
  320. data/rust/host/src/web/resend_webhook.rs +138 -80
  321. data/rust/host/src/web.rs +2416 -508
  322. data/rust/lsp/Cargo.toml +36 -0
  323. data/rust/lsp/clippy.toml +4 -0
  324. data/rust/lsp/src/json.rs +29 -22
  325. data/rust/lsp/src/main.rs +81 -31
  326. data/rust/lsp/src/outline.rs +23 -7
  327. data/rust/lsp/src/rpc.rs +17 -4
  328. data/rust/parser/Cargo.toml +36 -0
  329. data/rust/parser/clippy.toml +4 -0
  330. data/rust/parser/src/build/closed_sets.rs +6 -1
  331. data/rust/parser/src/build/identity.rs +4 -3
  332. data/rust/parser/src/build/mod.rs +1 -1
  333. data/rust/parser/src/build/named_pattern.rs +4 -1
  334. data/rust/parser/src/build/naming.rs +2 -80
  335. data/rust/parser/src/build/pattern_subset.rs +6 -8
  336. data/rust/parser/src/build/query_derive.rs +3 -1
  337. data/rust/parser/src/build/query_inference.rs +14 -5
  338. data/rust/parser/src/build/read_model.rs +4 -1
  339. data/rust/parser/src/build/references.rs +4 -3
  340. data/rust/parser/src/canonical.rs +34 -15
  341. data/rust/parser/src/emit.rs +187 -105
  342. data/rust/parser/src/expr/ast_json.rs +134 -42
  343. data/rust/parser/src/expr/evaluator.rs +27 -9
  344. data/rust/parser/src/expr/mod.rs +54 -13
  345. data/rust/parser/src/expr/resolver.rs +299 -92
  346. data/rust/parser/src/ir.rs +35 -3
  347. data/rust/parser/src/keywords.rs +7168 -516
  348. data/rust/parser/src/lex.rs +11 -25
  349. data/rust/parser/src/lib.rs +1 -0
  350. data/rust/parser/src/main.rs +65 -6
  351. data/rust/parser/src/parse/adapter.rs +157 -0
  352. data/rust/parser/src/parse/aggregate.rs +19 -10
  353. data/rust/parser/src/parse/chapter.rs +16 -17
  354. data/rust/parser/src/parse/command.rs +35 -16
  355. data/rust/parser/src/parse/domain_port.rs +4 -7
  356. data/rust/parser/src/parse/entity.rs +21 -11
  357. data/rust/parser/src/parse/hecksagon.rs +10 -4
  358. data/rust/parser/src/parse/lifecycle.rs +9 -5
  359. data/rust/parser/src/parse/mod.rs +22 -91
  360. data/rust/parser/src/parse/needs.rs +21 -12
  361. data/rust/parser/src/parse/policy.rs +2 -2
  362. data/rust/parser/src/parse/process_manager.rs +9 -11
  363. data/rust/parser/src/parse/query.rs +8 -4
  364. data/rust/parser/src/parse/read_model.rs +57 -16
  365. data/rust/parser/src/parse/value_object.rs +7 -2
  366. data/rust/parser/src/parse/world.rs +322 -0
  367. data/rust/parser/src/ruby_value.rs +7 -6
  368. data/rust/src/exemplar/json.rs +4 -4
  369. data/rust/src/exemplar/mutations.rs +18 -0
  370. data/rust/src/exemplar/read_models.rs +1 -10
  371. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  372. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  373. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  374. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  375. data/rust/src/kernel/cli.rs +244 -114
  376. data/rust/src/kernel/dispatch.rs +323 -115
  377. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  378. data/rust/src/kernel/expr.rs +203 -86
  379. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  380. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  381. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  382. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  383. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  384. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  385. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  386. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  387. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  388. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  389. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  390. data/rust/src/kernel/expression_operators/text.rs +120 -27
  391. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  392. data/rust/src/kernel/json.rs +244 -148
  393. data/rust/src/kernel/mod.rs +47 -33
  394. data/rust/src/kernel/named_query.rs +63 -15
  395. data/rust/src/kernel/needs.rs +112 -25
  396. data/rust/src/kernel/orchestrate.rs +980 -581
  397. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  398. data/rust/src/kernel/pattern.rs +107 -34
  399. data/rust/src/kernel/present.rs +261 -0
  400. data/rust/src/kernel/query_comparators.rs +118 -41
  401. data/rust/src/kernel/query_ordering.rs +13 -5
  402. data/rust/src/kernel/read_model.rs +479 -181
  403. data/rust/src/kernel/reference_lookup.rs +122 -26
  404. data/rust/src/kernel/repository.rs +293 -66
  405. data/rust/src/kernel/routing.rs +58 -42
  406. data/rust/src/kernel/ruby_float.rs +139 -0
  407. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  408. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  409. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  410. data/rust/src/kernel/vocab/mod.rs +6 -6
  411. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  412. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  413. data/rust/src/lib.rs +50 -0
  414. data/rust/src/main.rs +4 -1
  415. data/rust/web/Cargo.toml +36 -0
  416. data/rust/web/clippy.toml +4 -0
  417. metadata +85 -15
  418. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  419. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  420. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  421. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  422. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  423. data/lib/hecks/ports/authentication.port +0 -6
  424. data/lib/hecks/ports/authentication.rb +0 -67
  425. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  426. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  427. data/rust/host/src/approval.rs +0 -652
@@ -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
@@ -0,0 +1,84 @@
1
+ require_relative "../board"
2
+ require_relative "../outline"
3
+ require_relative "../workspace"
4
+
5
+ module Hecks
6
+ module Composer
7
+ module BoardEdit
8
+ # The bluebook text of a staged workspace as lines, with what a board edit needs to change
9
+ # it: where a declaration is, where its block ends (found by indentation, as the bluebooks
10
+ # are laid out), and a way to put a patched file back as one result.
11
+ class Source
12
+ # @return [Board::Places] where each aggregate, command and policy is declared
13
+ attr_reader :places
14
+
15
+ # @param stage [String] a staged directory (see {Workspace.stage})
16
+ def initialize(stage)
17
+ files = Workspace.files(stage)
18
+ @places = Board::Places.new(Outline.call(stage, files))
19
+ @lines = files.grep(/\.bluebook\z/).to_h { |file| [file, File.read(File.join(stage, file)).lines.map(&:chomp)] }
20
+ end
21
+
22
+ # @param at [Hash] `{file:, line:}` of a declaration
23
+ # @return [Integer] the 0-based index of the `end` that closes it
24
+ # @raise [Refused] if the block has no closing `end` at its own indentation
25
+ def block_end(at)
26
+ lines = @lines.fetch(at[:file])
27
+ start = at[:line] - 1
28
+ pattern = /\A {#{indent_of(lines[start])}}end\b/
29
+ ((start + 1)...lines.size).find { |index| lines[index].match?(pattern) } or
30
+ raise Refused, "cannot find where the block at #{at[:file]}:#{at[:line]} ends"
31
+ end
32
+
33
+ # @param at [Hash] `{file:, line:}` of a declaration
34
+ # @return [Integer] how many spaces its line is indented
35
+ def indent(at) = indent_of(@lines.fetch(at[:file])[at[:line] - 1])
36
+
37
+ # The file and `end` of the chapter's own block, where new aggregates and policies go.
38
+ # @return [Hash] `{file:, line:}` of the chapter header
39
+ # @raise [Refused] if no file declares the chapter
40
+ def chapter
41
+ file = @lines.keys.sort.find { |path| @lines[path].any? { |line| line.match?(/\A\s*Hecks\.bluebook\s+"/) } }
42
+ raise Refused, "no file declares the chapter" unless file
43
+
44
+ { file: file, line: @lines[file].index { |line| line.match?(/\A\s*Hecks\.bluebook\s+"/) } + 1 }
45
+ end
46
+
47
+ # @param file [String] a bluebook path
48
+ # @param from [Integer] first 0-based line to read
49
+ # @param upto [Integer] last 0-based line to read
50
+ # @return [Array<String>] those lines
51
+ def slice(file, from, upto) = @lines.fetch(file)[from..upto]
52
+
53
+ # Puts new lines before a line of a file, with a blank line above them unless one is there.
54
+ # @return [Hash] the patched file as `{path:, text:, line:}`
55
+ def insert(file, before, new_lines)
56
+ lines = @lines.fetch(file)
57
+ added = spaced(lines, before, new_lines)
58
+ lines.insert(before, *added)
59
+ patched(file, before + 1 + added.index { |line| !line.strip.empty? }.to_i)
60
+ end
61
+
62
+ # Takes lines out of a file, and the blank line above them when there is one.
63
+ # @return [Hash] the patched file as `{path:, text:, line:}`
64
+ def remove(file, from, upto)
65
+ lines = @lines.fetch(file)
66
+ from -= 1 if from.positive? && lines[from - 1].strip.empty?
67
+ lines.slice!(from..upto)
68
+ patched(file, nil)
69
+ end
70
+
71
+ private
72
+
73
+ def indent_of(line) = line[/\A */].size
74
+
75
+ # The new lines, without their leading blank line when one is already above the spot.
76
+ def spaced(lines, before, added)
77
+ added.first.to_s.empty? && lines[before - 1].to_s.strip.empty? ? added.drop(1) : added
78
+ end
79
+
80
+ def patched(file, line) = { path: file, text: "#{@lines.fetch(file).join("\n")}\n", line: line }
81
+ end
82
+ end
83
+ end
84
+ end