hecks 3.14.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/adapters/fake_processor.rb +34 -0
  3. data/examples/pizzas/adapters/payment_signature.rb +66 -0
  4. data/examples/pizzas/adapters/payment_webhook.rb +87 -0
  5. data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
  6. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  7. data/examples/pizzas/config.ru +22 -0
  8. data/lib/hecks/adapters/driven/prism.rb +0 -2
  9. data/lib/hecks/adapters/driven.rb +0 -2
  10. data/lib/hecks/adapters/driving/json.rb +1 -7
  11. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  12. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  13. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  14. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  15. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  16. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  17. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  18. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  19. data/lib/hecks/bluebook/translation.rb +18 -10
  20. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  21. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  22. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  23. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  24. data/lib/hecks/bluebook.rb +2 -0
  25. data/lib/hecks/chapters.rb +3 -3
  26. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  27. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  28. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  29. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  30. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  31. data/lib/hecks/composer/adapters/jev.rb +66 -0
  32. data/lib/hecks/composer/agent.rb +113 -0
  33. data/lib/hecks/composer/assistant.rb +110 -0
  34. data/lib/hecks/composer/behavior_run.rb +53 -0
  35. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  36. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  37. data/lib/hecks/composer/board.rb +65 -0
  38. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  39. data/lib/hecks/composer/board_edit/source.rb +84 -0
  40. data/lib/hecks/composer/board_edit.rb +144 -0
  41. data/lib/hecks/composer/check.rb +89 -0
  42. data/lib/hecks/composer/domains.rb +81 -0
  43. data/lib/hecks/composer/jobs.rb +134 -0
  44. data/lib/hecks/composer/layout.rb +37 -0
  45. data/lib/hecks/composer/live.rb +150 -0
  46. data/lib/hecks/composer/outline.rb +48 -0
  47. data/lib/hecks/composer/page.html +989 -0
  48. data/lib/hecks/composer/panels.rb +142 -0
  49. data/lib/hecks/composer/parse.rb +83 -0
  50. data/lib/hecks/composer/plain.rb +28 -0
  51. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  52. data/lib/hecks/composer/session.rb +141 -0
  53. data/lib/hecks/composer/toolbox.rb +166 -0
  54. data/lib/hecks/composer/web.rb +156 -0
  55. data/lib/hecks/composer/workbench.rb +51 -0
  56. data/lib/hecks/composer/workspace.rb +81 -0
  57. data/lib/hecks/composer.rb +17 -0
  58. data/lib/hecks/corpus/adapters.rb +22 -0
  59. data/lib/hecks/corpus/rust.rb +1 -1
  60. data/lib/hecks/corpus/worlds.rb +19 -0
  61. data/lib/hecks/corpus.rb +5 -0
  62. data/lib/hecks/forms/app/commands.rb +2 -3
  63. data/lib/hecks/forms/app/queries.rb +2 -3
  64. data/lib/hecks/forms/app/records.rb +17 -5
  65. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  66. data/lib/hecks/forms/field_controls.rb +2 -2
  67. data/lib/hecks/forms/field_renderer.rb +1 -1
  68. data/lib/hecks/forms/field_shape.rb +36 -17
  69. data/lib/hecks/forms/index_renderer.rb +2 -1
  70. data/lib/hecks/forms/page.rb +1 -1
  71. data/lib/hecks/forms/params.rb +0 -13
  72. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  73. data/lib/hecks/forms/record_renderer.rb +11 -7
  74. data/lib/hecks/forms/rule_words.rb +100 -0
  75. data/lib/hecks/forms/value_object_shape.rb +2 -1
  76. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  77. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  78. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  79. data/lib/hecks/gate/stages.yml +25 -0
  80. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  81. data/lib/hecks/grammar/evolve.rb +0 -3
  82. data/lib/hecks/grammar/translation.bluebook +1 -1
  83. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  84. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  85. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  86. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  87. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  88. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  89. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  90. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  91. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  92. data/lib/hecks/hecks/codebase.behaviors +6 -0
  93. data/lib/hecks/hecks/codebase.bluebook +21 -0
  94. data/lib/hecks/hecks/composer.behaviors +68 -0
  95. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  96. data/lib/hecks/hecks/custodian.behaviors +0 -6
  97. data/lib/hecks/hecks/custodian.bluebook +3 -80
  98. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  99. data/lib/hecks/hecks/hecks.world +1 -1
  100. data/lib/hecks/hecks/rehearsal.bluebook +7 -13
  101. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  102. data/lib/hecks/kernel_capabilities.rb +1 -1
  103. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  104. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  105. data/lib/hecks/ports/access_control.rb +2 -2
  106. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  107. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  108. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  109. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  110. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  111. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  112. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  113. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  114. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  115. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  116. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  117. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  118. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  119. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  120. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  121. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  122. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  123. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  124. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  125. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  126. data/lib/hecks/ports.rb +0 -1
  127. data/lib/hecks/projection_files.rb +3 -1
  128. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  129. data/lib/hecks/projections/glossary/entries.rb +10 -4
  130. data/lib/hecks/projections/glossary/groups.rb +42 -0
  131. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  132. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  133. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  134. data/lib/hecks/projections/glossary/html.rb +1 -2
  135. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  136. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  137. data/lib/hecks/projections/glossary/page.css +143 -153
  138. data/lib/hecks/projections/glossary/page.js +63 -3
  139. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  140. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  141. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  142. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  143. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  144. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  145. data/lib/hecks/projections/glossary.rb +2 -1
  146. data/lib/hecks/projections/plain_english.rb +97 -0
  147. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  148. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  149. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  150. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  151. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  152. data/lib/hecks/projections/statements/actions.rb +99 -0
  153. data/lib/hecks/projections/statements/relationships.rb +86 -0
  154. data/lib/hecks/projections/statements.rb +15 -24
  155. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  156. data/lib/hecks/projector/exporter/translations.rb +18 -35
  157. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  158. data/lib/hecks/projector/exporter.rb +5 -1
  159. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  160. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  161. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  162. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  163. data/lib/hecks/runtime/outbox.rb +2 -9
  164. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  165. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  166. data/lib/hecks/runtime/value.rb +13 -0
  167. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  168. data/lib/hecks/rust_build/wasm.rb +0 -12
  169. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  170. data/lib/hecks/rust_format.rb +77 -0
  171. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  172. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  173. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  174. data/lib/hecks/tools/translation_audit.rb +8 -10
  175. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  176. data/lib/hecks/version.rb +1 -1
  177. data/lib/hecks/vocabulary.rb +2 -1
  178. data/lib/hecks.rb +3 -0
  179. data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
  180. data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
  181. data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json} +1 -1
  182. data/rust/Cargo.toml +40 -0
  183. data/rust/build/Cargo.toml +36 -0
  184. data/rust/build/clippy.toml +4 -0
  185. data/rust/build/src/adapters_pass.rs +196 -0
  186. data/rust/build/src/binding_pass.rs +54 -13
  187. data/rust/build/src/build_artifact.rs +23 -5
  188. data/rust/build/src/cargo_sync.rs +69 -26
  189. data/rust/build/src/lineage_pass.rs +101 -87
  190. data/rust/build/src/main.rs +11 -2
  191. data/rust/build/src/pipeline.rs +80 -19
  192. data/rust/build/src/reserved_names.rs +6 -50
  193. data/rust/build/src/resolve.rs +82 -35
  194. data/rust/build/src/rustfmt.rs +100 -0
  195. data/rust/build/src/seams_pass.rs +85 -16
  196. data/rust/build/src/subprocess.rs +30 -6
  197. data/rust/build/src/tmp.rs +5 -2
  198. data/rust/build/src/translation_pass.rs +72 -22
  199. data/rust/build/src/world_pass.rs +109 -0
  200. data/rust/clippy.toml +4 -0
  201. data/rust/codegen/Cargo.toml +36 -0
  202. data/rust/codegen/clippy.toml +4 -0
  203. data/rust/codegen/src/asks.rs +40 -14
  204. data/rust/codegen/src/attr.rs +2 -2
  205. data/rust/codegen/src/bridging.rs +676 -117
  206. data/rust/codegen/src/commands.rs +979 -373
  207. data/rust/codegen/src/constraints.rs +88 -25
  208. data/rust/codegen/src/dependency_planning.rs +128 -34
  209. data/rust/codegen/src/domain_generator.rs +300 -170
  210. data/rust/codegen/src/exemplar.rs +87 -32
  211. data/rust/codegen/src/expr_emitter.rs +54 -6
  212. data/rust/codegen/src/fielded.rs +193 -41
  213. data/rust/codegen/src/hecks_naming.rs +124 -16
  214. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  215. data/rust/codegen/src/json.rs +80 -44
  216. data/rust/codegen/src/json_codec.rs +585 -149
  217. data/rust/codegen/src/literal.rs +104 -138
  218. data/rust/codegen/src/main.rs +145 -52
  219. data/rust/codegen/src/manifest.rs +76 -14
  220. data/rust/codegen/src/mutations.rs +1212 -239
  221. data/rust/codegen/src/naming.rs +111 -60
  222. data/rust/codegen/src/optional_pass.rs +90 -28
  223. data/rust/codegen/src/ports.rs +5 -4
  224. data/rust/codegen/src/prelude.rs +139 -49
  225. data/rust/codegen/src/queries.rs +601 -226
  226. data/rust/codegen/src/reactions.rs +268 -74
  227. data/rust/codegen/src/read_models.rs +606 -205
  228. data/rust/codegen/src/reference_specs.rs +16 -3
  229. data/rust/codegen/src/registry.rs +128 -79
  230. data/rust/codegen/src/reserved_names.rs +6 -50
  231. data/rust/codegen/src/shared.rs +71 -22
  232. data/rust/codegen/src/sidecars.rs +15 -5
  233. data/rust/codegen/src/skip_reason.rs +8 -2
  234. data/rust/codegen/src/types.rs +306 -108
  235. data/rust/host/Cargo.lock +1 -0
  236. data/rust/host/Cargo.toml +40 -4
  237. data/rust/host/HECKS_RELEASE +1 -1
  238. data/rust/host/clippy.toml +4 -0
  239. data/rust/host/src/api.rs +698 -218
  240. data/rust/host/src/auth.rs +491 -214
  241. data/rust/host/src/b64.rs +91 -0
  242. data/rust/host/src/bin/expr_harness.rs +6 -2
  243. data/rust/host/src/bin/lineage_harness.rs +47 -11
  244. data/rust/host/src/bin/mint_harness.rs +70 -37
  245. data/rust/host/src/bin/validate_harness.rs +16 -8
  246. data/rust/host/src/boot.rs +144 -68
  247. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  248. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  249. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  250. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  251. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  252. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  253. data/rust/host/src/checkout.rs +78 -32
  254. data/rust/host/src/clock.rs +8 -0
  255. data/rust/host/src/commerce_ir.rs +333 -95
  256. data/rust/host/src/crypto.rs +33 -0
  257. data/rust/host/src/dispatch.rs +990 -331
  258. data/rust/host/src/expr_json.rs +1021 -273
  259. data/rust/host/src/extension.rs +24 -4
  260. data/rust/host/src/field_hints.rs +11 -7
  261. data/rust/host/src/fuzz_support.rs +85 -13
  262. data/rust/host/src/host.rs +100 -0
  263. data/rust/host/src/http.rs +22 -0
  264. data/rust/host/src/ir.rs +326 -16
  265. data/rust/host/src/journal.rs +277 -176
  266. data/rust/host/src/lambda_client.rs +219 -53
  267. data/rust/host/src/lib.rs +16 -4
  268. data/rust/host/src/log.rs +62 -11
  269. data/rust/host/src/mint.rs +1153 -326
  270. data/rust/host/src/needs.rs +185 -44
  271. data/rust/host/src/payments/keystore.rs +197 -45
  272. data/rust/host/src/payments/roles.rs +39 -7
  273. data/rust/host/src/payments/tests.rs +1427 -282
  274. data/rust/host/src/payments.rs +391 -112
  275. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  276. data/rust/host/src/platform_signin/config.rs +11 -11
  277. data/rust/host/src/platform_signin/cookie.rs +2 -2
  278. data/rust/host/src/platform_signin/extension.rs +2 -1
  279. data/rust/host/src/platform_signin/handler.rs +9 -8
  280. data/rust/host/src/platform_signin/jti.rs +4 -2
  281. data/rust/host/src/platform_signin/jwks.rs +2 -1
  282. data/rust/host/src/platform_signin/jws.rs +2 -37
  283. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  284. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  285. data/rust/host/src/platform_signin/pages.rs +20 -20
  286. data/rust/host/src/platform_signin/session.rs +8 -1
  287. data/rust/host/src/platform_signin/settings.rs +6 -6
  288. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  289. data/rust/host/src/platform_signin/verify.rs +41 -41
  290. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  291. data/rust/host/src/presentation.rs +207 -63
  292. data/rust/host/src/presentation_write.rs +502 -187
  293. data/rust/host/src/query_step.rs +69 -18
  294. data/rust/host/src/rate_limit.rs +541 -142
  295. data/rust/host/src/reference_transform.rs +77 -25
  296. data/rust/host/src/reference_validate.rs +443 -104
  297. data/rust/host/src/resend.rs +190 -55
  298. data/rust/host/src/secrets.rs +116 -21
  299. data/rust/host/src/server.rs +483 -104
  300. data/rust/host/src/storage_shape.rs +88 -21
  301. data/rust/host/src/test_pg.rs +17 -7
  302. data/rust/host/src/ui_schema.rs +495 -207
  303. data/rust/host/src/wasm_runner.rs +21 -6
  304. data/rust/host/src/web/commerce.rs +101 -23
  305. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  306. data/rust/host/src/web/newsletter/template.rs +26 -7
  307. data/rust/host/src/web/newsletter/tests.rs +112 -23
  308. data/rust/host/src/web/newsletter.rs +209 -59
  309. data/rust/host/src/web/newsletter_send.rs +251 -79
  310. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  311. data/rust/host/src/web/registration_receipt.rs +100 -24
  312. data/rust/host/src/web/registrations.rs +1384 -332
  313. data/rust/host/src/web/resend_webhook.rs +138 -80
  314. data/rust/host/src/web.rs +2416 -508
  315. data/rust/lsp/Cargo.toml +36 -0
  316. data/rust/lsp/clippy.toml +4 -0
  317. data/rust/lsp/src/json.rs +29 -22
  318. data/rust/lsp/src/main.rs +81 -31
  319. data/rust/lsp/src/outline.rs +23 -7
  320. data/rust/lsp/src/rpc.rs +17 -4
  321. data/rust/parser/Cargo.toml +36 -0
  322. data/rust/parser/clippy.toml +4 -0
  323. data/rust/parser/src/build/closed_sets.rs +6 -1
  324. data/rust/parser/src/build/identity.rs +4 -3
  325. data/rust/parser/src/build/mod.rs +1 -1
  326. data/rust/parser/src/build/named_pattern.rs +4 -1
  327. data/rust/parser/src/build/naming.rs +2 -80
  328. data/rust/parser/src/build/pattern_subset.rs +6 -8
  329. data/rust/parser/src/build/query_derive.rs +3 -1
  330. data/rust/parser/src/build/query_inference.rs +14 -5
  331. data/rust/parser/src/build/read_model.rs +4 -1
  332. data/rust/parser/src/build/references.rs +4 -3
  333. data/rust/parser/src/canonical.rs +34 -15
  334. data/rust/parser/src/emit.rs +187 -105
  335. data/rust/parser/src/expr/ast_json.rs +134 -42
  336. data/rust/parser/src/expr/evaluator.rs +27 -9
  337. data/rust/parser/src/expr/mod.rs +54 -13
  338. data/rust/parser/src/expr/resolver.rs +299 -92
  339. data/rust/parser/src/ir.rs +35 -3
  340. data/rust/parser/src/keywords.rs +7168 -516
  341. data/rust/parser/src/lex.rs +11 -25
  342. data/rust/parser/src/lib.rs +1 -0
  343. data/rust/parser/src/main.rs +65 -6
  344. data/rust/parser/src/parse/adapter.rs +157 -0
  345. data/rust/parser/src/parse/aggregate.rs +19 -10
  346. data/rust/parser/src/parse/chapter.rs +16 -17
  347. data/rust/parser/src/parse/command.rs +35 -16
  348. data/rust/parser/src/parse/domain_port.rs +4 -7
  349. data/rust/parser/src/parse/entity.rs +21 -11
  350. data/rust/parser/src/parse/hecksagon.rs +10 -4
  351. data/rust/parser/src/parse/lifecycle.rs +9 -5
  352. data/rust/parser/src/parse/mod.rs +22 -91
  353. data/rust/parser/src/parse/needs.rs +21 -12
  354. data/rust/parser/src/parse/policy.rs +2 -2
  355. data/rust/parser/src/parse/process_manager.rs +9 -11
  356. data/rust/parser/src/parse/query.rs +8 -4
  357. data/rust/parser/src/parse/read_model.rs +57 -16
  358. data/rust/parser/src/parse/value_object.rs +7 -2
  359. data/rust/parser/src/parse/world.rs +322 -0
  360. data/rust/parser/src/ruby_value.rs +7 -6
  361. data/rust/src/exemplar/json.rs +4 -4
  362. data/rust/src/exemplar/mutations.rs +18 -0
  363. data/rust/src/exemplar/read_models.rs +1 -10
  364. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  365. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  366. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  367. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  368. data/rust/src/kernel/cli.rs +229 -112
  369. data/rust/src/kernel/dispatch.rs +323 -115
  370. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  371. data/rust/src/kernel/expr.rs +203 -86
  372. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  373. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  374. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  375. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  376. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  377. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  378. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  379. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  380. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  381. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  382. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  383. data/rust/src/kernel/expression_operators/text.rs +120 -27
  384. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  385. data/rust/src/kernel/json.rs +230 -148
  386. data/rust/src/kernel/mod.rs +46 -33
  387. data/rust/src/kernel/named_query.rs +63 -15
  388. data/rust/src/kernel/needs.rs +112 -25
  389. data/rust/src/kernel/orchestrate.rs +980 -581
  390. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  391. data/rust/src/kernel/pattern.rs +107 -34
  392. data/rust/src/kernel/query_comparators.rs +118 -41
  393. data/rust/src/kernel/query_ordering.rs +13 -5
  394. data/rust/src/kernel/read_model.rs +479 -181
  395. data/rust/src/kernel/reference_lookup.rs +122 -26
  396. data/rust/src/kernel/repository.rs +293 -66
  397. data/rust/src/kernel/routing.rs +58 -42
  398. data/rust/src/kernel/ruby_float.rs +139 -0
  399. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  400. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  401. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  402. data/rust/src/kernel/vocab/mod.rs +6 -6
  403. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  404. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  405. data/rust/src/lib.rs +50 -0
  406. data/rust/src/main.rs +4 -1
  407. data/rust/web/Cargo.toml +36 -0
  408. data/rust/web/clippy.toml +4 -0
  409. metadata +82 -15
  410. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  411. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  412. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  413. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  414. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  415. data/lib/hecks/ports/authentication.port +0 -6
  416. data/lib/hecks/ports/authentication.rb +0 -67
  417. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  418. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  419. data/rust/host/src/approval.rs +0 -652
@@ -0,0 +1,989 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Hecks Composer</title>
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/hint/show-hint.min.css">
9
+ <style>
10
+ :root {
11
+ --bg: #f6f6f4; --panel: #ffffff; --edge: #dcdcd6; --ink: #1d1d1b; --dim: #6b6b64;
12
+ --accent: #2f6fed; --bad: #c0392b; --warn: #b7791f; --ok: #2f855a; --line: #fdecea;
13
+ }
14
+ @media (prefers-color-scheme: dark) {
15
+ :root {
16
+ --bg: #161616; --panel: #1e1e1e; --edge: #333330; --ink: #e8e8e3; --dim: #9a9a92;
17
+ --accent: #6c9cff; --bad: #ff7b6b; --warn: #e0b04a; --ok: #6fcf97; --line: #3a2220;
18
+ }
19
+ }
20
+ * { box-sizing: border-box; }
21
+ html, body { height: 100%; margin: 0; }
22
+ body { background: var(--bg); color: var(--ink); font: 13px/1.45 ui-sans-serif, system-ui, sans-serif; display: flex; flex-direction: column; }
23
+ header { display: flex; align-items: center; gap: 12px; padding: 6px 12px; border-bottom: 1px solid var(--edge); background: var(--panel); }
24
+ header h1 { font-size: 13px; margin: 0; letter-spacing: .04em; }
25
+ header .grow { flex: 1; }
26
+ header select { margin: 0; max-width: 280px; }
27
+ #modes button { border-radius: 0; margin: 0; } #modes button:first-child { border-radius: 4px 0 0 4px; } #modes button:last-child { border-radius: 0 4px 4px 0; border-left: 0; }
28
+ #modes button.on { background: var(--accent); color: #fff; border-color: var(--accent); }
29
+ main[hidden], #boardView[hidden], #boardForm[hidden] { display: none; }
30
+ #boardView { flex: 1; display: flex; flex-direction: column; min-height: 0; background: var(--panel); }
31
+ #boardBar { display: flex; align-items: center; gap: 8px; padding: 6px 12px; border-bottom: 1px solid var(--edge); flex-wrap: wrap; }
32
+ #boardActions { display: inline-flex; gap: 6px; flex-wrap: wrap; } #boardHint { color: var(--dim); }
33
+ #boardForm { display: flex; gap: 8px; align-items: center; padding: 6px 12px; background: var(--bg); border-bottom: 1px solid var(--edge); flex-wrap: wrap; }
34
+ #boardForm input { font: inherit; color: var(--ink); background: var(--panel); border: 1px solid var(--edge); border-radius: 4px; padding: 4px 8px; min-width: 190px; }
35
+ #boardMsg { padding: 0 12px; min-height: 18px; color: var(--ok); } #boardMsg.bad { color: var(--bad); }
36
+ #boardScroll { flex: 1; min-height: 0; overflow: auto; }
37
+ #boardSvg { display: block; }
38
+ .lane-band { fill: var(--bg); stroke: var(--edge); } .lane-title { fill: var(--dim); font-size: 10px; text-transform: uppercase; letter-spacing: .08em; }
39
+ .note rect { stroke: rgba(0, 0, 0, .25); stroke-width: 1; cursor: pointer; } .note text { fill: #1d1d1b; font-size: 12px; pointer-events: none; }
40
+ .note .kind { font-size: 9px; opacity: .65; text-transform: uppercase; }
41
+ .note.aggregate rect { fill: #f2d04c; } .note.command rect { fill: #6fb1ff; } .note.event rect { fill: #ff9f5a; } .note.policy rect { fill: #b98cff; }
42
+ .note.selected rect { stroke: var(--ink); stroke-width: 3; } .note.target rect { stroke: var(--ok); stroke-width: 3; stroke-dasharray: 5 3; }
43
+ .link { fill: none; stroke: var(--dim); stroke-width: 1.5; } .link.policy { stroke: #b98cff; stroke-width: 2; }
44
+ button { font: inherit; color: var(--ink); background: var(--panel); border: 1px solid var(--edge); border-radius: 4px; padding: 3px 10px; cursor: pointer; }
45
+ button:hover { border-color: var(--accent); }
46
+ main { flex: 1; display: grid; grid-template-columns: 200px minmax(0, 1fr) 6px var(--right, 42%); grid-template-rows: minmax(0, 1fr); min-height: 0; overflow: hidden; }
47
+ aside, section { background: var(--panel); min-height: 0; overflow: auto; }
48
+ aside { border-right: 1px solid var(--edge); }
49
+ .title { padding: 6px 10px; color: var(--dim); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }
50
+ .file { display: flex; padding: 3px 10px; cursor: pointer; gap: 6px; }
51
+ .file:hover { background: var(--bg); }
52
+ .file.active { background: var(--bg); border-left: 2px solid var(--accent); padding-left: 8px; }
53
+ .file .dot { color: var(--warn); visibility: hidden; }
54
+ .file.dirty .dot { visibility: visible; }
55
+ .file .side { margin-left: auto; color: var(--dim); font-size: 11px; }
56
+ #editor { display: flex; flex-direction: column; min-width: 0; min-height: 0; overflow: hidden; }
57
+ #tabs { display: flex; border-bottom: 1px solid var(--edge); overflow-x: auto; }
58
+ #tabs .tab { padding: 5px 12px; border-right: 1px solid var(--edge); color: var(--dim); white-space: nowrap; cursor: pointer; }
59
+ #tabs .tab.active { color: var(--ink); background: var(--bg); }
60
+ .CodeMirror { flex: 1; height: auto; font: 13px/1.5 ui-monospace, Menlo, monospace; background: var(--panel); color: var(--ink); }
61
+ .CodeMirror-gutters { background: var(--panel); border-right: 1px solid var(--edge); }
62
+ .CodeMirror-cursor { border-left-color: var(--ink); }
63
+ .cm-s-default .cm-keyword { color: var(--accent); }
64
+ .cm-s-default .cm-string, .cm-s-default .cm-string-2 { color: var(--ok); }
65
+ .cm-s-default .cm-comment { color: var(--dim); }
66
+ .cm-s-default .cm-atom, .cm-s-default .cm-number { color: var(--warn); }
67
+ .cm-s-default .cm-variable, .cm-s-default .cm-variable-2, .cm-s-default .cm-def { color: var(--ink); }
68
+ .bad-line { background: var(--line); }
69
+ #splitter { background: var(--edge); cursor: col-resize; }
70
+ #right { display: flex; flex-direction: column; }
71
+ #panels { display: flex; border-bottom: 1px solid var(--edge); }
72
+ #panels .tab { padding: 5px 12px; color: var(--dim); cursor: pointer; }
73
+ #panels .tab.active { color: var(--ink); border-bottom: 2px solid var(--accent); }
74
+ .pane { display: none; padding: 10px 12px; overflow: auto; flex: 1; min-height: 0; }
75
+ .pane.active { display: block; }
76
+ .pane ul { margin: 0; padding-left: 18px; }
77
+ .pane li { margin: 2px 0; }
78
+ .finding.error { color: var(--bad); } .finding.warning { color: var(--warn); }
79
+ #diagram svg { max-width: 100%; height: auto; }
80
+ select { font: inherit; color: var(--ink); background: var(--panel); border: 1px solid var(--edge); border-radius: 4px; margin-bottom: 8px; }
81
+ footer { padding: 4px 12px; border-top: 1px solid var(--edge); background: var(--panel); color: var(--dim); display: flex; gap: 12px; }
82
+ footer #status { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
83
+ footer .bad { color: var(--bad); } footer .ok { color: var(--ok); }
84
+ .jump { cursor: pointer; text-decoration: underline dotted; }
85
+ .entry { display: flex; padding: 1px 10px 1px 16px; cursor: pointer; gap: 6px; }
86
+ .entry:hover { background: var(--bg); }
87
+ .entry .kind { color: var(--dim); font-size: 11px; min-width: 52px; }
88
+ #lower { border-top: 1px solid var(--edge); background: var(--panel); height: 36%; min-height: 120px; display: flex; flex-direction: column; }
89
+ #lowerTabs { display: flex; align-items: center; border-bottom: 1px solid var(--edge); }
90
+ #lowerTabs .tab { padding: 4px 12px; color: var(--dim); cursor: pointer; }
91
+ #lowerTabs .tab.active { color: var(--ink); border-bottom: 2px solid var(--accent); }
92
+ #lowerTabs .grow { flex: 1; } #lowerTabs button { margin-right: 8px; }
93
+ .lane { display: none; overflow: auto; flex: 1; min-height: 0; padding: 6px 12px; }
94
+ .lane.active { display: block; }
95
+ .lane pre { margin: 0 0 8px; font: 12px/1.45 ui-monospace, Menlo, monospace; white-space: pre-wrap; }
96
+ .lane .row { display: flex; gap: 8px; align-items: flex-start; margin-bottom: 6px; flex-wrap: wrap; }
97
+ .lane textarea { font: 12px/1.4 ui-monospace, Menlo, monospace; color: var(--ink); background: var(--bg); border: 1px solid var(--edge); border-radius: 4px; min-width: 280px; min-height: 64px; }
98
+ .lane input[type=text], .lane input[type=number] { font: inherit; color: var(--ink); background: var(--bg); border: 1px solid var(--edge); border-radius: 4px; padding: 3px 6px; }
99
+ .lane .ok, pre.ok { color: var(--ok); } .lane .bad, pre.bad { color: var(--bad); }
100
+ .lane .title { padding: 4px 0; } .lane .rev { display: flex; gap: 10px; align-items: center; margin: 2px 0; }
101
+ .run-mark { color: var(--ok); cursor: pointer; padding-left: 3px; } .run-gutter { width: 16px; }
102
+ #palette { position: fixed; inset: 0; background: rgba(0, 0, 0, .35); z-index: 20; display: flex; justify-content: center; align-items: flex-start; padding-top: 10vh; }
103
+ #palette[hidden] { display: none; }
104
+ #palette .box { width: min(720px, 92vw); max-height: 76vh; overflow: auto; background: var(--panel); border: 1px solid var(--edge); border-radius: 6px; padding: 10px; }
105
+ #palette .head { display: flex; gap: 10px; align-items: center; } #paletteInput { flex: 1; font: inherit; color: var(--ink); background: var(--bg); border: 1px solid var(--edge); border-radius: 4px; padding: 6px 8px; }
106
+ .pitem { padding: 3px 6px; cursor: pointer; border-radius: 3px; } .pitem:hover { background: var(--bg); }
107
+ #paletteDetail { margin-top: 8px; } #paletteDetail textarea, #paletteDetail input { width: 100%; margin: 4px 0; }
108
+ #turns .turn { margin: 2px 0; } #turns .who { color: var(--dim); font-size: 11px; text-transform: uppercase; margin-right: 6px; }
109
+ #turns .agent .who { color: var(--accent); }
110
+ #askForm { display: flex; gap: 8px; margin-top: 6px; } #prompt { flex: 1; font: inherit; color: var(--ink); background: var(--bg); border: 1px solid var(--edge); border-radius: 4px; padding: 5px 8px; }
111
+ #changes .file-diff { margin: 6px 0; border: 1px solid var(--edge); border-radius: 4px; } #changes .head { display: flex; gap: 8px; padding: 3px 8px; background: var(--bg); }
112
+ #changes pre { margin: 0; padding: 4px 8px; font: 12px/1.45 ui-monospace, Menlo, monospace; max-height: 200px; overflow: auto; }
113
+ #changes .add { background: rgba(47, 133, 90, .18); } #changes .del { background: rgba(192, 57, 43, .18); }
114
+ #tip { position: fixed; z-index: 10; max-width: 340px; padding: 4px 8px; background: var(--ink); color: var(--bg); border-radius: 4px; display: none; pointer-events: none; }
115
+ table.wire { border-collapse: collapse; margin-bottom: 12px; } table.wire td, table.wire th { border-bottom: 1px solid var(--edge); padding: 2px 10px 2px 0; text-align: left; }
116
+ #diagramView .node, #diagramView .nodeLabel { cursor: pointer; }
117
+ @media (max-width: 760px) { main { grid-template-columns: 1fr; } aside, #splitter { display: none; } }
118
+ </style>
119
+ </head>
120
+ <body>
121
+ <header>
122
+ <h1>HECKS COMPOSER</h1>
123
+ <span id="modes"><button id="modeEditor" class="on">Editor</button><button id="modeBoard">Board</button></span>
124
+ <span id="workspace" class="grow" style="color:var(--dim)"></span>
125
+ <select id="domainPick" title="Open one of the known domains, as a copy"><option value="">Open a domain…</option></select>
126
+ <button id="save" title="Save the open file (Ctrl/Cmd-S)">Save</button>
127
+ </header>
128
+ <main id="main">
129
+ <aside><div class="title">Explorer</div><div id="files"></div><div class="title">Outline</div><div id="outline"></div></aside>
130
+ <div id="editor"><div id="tabs"></div><div id="cm" style="flex:1;min-height:0;display:flex"></div></div>
131
+ <div id="splitter"></div>
132
+ <section id="right">
133
+ <div id="panels">
134
+ <div class="tab active" data-pane="diagram">Diagram</div>
135
+ <div class="tab" data-pane="statements">Statements</div>
136
+ <div class="tab" data-pane="findings">Findings <span id="findingCount"></span></div>
137
+ <div class="tab" data-pane="wiring">Wiring</div>
138
+ </div>
139
+ <div class="pane active" id="diagram"><select id="diagramPick"></select><div id="diagramView"></div></div>
140
+ <div class="pane" id="statements"><ul id="statementList"></ul></div>
141
+ <div class="pane" id="findings"><ul id="findingList"></ul></div>
142
+ <div class="pane" id="wiring"><div id="wiringView"></div></div>
143
+ </section>
144
+ </main>
145
+ <section id="boardView" hidden>
146
+ <div id="boardBar">
147
+ <button id="boardAddAggregate">+ Aggregate</button>
148
+ <span id="boardActions"></span>
149
+ <span id="boardHint">Click a note to see what you can do with it.</span>
150
+ </div>
151
+ <form id="boardForm" hidden></form>
152
+ <div id="boardMsg"></div>
153
+ <div id="boardScroll"><svg id="boardSvg" xmlns="http://www.w3.org/2000/svg"></svg></div>
154
+ </section>
155
+ <section id="lower">
156
+ <div id="lowerTabs">
157
+ <div class="tab active" data-lane="agent">Agent</div>
158
+ <div class="tab" data-lane="console">Console</div>
159
+ <div class="tab" data-lane="records">Records</div>
160
+ <div class="tab" data-lane="behaviors">Behaviors</div>
161
+ <div class="tab" data-lane="eras">Eras</div>
162
+ <div class="tab" data-lane="fuzz">Fuzz</div>
163
+ <div class="tab" data-lane="timeline">Timeline</div>
164
+ <span class="grow"></span>
165
+ <button id="paletteOpen" title="Hecks commands (Ctrl/Cmd-K)">Commands ⌘K</button>
166
+ </div>
167
+ <div class="lane active" id="lane-agent">
168
+ <div id="turns"></div>
169
+ <div id="changes"></div>
170
+ <form id="askForm"><input id="prompt" placeholder="Ask the agent to change the domain, or how it is wired…" autocomplete="off"><button id="ask">Ask</button></form>
171
+ </div>
172
+ <div class="lane" id="lane-console">
173
+ <div class="row"><select id="consoleVerb"></select><input type="text" id="consoleTo" placeholder="record id"><button id="consoleRun">Run</button><span id="consoleGoal" style="color:var(--dim)"></span></div>
174
+ <div class="row"><textarea id="consoleArgs" spellcheck="false" placeholder="arguments as JSON"></textarea></div>
175
+ <pre id="consoleOut"></pre>
176
+ </div>
177
+ <div class="lane" id="lane-records"><div class="row"><button id="recordsRefresh">Refresh</button></div><div id="recordsView"></div></div>
178
+ <div class="lane" id="lane-behaviors"><div class="row"><button id="behaviorsRun">Run behaviors</button></div><pre id="behaviorsView"></pre></div>
179
+ <div class="lane" id="lane-eras"><div class="row"><button id="erasRefresh">Refresh</button><span style="color:var(--dim)">read-only: the era queries over the workspace</span></div><div id="erasView"></div></div>
180
+ <div class="lane" id="lane-fuzz">
181
+ <div class="row">
182
+ <label>seeds <input type="number" id="fuzzSeeds" value="5" min="1" style="width:70px"></label>
183
+ <label>steps <input type="number" id="fuzzSteps" value="20" min="1" style="width:70px"></label>
184
+ <button id="fuzzStart">Fuzz</button>
185
+ <input type="text" id="conformArtifact" placeholder="Rust artifact path" style="width:240px"><button id="conformStart">Check conformance</button>
186
+ <button id="jobCancel">Cancel</button><span id="jobStatus" style="color:var(--dim)">idle</span>
187
+ </div>
188
+ <pre id="jobOut"></pre>
189
+ </div>
190
+ <div class="lane" id="lane-timeline"><div id="timelineView"></div></div>
191
+ </section>
192
+ <div id="palette" hidden>
193
+ <div class="box">
194
+ <div class="head"><input id="paletteInput" placeholder="Search Hecks commands and queries…" autocomplete="off"><label><input type="checkbox" id="paletteGuarded"> show guarded</label></div>
195
+ <div id="paletteList"></div>
196
+ <div id="paletteDetail"></div>
197
+ </div>
198
+ </div>
199
+ <div id="tip"></div>
200
+ <footer><span id="status">loading</span><span id="timing"></span></footer>
201
+
202
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"></script>
203
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/ruby/ruby.min.js"></script>
204
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/hint/show-hint.min.js"></script>
205
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.9.1/mermaid.min.js"></script>
206
+ <script>
207
+ (() => {
208
+ const $ = (id) => document.getElementById(id);
209
+ const api = async (method, path, body) => {
210
+ const res = await fetch("/composer/" + path, { method, headers: { "content-type": "application/json" }, body: body && JSON.stringify(body) });
211
+ return res.json();
212
+ };
213
+ const dark = matchMedia("(prefers-color-scheme: dark)").matches;
214
+ mermaid.initialize({ startOnLoad: false, theme: dark ? "dark" : "default", securityLevel: "strict" });
215
+
216
+ const state = { docs: {}, disk: {}, open: null, diagrams: {}, diagnostic: null, badLine: null, serial: 0, outline: [], lane: "agent",
217
+ mode: "editor", board: null, boardStale: false, boardSelected: null, boardConnect: null, boardSelect: null };
218
+ const editor = CodeMirror($("cm"), { mode: "ruby", lineNumbers: true, indentUnit: 2, gutters: ["CodeMirror-linenumbers", "run-gutter"], extraKeys: {
219
+ "Ctrl-S": save, "Cmd-S": save, "Ctrl-Space": complete
220
+ } });
221
+ editor.setSize("100%", "100%");
222
+
223
+ const dirty = (path) => state.docs[path].getValue() !== state.disk[path];
224
+ const side = (path) => path.endsWith(".bluebook") ? "bluebook" : path.endsWith(".behaviors") ? "behaviors" : "hecksagon";
225
+ const buffers = () => Object.fromEntries(Object.keys(state.docs).filter(dirty).map((p) => [p, state.docs[p].getValue()]));
226
+
227
+ function renderFiles() {
228
+ $("files").replaceChildren(...Object.keys(state.docs).sort().map((path) => {
229
+ const row = document.createElement("div");
230
+ row.className = "file" + (path === state.open ? " active" : "") + (dirty(path) ? " dirty" : "");
231
+ row.innerHTML = `<span class="dot">●</span><span></span><span class="side">${side(path)}</span>`;
232
+ row.children[1].textContent = path;
233
+ row.onclick = () => openFile(path);
234
+ return row;
235
+ }));
236
+ $("tabs").replaceChildren(...(state.open ? [state.open] : []).map((path) => {
237
+ const tab = document.createElement("div");
238
+ tab.className = "tab active";
239
+ tab.textContent = path + (dirty(path) ? " ●" : "");
240
+ return tab;
241
+ }));
242
+ }
243
+
244
+ function openFile(path) {
245
+ state.open = path;
246
+ editor.swapDoc(state.docs[path]);
247
+ paintBadLine();
248
+ markRunGutter();
249
+ renderFiles();
250
+ editor.focus();
251
+ if (state.lane === "timeline") loadTimeline();
252
+ }
253
+
254
+ function paintBadLine() {
255
+ if (state.badLine !== null) { editor.removeLineClass(state.badLine, "background", "bad-line"); state.badLine = null; }
256
+ const d = state.diagnostic;
257
+ if (d && d.file === state.open && d.line) {
258
+ state.badLine = d.line - 1;
259
+ editor.addLineClass(state.badLine, "background", "bad-line");
260
+ }
261
+ }
262
+
263
+ function showDiagnostic(d) {
264
+ state.diagnostic = d;
265
+ paintBadLine();
266
+ const status = $("status");
267
+ if (!d) { status.className = "ok"; status.textContent = "ok"; return; }
268
+ status.className = "bad";
269
+ const where = d.line ? `${d.file}:${d.line} ` : "";
270
+ const expected = d.expected && d.expected.length ? ` — expected one of: ${d.expected.join(", ")}` : "";
271
+ status.textContent = where + d.message + expected;
272
+ status.title = status.textContent;
273
+ }
274
+
275
+ let parseTimer, checkTimer;
276
+ function changed() {
277
+ renderFiles();
278
+ clearTimeout(parseTimer); clearTimeout(checkTimer);
279
+ const serial = ++state.serial;
280
+ parseTimer = setTimeout(async () => {
281
+ const { diagnostic } = await api("POST", "parse", { buffers: buffers() });
282
+ if (serial === state.serial) showDiagnostic(diagnostic);
283
+ }, 120);
284
+ checkTimer = setTimeout(async () => {
285
+ const result = await api("POST", "check", { buffers: buffers() });
286
+ if (serial === state.serial) applyCheck(result);
287
+ }, 450);
288
+ }
289
+
290
+ function entryNamed(name) {
291
+ const last = String(name).split(/[.:\s]/).filter(Boolean).pop();
292
+ return state.outline.find((e) => e.name === name) || state.outline.find((e) => e.name === last);
293
+ }
294
+
295
+ function jumpTo(file, line) {
296
+ if (file !== state.open) openFile(file);
297
+ editor.setCursor({ line: line - 1, ch: 0 });
298
+ editor.scrollIntoView({ line: line - 1, ch: 0 }, 120);
299
+ editor.focus();
300
+ }
301
+
302
+ function renderOutline() {
303
+ $("outline").replaceChildren(...state.outline.filter((e) => e.kind !== "value_object").map((e) => {
304
+ const row = document.createElement("div");
305
+ row.className = "entry";
306
+ row.innerHTML = '<span class="kind"></span><span></span>';
307
+ row.children[0].textContent = e.kind;
308
+ row.children[1].textContent = e.name;
309
+ row.onclick = () => jumpTo(e.file, e.line);
310
+ return row;
311
+ }));
312
+ }
313
+
314
+ function renderWiring(w) {
315
+ const view = $("wiringView");
316
+ if (!w) { view.textContent = "Nothing is wired until the workspace loads."; return; }
317
+ const table = (heads, rows) => {
318
+ const t = document.createElement("table");
319
+ t.className = "wire";
320
+ t.innerHTML = "<tr>" + heads.map((h) => `<th>${h}</th>`).join("") + "</tr>";
321
+ rows.forEach((r) => { const tr = document.createElement("tr"); r.forEach((c) => { const td = document.createElement("td"); td.textContent = c ?? ""; tr.appendChild(td); }); t.appendChild(tr); });
322
+ return t;
323
+ };
324
+ const facts = [["default adapter", w.default_adapter || "(none)"], ["attaches", w.attaches.join(", ") || "(none)"],
325
+ ["driven by", w.driving.join(", ") || "(any)"]];
326
+ const ports = Object.entries(w.ports).map(([port, adapters]) => [port, adapters.join(", ") || "unbound"]);
327
+ view.replaceChildren(table(["", ""], facts), table(["bind", "aggregate", "adapter", "role"], w.binds.map((b) => [b.verb, b.aggregate, b.adapter, b.role])), table(["port", "adapters"], ports));
328
+ }
329
+
330
+ function applyCheck(result) {
331
+ showDiagnostic(result.diagnostic);
332
+ $("timing").textContent = result.elapsed_ms == null ? "" : `${result.elapsed_ms} ms`;
333
+ state.outline = result.outline || [];
334
+ renderOutline();
335
+ markRunGutter();
336
+ if (["records", "timeline"].includes(state.lane)) refreshLane(state.lane);
337
+ state.boardStale = Boolean(result.diagnostic);
338
+ if (!result.diagnostic && result.board) state.board = result.board;
339
+ if (state.mode === "board") renderBoard();
340
+ if (result.diagnostic) return;
341
+ renderWiring(result.wiring);
342
+ state.diagrams = result.diagrams;
343
+ const pick = $("diagramPick");
344
+ const names = Object.keys(result.diagrams);
345
+ const keep = names.includes(pick.value) ? pick.value : (names.find((n) => n.endsWith("_surface.mmd")) || names[0]);
346
+ pick.replaceChildren(...names.map((n) => new Option(n.replace(/\.mmd$/, ""), n)));
347
+ pick.value = keep;
348
+ drawDiagram();
349
+ $("statementList").replaceChildren(...result.statements.map((s) => Object.assign(document.createElement("li"), { textContent: s })));
350
+ $("findingList").replaceChildren(...result.findings.map((f) => {
351
+ const li = Object.assign(document.createElement("li"), { className: "finding " + f.severity, textContent: `${f.kind} ${f.subject} — ${f.message}` });
352
+ const hit = entryNamed(f.subject);
353
+ if (hit) { li.classList.add("jump"); li.onclick = () => jumpTo(hit.file, hit.line); }
354
+ return li;
355
+ }));
356
+ $("findingCount").textContent = result.findings.length ? `(${result.findings.length})` : "";
357
+ }
358
+
359
+ async function drawDiagram() {
360
+ const text = state.diagrams[$("diagramPick").value];
361
+ if (!text) { $("diagramView").replaceChildren(); return; }
362
+ try {
363
+ const { svg } = await mermaid.render("d" + Date.now(), text);
364
+ $("diagramView").innerHTML = svg;
365
+ } catch (e) {
366
+ $("diagramView").textContent = String(e.message || e);
367
+ }
368
+ }
369
+ $("diagramPick").onchange = drawDiagram;
370
+ $("diagramView").addEventListener("click", (e) => {
371
+ const node = e.target.closest(".node, .nodeLabel, .label, text");
372
+ const hit = node && entryNamed(node.textContent.trim());
373
+ if (hit) jumpTo(hit.file, hit.line);
374
+ });
375
+
376
+ const tip = $("tip");
377
+ editor.getWrapperElement().addEventListener("mousemove", (e) => {
378
+ const at = editor.coordsChar({ left: e.clientX, top: e.clientY }, "window");
379
+ const entry = state.outline.find((o) => o.file === state.open && o.line === at.line + 1 && o.summary);
380
+ if (!entry) { tip.style.display = "none"; return; }
381
+ tip.textContent = entry.summary;
382
+ tip.style.left = e.clientX + 14 + "px"; tip.style.top = e.clientY + 14 + "px"; tip.style.display = "block";
383
+ });
384
+ editor.getWrapperElement().addEventListener("mouseleave", () => { tip.style.display = "none"; });
385
+
386
+ async function complete() {
387
+ const d = state.diagnostic;
388
+ if (!d || !d.expected || !d.expected.length) return;
389
+ CodeMirror.showHint(editor, () => {
390
+ const cur = editor.getCursor();
391
+ return { list: d.expected, from: cur, to: cur };
392
+ }, { completeSingle: false });
393
+ }
394
+
395
+ async function save() {
396
+ const path = state.open;
397
+ if (!path) return;
398
+ const text = state.docs[path].getValue();
399
+ await api("PUT", "file", { path, text });
400
+ state.disk[path] = text;
401
+ renderFiles();
402
+ if (state.lane === "timeline") loadTimeline();
403
+ }
404
+ $("save").onclick = save;
405
+
406
+ async function saveAll() {
407
+ for (const path of Object.keys(state.docs).filter(dirty)) {
408
+ const text = state.docs[path].getValue();
409
+ await api("PUT", "file", { path, text });
410
+ state.disk[path] = text;
411
+ }
412
+ renderFiles();
413
+ }
414
+
415
+ // Puts what the agent left on disk into the editor's documents without marking them unsaved.
416
+ function adopt(path, text) {
417
+ if (text === null) { delete state.docs[path]; delete state.disk[path]; if (state.open === path) state.open = Object.keys(state.docs).sort()[0] || null; return; }
418
+ state.disk[path] = text;
419
+ if (state.docs[path]) state.docs[path].setValue(text);
420
+ else state.docs[path] = CodeMirror.Doc(text, "ruby");
421
+ }
422
+
423
+ // A line diff by longest common subsequence: enough for files of a few hundred lines.
424
+ function lineDiff(before, after) {
425
+ const a = (before ?? "").split("\n"), b = (after ?? "").split("\n");
426
+ const table = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
427
+ for (let i = a.length - 1; i >= 0; i--) for (let j = b.length - 1; j >= 0; j--)
428
+ table[i][j] = a[i] === b[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);
429
+ const out = []; let i = 0, j = 0;
430
+ while (i < a.length && j < b.length) {
431
+ if (a[i] === b[j]) { out.push([" ", a[i]]); i++; j++; }
432
+ else if (table[i + 1][j] >= table[i][j + 1]) out.push(["-", a[i++]]);
433
+ else out.push(["+", b[j++]]);
434
+ }
435
+ while (i < a.length) out.push(["-", a[i++]]);
436
+ while (j < b.length) out.push(["+", b[j++]]);
437
+ return out;
438
+ }
439
+
440
+ function showChanges(changed) {
441
+ const box = $("changes");
442
+ const paths = Object.keys(changed);
443
+ if (!paths.length) { box.replaceChildren(); return; }
444
+ const files = paths.map((path) => {
445
+ const card = document.createElement("div");
446
+ card.className = "file-diff";
447
+ const pre = document.createElement("pre");
448
+ for (const [mark, line] of lineDiff(changed[path].before, changed[path].after)) {
449
+ if (mark === " ") continue;
450
+ const row = document.createElement("div");
451
+ row.className = mark === "+" ? "add" : "del";
452
+ row.textContent = mark + " " + line;
453
+ pre.appendChild(row);
454
+ }
455
+ const head = document.createElement("div");
456
+ head.className = "head";
457
+ head.textContent = path + (changed[path].before === null ? " (new)" : "");
458
+ card.append(head, pre);
459
+ return card;
460
+ });
461
+ const keep = Object.assign(document.createElement("button"), { textContent: "Keep" });
462
+ const undo = Object.assign(document.createElement("button"), { textContent: "Undo" });
463
+ keep.onclick = () => box.replaceChildren();
464
+ undo.onclick = async () => {
465
+ for (const path of paths) {
466
+ const before = changed[path].before;
467
+ if (before === null) await api("DELETE", "file", { path });
468
+ else await api("PUT", "file", { path, text: before });
469
+ adopt(path, before);
470
+ }
471
+ if (state.open && state.docs[state.open]) openFile(state.open);
472
+ box.replaceChildren();
473
+ renderFiles();
474
+ applyCheck(await api("POST", "check", { buffers: {} }));
475
+ };
476
+ const bar = document.createElement("div");
477
+ bar.append(keep, " ", undo);
478
+ box.replaceChildren(...files, bar);
479
+ }
480
+
481
+ function showTurns(draft) {
482
+ $("turns").replaceChildren(...((draft && draft.turns) || []).map((turn) => {
483
+ const row = document.createElement("div");
484
+ row.className = "turn " + turn.speaker;
485
+ row.innerHTML = '<span class="who"></span><span></span>';
486
+ row.children[0].textContent = turn.speaker;
487
+ row.children[1].textContent = turn.said;
488
+ return row;
489
+ }));
490
+ }
491
+
492
+ $("askForm").onsubmit = async (event) => {
493
+ event.preventDefault();
494
+ const prompt = $("prompt").value.trim();
495
+ if (!prompt) return;
496
+ $("ask").disabled = true;
497
+ editor.setOption("readOnly", "nocursor");
498
+ $("status").className = ""; $("status").textContent = "asking the agent…";
499
+ try {
500
+ await saveAll();
501
+ const result = await api("POST", "ask", { prompt });
502
+ if (result.error) { $("status").className = "bad"; $("status").textContent = result.error; return; }
503
+ for (const [path, { after }] of Object.entries(result.changed)) adopt(path, after);
504
+ if (state.open && state.docs[state.open]) { editor.swapDoc(state.docs[state.open]); }
505
+ $("prompt").value = "";
506
+ renderFiles();
507
+ applyCheck(result.check);
508
+ showChanges(result.changed);
509
+ if (result.layout) { showRight(result.layout.right); showLower(result.layout.bottom); }
510
+ showTurns((await api("GET", "draft")).draft);
511
+ if (!result.check.diagnostic) { $("status").className = "ok"; $("status").textContent = `${result.intent}: ${result.note}`; }
512
+ } finally {
513
+ editor.setOption("readOnly", false);
514
+ $("ask").disabled = false;
515
+ }
516
+ };
517
+
518
+ function showRight(name) {
519
+ document.querySelectorAll("#panels .tab, .pane").forEach((el) => el.classList.remove("active"));
520
+ document.querySelector(`#panels .tab[data-pane="${name}"]`)?.classList.add("active");
521
+ $(name)?.classList.add("active");
522
+ }
523
+ document.querySelectorAll("#panels .tab").forEach((tab) => tab.onclick = () => showRight(tab.dataset.pane));
524
+
525
+ // The lower drawer: one lane at a time, each loading what it shows when it opens.
526
+ function showLower(name) {
527
+ document.querySelectorAll("#lowerTabs .tab").forEach((tab) => tab.classList.toggle("active", tab.dataset.lane === name));
528
+ document.querySelectorAll(".lane").forEach((lane) => lane.classList.toggle("active", lane.id === "lane-" + name));
529
+ state.lane = name;
530
+ return refreshLane(name);
531
+ }
532
+ document.querySelectorAll("#lowerTabs .tab").forEach((tab) => tab.onclick = () => showLower(tab.dataset.lane));
533
+
534
+ function refreshLane(name) {
535
+ const loaders = { console: loadConsole, records: loadRecords, behaviors: runBehaviors, eras: loadEras, fuzz: renderJob, timeline: loadTimeline };
536
+ return loaders[name] ? loaders[name]() : Promise.resolve();
537
+ }
538
+
539
+ function section(title, lines) {
540
+ const box = document.createElement("div");
541
+ const head = Object.assign(document.createElement("div"), { className: "title", textContent: title });
542
+ const pre = Object.assign(document.createElement("pre"), { textContent: lines.join("\n") || "(none)" });
543
+ box.append(head, pre);
544
+ return box;
545
+ }
546
+
547
+ // Console: dispatch a command of the workspace against a booted copy of it.
548
+ const consoleState = { commands: [] };
549
+ const currentCommand = () => consoleState.commands.find((c) => c.verb === $("consoleVerb").value);
550
+
551
+ async function loadConsole() {
552
+ const result = await api("POST", "live/commands", { buffers: buffers() });
553
+ if (result.error) { $("consoleOut").className = "bad"; $("consoleOut").textContent = result.error; return; }
554
+ consoleState.commands = result.commands;
555
+ const pick = $("consoleVerb");
556
+ const keep = pick.value;
557
+ pick.replaceChildren(...result.commands.map((c) => new Option(`${c.aggregate}.${c.command}`, c.verb)));
558
+ if (result.commands.some((c) => c.verb === keep)) pick.value = keep;
559
+ fillConsoleForm();
560
+ }
561
+
562
+ function fillConsoleForm() {
563
+ const command = currentCommand();
564
+ if (!command) return;
565
+ $("consoleTo").style.display = command.creates ? "none" : "";
566
+ $("consoleArgs").value = JSON.stringify(command.template, null, 2);
567
+ $("consoleGoal").textContent = command.goal || "";
568
+ }
569
+ $("consoleVerb").onchange = fillConsoleForm;
570
+
571
+ $("consoleRun").onclick = async () => {
572
+ const command = currentCommand();
573
+ if (!command) return;
574
+ let args;
575
+ try { args = JSON.parse($("consoleArgs").value || "{}"); }
576
+ catch (e) { $("consoleOut").className = "bad"; $("consoleOut").textContent = "the arguments are not JSON: " + e.message; return; }
577
+ const reply = await api("POST", "live/dispatch", { buffers: buffers(), verb: command.verb, to: command.creates ? null : $("consoleTo").value, with: args });
578
+ $("consoleOut").className = reply.ok ? "ok" : "bad";
579
+ $("consoleOut").textContent = reply.ok ? `${reply.events.join(", ")}\n${JSON.stringify(reply.instance, null, 2)}` : `${reply.kind || "error"}: ${reply.error}`;
580
+ };
581
+
582
+ // A play mark beside each command of the open file opens it in the Console.
583
+ function markRunGutter() {
584
+ editor.clearGutter("run-gutter");
585
+ let aggregate = null;
586
+ for (const entry of state.outline.filter((o) => o.file === state.open)) {
587
+ if (entry.kind === "aggregate") aggregate = entry.name;
588
+ if (entry.kind !== "command") continue;
589
+ const mark = Object.assign(document.createElement("span"), { textContent: "▶", className: "run-mark", title: `Run ${entry.name} in the Console` });
590
+ const owner = aggregate;
591
+ mark.onclick = () => runFromGutter(owner, entry.name);
592
+ editor.setGutterMarker(entry.line - 1, "run-gutter", mark);
593
+ }
594
+ }
595
+
596
+ async function runFromGutter(aggregate, command) {
597
+ await showLower("console");
598
+ const found = consoleState.commands.find((c) => c.aggregate === aggregate && c.command === command);
599
+ if (found) { $("consoleVerb").value = found.verb; fillConsoleForm(); }
600
+ }
601
+
602
+ // Records and history of the booted copy the Console runs against.
603
+ async function loadRecords() {
604
+ const reply = await api("POST", "live/records", { buffers: buffers() });
605
+ if (reply.error) { $("recordsView").textContent = reply.error; return; }
606
+ const tables = Object.entries(reply.records).map(([name, rows]) => section(`${name} (${rows.length})`, rows.map((row) => JSON.stringify(row))));
607
+ const events = reply.history.slice().reverse().map((e) => `${e.name} ${e.id ?? ""} ${JSON.stringify(e.payload)}`);
608
+ $("recordsView").replaceChildren(...tables, section(`History (${reply.history.length})`, events));
609
+ }
610
+ $("recordsRefresh").onclick = loadRecords;
611
+
612
+ async function runBehaviors() {
613
+ const reply = await api("POST", "behaviors", { buffers: buffers() });
614
+ if (!reply.files || !reply.files.length) { $("behaviorsView").textContent = "This workspace has no .behaviors file."; return; }
615
+ const lines = [];
616
+ for (const file of reply.files) {
617
+ lines.push(file.path);
618
+ if (file.parse_error) lines.push(" PARSE ERROR — " + file.parse_error);
619
+ for (const run of file.runs) {
620
+ lines.push(` ${run.status === "pass" ? "ok " : "FAIL"} ${run.description}`);
621
+ if (run.message) lines.push(" " + run.message);
622
+ }
623
+ }
624
+ const s = reply.summary;
625
+ lines.push("", `${s.total} tests: ${s.passed} passed, ${s.failed} failed, ${s.errored} errored`);
626
+ $("behaviorsView").textContent = lines.join("\n");
627
+ }
628
+ $("behaviorsRun").onclick = runBehaviors;
629
+
630
+ async function loadEras() {
631
+ const reply = await api("GET", "eras");
632
+ $("erasView").replaceChildren(...Object.entries(reply).map(([name, run]) => section(name.replace(/_/g, " "), [run.output || run.error || ""])));
633
+ }
634
+ $("erasRefresh").onclick = loadEras;
635
+
636
+ // Fuzz and conformance run as one background job on the saved files; the page polls it.
637
+ let jobTimer = null;
638
+ function startJob(verb, args) {
639
+ if (Object.keys(state.docs).some(dirty)) { $("jobOut").textContent = "Save your files first: a job runs on what is on disk."; return; }
640
+ if (state.diagnostic) { $("jobOut").textContent = "Fix the refusal first: a job needs a workspace that loads."; return; }
641
+ api("POST", "job", { kind: "command", verb, args }).then((reply) => { if (!reply.started) $("jobOut").textContent = reply.error; renderJob(); });
642
+ }
643
+ $("fuzzStart").onclick = () => startJob("fuzz_run.fuzz", { "domain.value": "$WORKSPACE", "seeds.value": $("fuzzSeeds").value, "steps.value": $("fuzzSteps").value, "workers.value": "1", "adapter.value": "memory" });
644
+ $("conformStart").onclick = () => startJob("build.fuzz_conformance", { "domain.value": "$WORKSPACE", "artifact.value": $("conformArtifact").value });
645
+ $("jobCancel").onclick = async () => { await api("DELETE", "job"); renderJob(); };
646
+
647
+ async function renderJob() {
648
+ const job = await api("GET", "job");
649
+ $("jobStatus").textContent = job.status + (job.verb ? ` — ${job.verb}` : "");
650
+ $("jobOut").textContent = job.output || "";
651
+ clearTimeout(jobTimer);
652
+ if (job.status === "running") jobTimer = setTimeout(renderJob, 2000);
653
+ }
654
+
655
+ // Timeline: what the open file held at each save, with a way back to any of it.
656
+ async function loadTimeline() {
657
+ const path = state.open;
658
+ if (!path) return;
659
+ const { revisions } = await api("POST", "timeline", { path });
660
+ const box = $("timelineView");
661
+ if (!revisions.length) { box.textContent = "Nothing saved for this file yet. Save it to start its timeline."; return; }
662
+ box.replaceChildren(...revisions.map((rev) => {
663
+ const row = Object.assign(document.createElement("div"), { className: "rev" });
664
+ const load = Object.assign(document.createElement("button"), { textContent: "Load into editor" });
665
+ load.onclick = () => { state.docs[path].setValue(rev.text ?? ""); changed(); };
666
+ row.append(Object.assign(document.createElement("span"), { textContent: `${rev.at} ${rev.name}` }), load);
667
+ return row;
668
+ }));
669
+ }
670
+
671
+ // The command palette: Hecks's own commands and queries, the guarded ones hidden until asked for.
672
+ let catalog = null;
673
+ async function openPalette() {
674
+ $("palette").hidden = false;
675
+ $("paletteInput").value = "";
676
+ $("paletteDetail").replaceChildren();
677
+ $("paletteInput").focus();
678
+ if (!catalog) catalog = (await api("GET", "catalog")).catalog;
679
+ renderPalette();
680
+ }
681
+ function closePalette() { $("palette").hidden = true; editor.focus(); }
682
+
683
+ function paletteRows() {
684
+ const needle = $("paletteInput").value.toLowerCase();
685
+ const guarded = $("paletteGuarded").checked;
686
+ return (catalog || []).filter((r) => (guarded || !r.guarded) && `${r.verb} ${r.goal || ""}`.toLowerCase().includes(needle)).slice(0, 60);
687
+ }
688
+
689
+ function renderPalette() {
690
+ $("paletteList").replaceChildren(...paletteRows().map((row) => {
691
+ const item = Object.assign(document.createElement("div"), { className: "pitem" });
692
+ item.textContent = `${row.kind === "query" ? "?" : "!"} ${row.verb}${row.guarded ? " [guarded]" : ""} — ${row.goal || ""}`;
693
+ item.onclick = () => choose(row);
694
+ return item;
695
+ }));
696
+ }
697
+
698
+ function parseArgs(text) {
699
+ return Object.fromEntries(text.split("\n").map((l) => l.trim()).filter(Boolean).map((l) => [l.slice(0, l.indexOf("=")), l.slice(l.indexOf("=") + 1)]));
700
+ }
701
+
702
+ async function choose(row) {
703
+ const { help } = await api("POST", "help", { kind: row.kind, verb: row.verb });
704
+ const detail = $("paletteDetail");
705
+ const text = Object.assign(document.createElement("pre"), { textContent: help });
706
+ const args = Object.assign(document.createElement("textarea"), { placeholder: "name.value=…, one per line; $WORKSPACE is this workspace", rows: 3 });
707
+ const confirm = Object.assign(document.createElement("input"), { placeholder: `type RUN ${row.verb} to run a guarded command` });
708
+ const out = Object.assign(document.createElement("pre"), { textContent: "" });
709
+ const run = Object.assign(document.createElement("button"), { textContent: "Run" });
710
+ run.onclick = async () => {
711
+ const reply = await api("POST", "run", { kind: row.kind, verb: row.verb, args: parseArgs(args.value), confirm: confirm.value });
712
+ out.className = reply.ok ? "ok" : "bad";
713
+ out.textContent = reply.ok ? reply.output : (reply.error || reply.output);
714
+ };
715
+ detail.replaceChildren(text, args, ...(row.guarded ? [confirm] : []), run, out);
716
+ }
717
+
718
+ $("paletteOpen").onclick = openPalette;
719
+ $("paletteInput").oninput = renderPalette;
720
+ $("paletteGuarded").onchange = renderPalette;
721
+ $("paletteInput").onkeydown = (e) => { if (e.key === "Enter") { const first = paletteRows()[0]; if (first) choose(first); } };
722
+ $("palette").onclick = (e) => { if (e.target === $("palette")) closePalette(); };
723
+ addEventListener("keydown", (e) => {
724
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { e.preventDefault(); openPalette(); }
725
+ if (e.key === "Escape" && !$("palette").hidden) closePalette();
726
+ }, true);
727
+
728
+ $("splitter").onmousedown = (down) => {
729
+ const move = (e) => $("main").style.setProperty("--right", Math.max(240, innerWidth - e.clientX) + "px");
730
+ const up = () => { removeEventListener("mousemove", move); removeEventListener("mouseup", up); };
731
+ addEventListener("mousemove", move); addEventListener("mouseup", up);
732
+ down.preventDefault();
733
+ };
734
+
735
+ editor.on("change", (cm, change) => { if (change.origin !== "setValue") changed(); });
736
+ editor.on("inputRead", (cm, change) => { if (/\w/.test(change.text.join("")) && state.diagnostic && state.diagnostic.expected.length) complete(); });
737
+
738
+ // The board: the chapter as event-storming notes, one lane per aggregate, read from the check's
739
+ // `board`. Every action on a note is one edit of the text (see BoardEdit), applied to the editor
740
+ // like typing, so the board never holds anything the text does not.
741
+ const NOTE = { w: 140, h: 46, pillW: 130, pillH: 26 };
742
+ const svgEl = (tag, attrs = {}, text) => {
743
+ const node = document.createElementNS("http://www.w3.org/2000/svg", tag);
744
+ for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, value);
745
+ if (text !== undefined) node.textContent = text;
746
+ return node;
747
+ };
748
+ const clip = (text, max = 17) => (text.length > max ? text.slice(0, max - 1) + "…" : text);
749
+ const past = (name) => (name.endsWith("e") ? name + "d" : name + "ed");
750
+
751
+ function setMode(mode) {
752
+ state.mode = mode;
753
+ $("main").hidden = mode === "board";
754
+ $("boardView").hidden = mode !== "board";
755
+ $("modeEditor").classList.toggle("on", mode === "editor");
756
+ $("modeBoard").classList.toggle("on", mode === "board");
757
+ if (mode === "board") renderBoard(); else editor.refresh();
758
+ }
759
+ $("modeEditor").onclick = () => setMode("editor");
760
+ $("modeBoard").onclick = () => setMode("board");
761
+
762
+ // Where every note goes: an aggregate's lane holds its commands left to right, each with its events beside it.
763
+ function layoutBoard(board) {
764
+ const notes = [], lanes = [], commandAt = {}, eventAt = {};
765
+ let y = 14;
766
+ for (const aggregate of board.aggregates) {
767
+ const stack = Math.max(1, ...aggregate.commands.map((c) => Math.max(1, c.emits.length)));
768
+ const height = stack * (NOTE.h + 10) + 16;
769
+ lanes.push({ y, height });
770
+ notes.push({ kind: "aggregate", key: `aggregate:${aggregate.name}`, label: aggregate.name, x: 16, y: y + 8, w: NOTE.w, h: NOTE.h, at: aggregate.at, aggregate: aggregate.name });
771
+ let x = 190;
772
+ for (const command of aggregate.commands) {
773
+ const note = { kind: "command", key: `command:${aggregate.name}.${command.name}`, label: command.name, x, y: y + 8, w: NOTE.w, h: NOTE.h, at: command.at, aggregate: aggregate.name, command: command.name };
774
+ notes.push(note); commandAt[note.key] = note;
775
+ command.emits.forEach((event, row) => {
776
+ const ev = { kind: "event", key: `event:${aggregate.name}.${event}`, label: event, x: x + NOTE.w + 34, y: y + 8 + row * (NOTE.h + 10), w: NOTE.w, h: NOTE.h, at: command.at, aggregate: aggregate.name, command: command.name, event };
777
+ notes.push(ev); eventAt[ev.key] = { ...ev, owner: note };
778
+ });
779
+ x += NOTE.w * 2 + 74;
780
+ }
781
+ y += height + 12;
782
+ }
783
+ return { notes, lanes, commandAt, eventAt, width: Math.max(900, ...notes.map((n) => n.x + n.w + 40)), height: y + 16 };
784
+ }
785
+
786
+ // The arrows: a command to each event it emits, and a policy from an event to the command it triggers.
787
+ function boardLinks(board, plan) {
788
+ const links = [], pills = [], unplaced = [];
789
+ for (const note of plan.notes.filter((n) => n.kind === "event")) {
790
+ const from = plan.commandAt[`command:${note.aggregate}.${note.command}`];
791
+ links.push({ d: `M${from.x + from.w},${from.y + from.h / 2} L${note.x},${note.y + note.h / 2}`, cls: "link" });
792
+ }
793
+ for (const policy of board.policies) {
794
+ const source = plan.eventAt[`event:${policy.on}`], target = plan.commandAt[`command:${policy.trigger}`];
795
+ if (!source || !target) { unplaced.push(policy.name); continue; }
796
+ const [sx, sy, tx, ty] = [source.x + source.w, source.y + source.h / 2, target.x, target.y + target.h / 2];
797
+ const forward = tx > sx + 40, reach = forward ? Math.max(60, (tx - sx) / 2) : 90, lift = forward ? 0 : -70;
798
+ const [c1x, c1y, c2x, c2y] = [sx + reach, sy + lift, tx - reach, ty + lift];
799
+ links.push({ d: `M${sx},${sy} C${c1x},${c1y} ${c2x},${c2y} ${tx},${ty}`, cls: "link policy" });
800
+ pills.push({ kind: "policy", key: `policy:${policy.name}`, label: policy.name, at: policy.at, w: NOTE.pillW, h: NOTE.pillH,
801
+ x: (sx + 3 * c1x + 3 * c2x + tx) / 8 - NOTE.pillW / 2, y: (sy + 3 * c1y + 3 * c2y + ty) / 8 - NOTE.pillH / 2, policy });
802
+ }
803
+ return { links, pills, unplaced };
804
+ }
805
+
806
+ function noteNode(note) {
807
+ const picked = state.boardSelected === note.key;
808
+ const target = state.boardConnect && note.kind === "command";
809
+ const group = svgEl("g", { class: `note ${note.kind}${picked ? " selected" : ""}${target ? " target" : ""}`, transform: `translate(${note.x},${note.y})` });
810
+ const pill = note.kind === "policy";
811
+ group.append(svgEl("rect", { width: note.w, height: note.h, rx: pill ? 13 : 4 }), svgEl("text", { x: 8, y: pill ? 17 : 31 }, clip(note.label, pill ? 18 : 17)), svgEl("title", {}, note.label));
812
+ if (!pill) group.append(svgEl("text", { x: 8, y: 13, class: "kind" }, note.kind));
813
+ group.onclick = (event) => { event.stopPropagation(); clickNote(note); };
814
+ return group;
815
+ }
816
+
817
+ function renderBoard() {
818
+ const svg = $("boardSvg");
819
+ svg.replaceChildren();
820
+ const board = state.board;
821
+ if (!board) {
822
+ svg.setAttribute("width", 600); svg.setAttribute("height", 60);
823
+ svg.append(svgEl("text", { x: 20, y: 34, fill: "currentColor" }, "Nothing to draw until the workspace loads."));
824
+ state.boardIndex = {}; renderBoardActions(); return;
825
+ }
826
+ const plan = layoutBoard(board);
827
+ const { links, pills, unplaced } = boardLinks(board, plan);
828
+ const notes = [...plan.notes, ...pills];
829
+ state.boardIndex = Object.fromEntries(notes.map((n) => [n.key, n]));
830
+ if (state.boardSelect && state.boardIndex[state.boardSelect]) state.boardSelected = state.boardSelect;
831
+ state.boardSelect = null;
832
+ svg.setAttribute("width", plan.width); svg.setAttribute("height", plan.height);
833
+ const arrow = svgEl("marker", { id: "arrow", markerWidth: 8, markerHeight: 8, refX: 7, refY: 4, orient: "auto" });
834
+ arrow.append(svgEl("path", { d: "M0,0 L8,4 L0,8 z", fill: "currentColor" }));
835
+ svg.append(svgEl("defs"), ...plan.lanes.map((l) => svgEl("rect", { class: "lane-band", x: 4, y: l.y, width: plan.width - 8, height: l.height, rx: 6 })));
836
+ svg.firstChild.append(arrow);
837
+ svg.append(...links.map((l) => svgEl("path", { d: l.d, class: l.cls, "marker-end": "url(#arrow)" })), ...notes.map(noteNode));
838
+ $("boardHint").textContent = hintFor(unplaced);
839
+ renderBoardActions();
840
+ }
841
+
842
+ function hintFor(unplaced) {
843
+ if (state.boardStale) return "The text does not parse yet, so this is the last board that did.";
844
+ if (state.boardConnect) return "Click the command this event should trigger. Esc cancels.";
845
+ const extra = unplaced.length ? ` ${unplaced.length} policy note(s) point at something this board cannot place: ${unplaced.join(", ")}.` : "";
846
+ return `Click a note to see what you can do with it.${extra}`;
847
+ }
848
+
849
+ function clickNote(note) {
850
+ if (state.boardConnect && note.kind === "command") return askPolicy(state.boardConnect, note);
851
+ state.boardConnect = null;
852
+ state.boardSelected = note.key;
853
+ renderBoard();
854
+ }
855
+ $("boardSvg").onclick = () => { state.boardSelected = null; state.boardConnect = null; closeBoardForm(); renderBoard(); };
856
+ addEventListener("keydown", (e) => { if (e.key === "Escape" && state.boardConnect) { state.boardConnect = null; renderBoard(); } });
857
+
858
+ function actionButton(label, run) {
859
+ return Object.assign(document.createElement("button"), { textContent: label, onclick: run });
860
+ }
861
+
862
+ // The buttons for the selected note; a delete asks twice, so a stray click cannot take a note away.
863
+ function renderBoardActions() {
864
+ const note = state.boardIndex && state.boardIndex[state.boardSelected];
865
+ const box = $("boardActions");
866
+ box.replaceChildren();
867
+ if (!note) return;
868
+ const jump = actionButton("Jump to source", () => { if (note.at) { setMode("editor"); jumpTo(note.at.file, note.at.line); } });
869
+ const remove = (args, said) => twice("Delete", () => boardEdit("delete", args, null, said));
870
+ const kinds = {
871
+ aggregate: () => [actionButton("+ Command", () => askCommand(note)), jump, remove({ kind: "aggregate", name: note.label }, `Deleted ${note.label}.`)],
872
+ command: () => [actionButton("+ Event", () => askEvent(note)), actionButton("▶ Run", () => runFromGutter(note.aggregate, note.label)), jump, remove({ kind: "command", aggregate: note.aggregate, name: note.label }, `Deleted ${note.label}.`)],
873
+ event: () => [actionButton("Trigger a command →", () => { state.boardConnect = note; renderBoard(); }), jump,
874
+ twice("Delete event", () => boardEdit("delete_event", { aggregate: note.aggregate, command: note.command, name: note.label }, null, `Deleted ${note.label}.`))],
875
+ policy: () => [jump, remove({ kind: "policy", name: note.label }, `Deleted ${note.label}.`)]
876
+ };
877
+ box.append(...kinds[note.kind]());
878
+ }
879
+
880
+ function twice(label, run) {
881
+ const button = actionButton(label, () => {
882
+ if (button.dataset.armed) return run();
883
+ button.dataset.armed = "1"; button.textContent = "Really?";
884
+ setTimeout(() => { delete button.dataset.armed; button.textContent = label; }, 3500);
885
+ });
886
+ return button;
887
+ }
888
+
889
+ function closeBoardForm() { $("boardForm").hidden = true; $("boardForm").replaceChildren(); }
890
+
891
+ // A small inline form; its first field is focused, and Enter submits.
892
+ function boardForm(title, fields, submit) {
893
+ const form = $("boardForm");
894
+ form.hidden = false;
895
+ form.replaceChildren(Object.assign(document.createElement("strong"), { textContent: title }));
896
+ const inputs = fields.map((f) => Object.assign(document.createElement("input"), { name: f.name, placeholder: f.placeholder, value: f.value || "" }));
897
+ const cancel = Object.assign(document.createElement("button"), { textContent: "Cancel", type: "button", onclick: closeBoardForm });
898
+ form.append(...inputs, Object.assign(document.createElement("button"), { textContent: "Add", type: "submit" }), cancel);
899
+ form.onsubmit = (e) => { e.preventDefault(); const values = Object.fromEntries(inputs.map((i) => [i.name, i.value.trim()])); closeBoardForm(); submit(values); };
900
+ inputs[0].focus();
901
+ return inputs;
902
+ }
903
+
904
+ $("boardAddAggregate").onclick = () => boardForm("New aggregate", [{ name: "name", placeholder: "Aggregate, e.g. Invoice" }],
905
+ (v) => boardEdit("add_aggregate", { name: v.name }, `aggregate:${v.name}`, `Added aggregate ${v.name}.`));
906
+
907
+ function askCommand(aggregate) {
908
+ const [name, event] = boardForm(`New command on ${aggregate.label}`, [{ name: "name", placeholder: "Command, e.g. Close" }, { name: "event", placeholder: "Event it emits, e.g. AccountClosed" }],
909
+ (v) => boardEdit("add_command", { aggregate: aggregate.label, name: v.name, event: v.event }, `command:${aggregate.label}.${v.name}`, `Added command ${v.name}.`));
910
+ name.oninput = () => { if (!event.dataset.touched) event.value = name.value ? aggregate.label + past(name.value) : ""; };
911
+ event.oninput = () => { event.dataset.touched = "1"; };
912
+ }
913
+
914
+ function askEvent(command) {
915
+ boardForm(`New event from ${command.label}`, [{ name: "name", placeholder: "Event, e.g. BalanceChanged" }],
916
+ (v) => boardEdit("add_event", { aggregate: command.aggregate, command: command.label, name: v.name }, `event:${command.aggregate}.${v.name}`, `Added event ${v.name}.`));
917
+ }
918
+
919
+ function askPolicy(event, command) {
920
+ state.boardConnect = null;
921
+ boardForm(`When ${event.label} happens, trigger ${command.label}`, [{ name: "name", placeholder: "Policy name", value: `${command.label}When${event.label}` }],
922
+ (v) => boardEdit("add_policy", { name: v.name, on: `${event.aggregate}.${event.label}`, trigger: `${command.aggregate}.${command.label}` }, `policy:${v.name}`, `Added policy ${v.name}.`));
923
+ }
924
+
925
+ // Asks the server for the edit, then puts the patched file into the editor as an ordinary change, so Undo works.
926
+ async function boardEdit(op, args, select, said) {
927
+ const message = $("boardMsg");
928
+ message.className = "";
929
+ const reply = await api("POST", "board/edit", { buffers: buffers(), op, args });
930
+ if (reply.error) { message.className = "bad"; message.textContent = reply.error; return; }
931
+ const doc = state.docs[reply.path];
932
+ doc.replaceRange(reply.text, { line: 0, ch: 0 }, { line: doc.lastLine(), ch: doc.getLine(doc.lastLine()).length });
933
+ state.boardSelect = select;
934
+ if (!select) state.boardSelected = null;
935
+ message.textContent = `${said} The change is in the editor, unsaved; Ctrl-Z there undoes it.`;
936
+ changed();
937
+ }
938
+
939
+ // Loads the server's workspace into the editor, forgetting whatever the last one left on screen.
940
+ async function loadWorkspace() {
941
+ const { files } = await api("GET", "files");
942
+ Object.assign(state, { docs: {}, disk: {}, open: null, diagrams: {}, diagnostic: null, badLine: null, outline: [],
943
+ board: null, boardStale: false, boardSelected: null, boardConnect: null });
944
+ consoleState.commands = [];
945
+ for (const id of ["turns", "changes", "consoleOut", "recordsView", "behaviorsView", "erasView", "jobOut", "timelineView"]) $(id).textContent = "";
946
+ for (const { path, text } of files) { state.disk[path] = text; state.docs[path] = CodeMirror.Doc(text, "ruby"); }
947
+ const first = Object.keys(state.docs).sort().find((p) => p.endsWith(".bluebook")) || Object.keys(state.docs).sort()[0];
948
+ if (first) openFile(first); else editor.swapDoc(CodeMirror.Doc("", "ruby"));
949
+ $("workspace").textContent = `${files.length} files`;
950
+ renderFiles();
951
+ renderOutline();
952
+ applyCheck(await api("POST", "check", { buffers: {} }));
953
+ if (state.lane !== "agent") refreshLane(state.lane);
954
+ }
955
+
956
+ // The known domains, grouped; choosing one opens a scratch copy of it, never the real files.
957
+ async function loadDomains() {
958
+ const { domains } = await api("GET", "domains");
959
+ const pick = $("domainPick");
960
+ const groups = {};
961
+ for (const domain of domains) {
962
+ groups[domain.group] ||= Object.assign(document.createElement("optgroup"), { label: domain.group });
963
+ groups[domain.group].append(new Option(domain.id, domain.id));
964
+ }
965
+ pick.replaceChildren(new Option("Open a domain…", ""), ...Object.values(groups));
966
+ }
967
+
968
+ $("domainPick").onchange = async () => {
969
+ const pick = $("domainPick");
970
+ const id = pick.value;
971
+ if (!id) return;
972
+ if (Object.keys(state.docs).some(dirty)) {
973
+ $("status").className = "bad"; $("status").textContent = "Save or undo your changes before opening another domain.";
974
+ pick.value = "";
975
+ return;
976
+ }
977
+ $("status").className = ""; $("status").textContent = `opening ${id}…`;
978
+ const reply = await api("POST", "open", { id });
979
+ if (reply.error) { $("status").className = "bad"; $("status").textContent = reply.error; return; }
980
+ await loadWorkspace();
981
+ $("workspace").textContent = `${id} (a copy) · ${Object.keys(state.docs).length} files`;
982
+ };
983
+
984
+ loadWorkspace();
985
+ loadDomains();
986
+ })();
987
+ </script>
988
+ </body>
989
+ </html>