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,144 @@
1
+ require_relative "board_edit/command_text"
2
+ require_relative "board_edit/source"
3
+
4
+ module Hecks
5
+ module Composer
6
+ # What the board does to the text, on the hecksagon side: each action on a note is one exact
7
+ # edit of the bluebook, with no model in the loop, so a board change is instant and the text
8
+ # stays the single source of truth. It answers the patched file; the editor applies it as an
9
+ # ordinary edit, so Undo takes it back.
10
+ #
11
+ # Fields, rules and givens are not on the board: they stay in the text, where the agent can
12
+ # fill them in.
13
+ module BoardEdit
14
+ # Raised when an action cannot be carried out, in the board's own words.
15
+ class Refused < StandardError; end
16
+
17
+ NAME = /\A[A-Z][A-Za-z0-9]*\z/
18
+ DOTTED = /\A([A-Z][A-Za-z0-9]*)\.([A-Z][A-Za-z0-9]*)\z/
19
+
20
+ # Each action and the arguments it takes.
21
+ OPERATIONS = {
22
+ "add_aggregate" => %i[name], "add_command" => %i[aggregate name event],
23
+ "add_event" => %i[aggregate command name], "add_policy" => %i[name on trigger],
24
+ "delete" => %i[kind name aggregate], "delete_event" => %i[aggregate command name]
25
+ }.freeze
26
+
27
+ module_function
28
+
29
+ # @param stage [String] a staged directory holding the workspace as it is on screen
30
+ # @param action [String] one of the keys of `OPERATIONS`
31
+ # @param args [Hash{String => String}] the action's arguments
32
+ # @return [Hash] the patched file: `path`, its whole new `text`, and the `line` to show
33
+ # @raise [Refused] if the action is unknown or cannot be carried out
34
+ def call(stage:, action:, args:)
35
+ wanted = OPERATIONS.fetch(action) { raise Refused, "the board has no action called #{action.inspect}" }
36
+ values = wanted.to_h { |key| [key, args[key.to_s].to_s.strip] }
37
+ public_send(action, Source.new(stage), **values)
38
+ end
39
+
40
+ # @api private
41
+ def add_aggregate(source, name:)
42
+ named!(name, "an aggregate")
43
+ refuse!("#{name} is already an aggregate") if source.places.of("aggregate", name)
44
+ header = source.chapter
45
+ source.insert(header[:file], source.block_end(header), aggregate_lines(name))
46
+ end
47
+
48
+ # A new aggregate is known by a key of its own, so its first command can create it.
49
+ # @api private
50
+ def aggregate_lines(name)
51
+ ["", " aggregate \"#{name}\" do", " description \"Describe #{name}.\"", "",
52
+ " attribute :key, #{name}Key", " identified_by :key", "",
53
+ " value_object \"#{name}Key\" do", " attribute :value, String", " end", " end"]
54
+ end
55
+
56
+ # @api private
57
+ def add_command(source, aggregate:, name:, event:)
58
+ named!(name, "a command")
59
+ named!(event, "an event") unless event.empty?
60
+ where = aggregate_at(source, aggregate)
61
+ refuse!("#{aggregate} already has a command #{name}") if source.places.of("command", name, owner: aggregate)
62
+ close = source.block_end(where)
63
+ block = source.slice(where[:file], where[:line], close)
64
+ lines = CommandText.lines(block: block, indent: source.indent(where), aggregate: aggregate, name: name, event: event)
65
+ source.insert(where[:file], close, lines)
66
+ end
67
+
68
+ # @api private
69
+ def add_event(source, aggregate:, command:, name:)
70
+ named!(name, "an event")
71
+ where = command_at(source, aggregate, command)
72
+ refuse!("#{command} already emits #{name}") if emit_line(source, where, name)
73
+ pad = " " * (source.indent(where) + 2)
74
+ source.insert(where[:file], source.block_end(where), ["#{pad}emits #{name}"])
75
+ end
76
+
77
+ # @api private
78
+ def add_policy(source, name:, on:, trigger:)
79
+ named!(name, "a policy")
80
+ refuse!("there is already a policy #{name}") if source.places.of("policy", name)
81
+ event = dotted!(on, "the event")
82
+ target = dotted!(trigger, "the command")
83
+ command_at(source, target[1], target[2])
84
+ header = source.chapter
85
+ source.insert(header[:file], source.block_end(header), policy_lines(name, event, target))
86
+ end
87
+
88
+ # @api private
89
+ def policy_lines(name, event, target)
90
+ ["", " policy \"#{name}\" do", " on #{event[1]}::#{event[2]}", " trigger #{target[1]}::#{target[2]}", " end"]
91
+ end
92
+
93
+ # @api private
94
+ def delete(source, kind:, name:, aggregate:)
95
+ where = case kind
96
+ when "aggregate" then aggregate_at(source, name)
97
+ when "command" then command_at(source, aggregate, name)
98
+ when "policy" then source.places.of("policy", name) || refuse!("there is no policy #{name}")
99
+ else refuse!("the board cannot delete a #{kind.inspect}")
100
+ end
101
+ source.remove(where[:file], where[:line] - 1, source.block_end(where))
102
+ end
103
+
104
+ # @api private
105
+ def delete_event(source, aggregate:, command:, name:)
106
+ where = command_at(source, aggregate, command)
107
+ line = emit_line(source, where, name) or refuse!("#{command} does not emit #{name}")
108
+ source.remove(where[:file], line, line)
109
+ end
110
+
111
+ # @api private
112
+ def aggregate_at(source, name)
113
+ source.places.of("aggregate", name) || refuse!("there is no aggregate #{name}")
114
+ end
115
+
116
+ # @api private
117
+ def command_at(source, aggregate, name)
118
+ source.places.of("command", name, owner: aggregate) || refuse!("#{aggregate} has no command #{name}")
119
+ end
120
+
121
+ # The 0-based index of the line in a command's block that emits the event, or nil.
122
+ # @api private
123
+ def emit_line(source, where, name)
124
+ from = where[:line]
125
+ block = source.slice(where[:file], from, source.block_end(where))
126
+ found = block.index { |text| text.match?(/\A\s*emits\s+("?)#{Regexp.escape(name)}\1\s*\z/) }
127
+ found && (from + found)
128
+ end
129
+
130
+ # @api private
131
+ def dotted!(text, what)
132
+ DOTTED.match(text) || refuse!("#{what} is written Aggregate.Name, not #{text.inspect}")
133
+ end
134
+
135
+ # @api private
136
+ def named!(name, what)
137
+ refuse!("#{what} needs a name that starts with a capital letter, with letters and digits only") unless NAME.match?(name)
138
+ end
139
+
140
+ # @api private
141
+ def refuse!(message) = raise(Refused, message)
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,89 @@
1
+ require "hecks"
2
+ require "hecks/bluebook/model_check"
3
+ require "hecks/runtime/capability_graph"
4
+ require_relative "board"
5
+
6
+ module Hecks
7
+ module Composer
8
+ # Judges a workspace as it stands on screen: parse it, boot it, then draw and read back what
9
+ # it declares. It changes nothing on disk and holds nothing between calls, so a caller may
10
+ # run it on every pause in typing.
11
+ module Check
12
+ module_function
13
+
14
+ # @param dir [String] the workspace directory
15
+ # @param buffers [Hash{String => String}] unsaved text by path relative to `dir`
16
+ # @param chapter_name [String, nil] the chapter to draw; the first bluebook's when nil
17
+ # @return [Hash] `diagnostic` (nil, or `{file:, line:, message:, expected:}` for the first
18
+ # refusal), `files` (the workspace's declaration files), `outline` (see {Outline}),
19
+ # `chapter` (the name drawn), `diagrams` (Mermaid text by file name), `statements` (plain
20
+ # sentences), `findings` (`{kind:, severity:, subject:, message:}` per model-check
21
+ # finding), `wiring` (see {wiring}, nil until the workspace boots), `board` (the
22
+ # event-storming notes, see {Board}, nil until it boots) and `elapsed_ms`
23
+ def call(dir:, buffers: {}, chapter_name: nil)
24
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
25
+ result = Workspace.stage(dir: dir, buffers: buffers) { |stage| judge(stage, chapter_name) }
26
+ result.merge(elapsed_ms: ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round)
27
+ end
28
+
29
+ # @api private
30
+ def judge(stage, chapter_name)
31
+ files = Workspace.files(stage)
32
+ verdict(stage, empty(files, Outline.call(stage, files)), chapter_name)
33
+ end
34
+
35
+ # What the parser and the boot say of the staged text, laid over `base`.
36
+ # @api private
37
+ def verdict(stage, base, chapter_name)
38
+ refusal = Parse.call(stage)
39
+ return base.merge(diagnostic: refusal) if refusal
40
+
41
+ project(stage, base, chapter_name || Parse.bluebooks(stage).keys.first)
42
+ rescue Parse::ParserMissing
43
+ raise
44
+ rescue StandardError => e
45
+ base.merge(diagnostic: { file: nil, line: nil, message: e.message.lines.first.to_s.chomp, expected: [] })
46
+ end
47
+
48
+ # @api private
49
+ def project(stage, base, name)
50
+ registry = Hecks.boot(stage, install_driving: false, environment: "memory").registry
51
+ chapter = registry.bluebook(name)
52
+ base.merge(chapter: name, wiring: wiring(registry, name), board: Board.call(chapter, outline: base[:outline]),
53
+ **read(chapter, registry.hecksagon(name)))
54
+ end
55
+
56
+ # The diagrams, statements and model-check findings of a booted chapter.
57
+ # @api private
58
+ def read(chapter, hecksagon)
59
+ { diagrams: Projections::Diagrams.call(bluebook: chapter, options: { hecksagon: hecksagon }),
60
+ statements: Projections::Statements.call(bluebook: chapter),
61
+ findings: Bluebook::ModelCheck.call(chapter, hecksagon: hecksagon).map(&:to_h) }
62
+ end
63
+
64
+ # What the chapter is wired to: the binds its hecksagon declares, the chapters it attaches,
65
+ # the driving adapters it admits, the world's default adapter, and each declared port with
66
+ # the adapters bound to it.
67
+ #
68
+ # @param registry [Runtime::Registry] a booted registry
69
+ # @param name [String] the chapter's name
70
+ # @return [Hash] `binds`, `attaches`, `driving`, `default_adapter` and `ports`
71
+ def wiring(registry, name)
72
+ hecksagon = registry.hecksagon(name)
73
+ {
74
+ binds: (hecksagon&.binds || []).map { |bind| bind.to_h.slice(:aggregate, :verb, :adapter, :role) },
75
+ attaches: hecksagon&.member_chapters || [],
76
+ driving: hecksagon&.driving || [],
77
+ default_adapter: registry.world(name)&.default_adapter,
78
+ ports: Runtime::CapabilityGraph.new(registry).fulfillments
79
+ }
80
+ end
81
+
82
+ # @api private
83
+ def empty(files, outline)
84
+ { diagnostic: nil, files: files, outline: outline, chapter: nil, diagrams: {}, statements: [],
85
+ findings: [], wiring: nil, board: nil }
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,81 @@
1
+ require "fileutils"
2
+ require_relative "workspace"
3
+
4
+ module Hecks
5
+ module Composer
6
+ # The domains the editor can open, on the hecksagon side: every directory under a checkout
7
+ # that holds a bluebook, in groups, and the copy of one that the editor works on.
8
+ #
9
+ # A domain is opened as a scratch copy under `tmp/composer/`, so saving, the agent and Undo
10
+ # never touch the real files. Only an id from {known} can be opened, so a request cannot name
11
+ # a path of its own.
12
+ module Domains
13
+ # Where scratch copies live, relative to the checkout (it is not committed).
14
+ SCRATCH = "tmp/composer".freeze
15
+
16
+ # The groups the picker shows, in order, each with the ids it takes.
17
+ GROUPS = [["Examples", %r{\Aexamples/[^/]+\z}],
18
+ ["Stress domains", %r{\Aqa/stress_domains/[^/]+\z}],
19
+ ["Hecks chapters", %r{\Alib/hecks/}],
20
+ ["Quality control", /\Aqa\z/],
21
+ ["Composer fixtures", %r{\Aspec/fixtures/composer/[^/]+\z}]].freeze
22
+
23
+ # Raised when an id is not one of the known domains.
24
+ class Unknown < ArgumentError; end
25
+
26
+ module_function
27
+
28
+ # @param root [String] the checkout to search
29
+ # @return [Array<Hash>] `{id:, group:}` for each known domain, in group then id order, where
30
+ # `id` is the domain's directory relative to `root`
31
+ def known(root:)
32
+ entries = owners(root).filter_map { |id| entry_for(id) }
33
+ entries.sort_by { |entry| [rank(entry[:group]), entry[:id]] }
34
+ end
35
+
36
+ # Copies a known domain's declaration files into a fresh scratch directory.
37
+ #
38
+ # @param root [String] the checkout
39
+ # @param id [String] one of the ids {known} lists
40
+ # @return [String] the scratch directory the editor should open
41
+ # @raise [Unknown] if `id` is not a known domain
42
+ def open(root:, id:)
43
+ raise Unknown, "#{id.inspect} is not a domain this checkout knows" unless known?(root, id)
44
+
45
+ scratch = File.join(root, SCRATCH, id.tr("/", "_"))
46
+ FileUtils.rm_rf(scratch)
47
+ FileUtils.mkdir_p(scratch)
48
+ Workspace.copy_declarations(source(root, id), scratch)
49
+ scratch
50
+ end
51
+
52
+ # @api private
53
+ def known?(root, id) = known(root: root).any? { |entry| entry[:id] == id }
54
+
55
+ # @api private
56
+ def entry_for(id)
57
+ group = GROUPS.find { |_, pattern| pattern.match?(id) }
58
+ { id: id, group: group.first } if group
59
+ end
60
+
61
+ # @api private
62
+ def rank(group) = GROUPS.index { |name, _| name == group }
63
+
64
+ # @api private
65
+ def owners(root)
66
+ Dir.glob("**/*.bluebook", base: root).map { |path| owner(File.dirname(path)) }.uniq
67
+ end
68
+
69
+ # A `bluebook/` folder stands for the domain that holds it.
70
+ # @api private
71
+ def owner(dir) = File.basename(dir) == "bluebook" ? File.dirname(dir) : dir
72
+
73
+ # Where a domain's bluebooks are: its `bluebook/` folder if it has one, else itself.
74
+ # @api private
75
+ def source(root, id)
76
+ folder = File.join(root, id, "bluebook")
77
+ File.directory?(folder) ? folder : File.join(root, id)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,134 @@
1
+ require "time"
2
+
3
+ module Hecks
4
+ module Composer
5
+ # Runs one long-running command at a time in the background, on the hecksagon side like the
6
+ # other composer files. Fuzzing and conformance runs take seconds to minutes, so the caller
7
+ # starts a job and the page polls {#state}.
8
+ #
9
+ # A job is a call to the injected runner on a worker thread. A watchdog thread ends it when
10
+ # it outlives the timeout. Cancelling or timing out kills the worker thread only: a child
11
+ # process the command spawned is not killed and keeps running until it exits on its own.
12
+ # All state sits behind one mutex, so {#state} is safe to call from any thread.
13
+ class Jobs
14
+ # Raised by {#start} while another job is still running.
15
+ class Busy < StandardError; end
16
+
17
+ # How much of a job's output {#state} keeps, counted from the end.
18
+ OUTPUT_LIMIT = 20_000
19
+
20
+ # @param runner [#call] takes `kind:, verb:, args:, confirm:` and returns
21
+ # `{ok:, status:, output:}`
22
+ # @param timeout [Numeric] seconds a job may run before it is marked "timed_out"
23
+ def initialize(runner:, timeout: 600)
24
+ @runner = runner
25
+ @timeout = timeout
26
+ @mutex = Mutex.new
27
+ @settled = ConditionVariable.new
28
+ @generation = 0
29
+ @state = blank
30
+ end
31
+
32
+ # @param kind [String] the kind of command the runner dispatches on
33
+ # @param verb [String] the command's verb
34
+ # @param args [Hash] the command's arguments
35
+ # @param confirm [String, nil] the confirmation the runner requires, if any
36
+ # @return [true]
37
+ # @raise [Busy] when a job is running
38
+ def start(kind:, verb:, args: {}, confirm: nil) # rubocop:disable Naming/PredicateMethod
39
+ @mutex.synchronize do
40
+ raise Busy, "a job is already running" if running?
41
+
42
+ @state = blank.merge(status: "running", verb: verb, started_at: stamp)
43
+ launch(@generation += 1, { kind: kind, verb: verb, args: args, confirm: confirm })
44
+ end
45
+ true
46
+ end
47
+
48
+ # @return [Hash] `status` ("idle", "running", "done", "failed", "cancelled" or
49
+ # "timed_out"), `verb`, `started_at` and `finished_at` (ISO 8601 or nil), `ok` (nil until
50
+ # the job settles) and `output` (the last {OUTPUT_LIMIT} characters)
51
+ def state
52
+ @mutex.synchronize { @state.dup }
53
+ end
54
+
55
+ # Stops the running job's worker thread. A child process it spawned is not killed.
56
+ # @return [Boolean] true when a job was running
57
+ def cancel
58
+ @mutex.synchronize do
59
+ return false unless running?
60
+
61
+ @worker.kill
62
+ settle("cancelled", "")
63
+ true
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ # @api private
70
+ def blank
71
+ { status: "idle", verb: nil, started_at: nil, finished_at: nil, ok: nil, output: "" }
72
+ end
73
+
74
+ # @api private
75
+ def running? = @state[:status] == "running"
76
+
77
+ # @api private
78
+ def current?(generation) = generation == @generation && running?
79
+
80
+ # @api private
81
+ def stamp = Time.now.utc.iso8601(3)
82
+
83
+ # @api private
84
+ def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
85
+
86
+ # Starts the worker and its watchdog; the caller holds the mutex.
87
+ # @api private
88
+ def launch(generation, call)
89
+ @worker = Thread.new { work(generation, call) }
90
+ Thread.new { watch(generation) }
91
+ end
92
+
93
+ # @api private
94
+ def work(generation, call)
95
+ result = @runner.call(**call)
96
+ conclude(generation, result[:ok] ? "done" : "failed", result[:output])
97
+ rescue StandardError => e
98
+ conclude(generation, "failed", e.message)
99
+ end
100
+
101
+ # @api private
102
+ def conclude(generation, status, output)
103
+ @mutex.synchronize { settle(status, output) if current?(generation) }
104
+ end
105
+
106
+ # Ends the job when it outlives the timeout; wakes early when the job settles first.
107
+ # @api private
108
+ def watch(generation)
109
+ deadline = now + @timeout
110
+ @mutex.synchronize do
111
+ @settled.wait(@mutex, deadline - now) while current?(generation) && now < deadline
112
+ expire(generation)
113
+ end
114
+ end
115
+
116
+ # @api private
117
+ def expire(generation)
118
+ return unless current?(generation)
119
+
120
+ @worker.kill
121
+ settle("timed_out", "timed out after #{@timeout} seconds")
122
+ end
123
+
124
+ # Records how the job ended; the caller holds the mutex.
125
+ # @api private
126
+ def settle(status, output)
127
+ text = output.to_s
128
+ text = text[-OUTPUT_LIMIT..] if text.length > OUTPUT_LIMIT
129
+ @state = @state.merge(status: status, finished_at: stamp, ok: status == "done", output: text)
130
+ @settled.broadcast
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,37 @@
1
+ module Hecks
2
+ module Composer
3
+ # Proposes which panels the editor shows beside the text, on the hecksagon side like the
4
+ # other composer files. It is a small rule set, not a model call: the same inputs always give
5
+ # the same answer, and it reads only the author's intent and the check's verdict.
6
+ module Layout
7
+ module_function
8
+
9
+ # @param intent [String] "edit", "wire", "question" or "new_domain"
10
+ # @param check [Hash] the result of {Check.call}
11
+ # @param changed [Hash] the files the last request changed, by path
12
+ # @return [Hash] `right` (one of the panel names) and `bottom` (always "agent")
13
+ def propose(intent:, check:, changed:)
14
+ { right: right(intent, check, changed), bottom: "agent" }
15
+ end
16
+
17
+ # A refusal or an error finding comes first; then the author's intent decides.
18
+ # @api private
19
+ def right(intent, check, _changed)
20
+ return "findings" if refused?(check)
21
+
22
+ case intent
23
+ when "wire" then "wiring"
24
+ when "question" then "statements"
25
+ else "diagram"
26
+ end
27
+ end
28
+
29
+ # @api private
30
+ def refused?(check)
31
+ return true if check[:diagnostic]
32
+
33
+ Array(check[:findings]).any? { |finding| finding[:severity].to_s == "error" }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,150 @@
1
+ require "fileutils"
2
+ require "hecks"
3
+ require_relative "parse"
4
+ require_relative "plain"
5
+ require_relative "workspace"
6
+
7
+ module Hecks
8
+ module Composer
9
+ # A workspace domain booted once and kept, so the Console and Records panels can dispatch
10
+ # commands against it and read back what it holds. It sits on the driving side of the
11
+ # hecksagon: the editor drives the domain, the domain keeps its records in memory, and the
12
+ # staged copy of the files lives until `close`.
13
+ class Live
14
+ # @return [String] the chapter the workspace booted
15
+ attr_reader :chapter_name
16
+
17
+ # Stages the workspace and boots it in memory.
18
+ #
19
+ # @param dir [String] the workspace directory
20
+ # @param buffers [Hash{String => String}] unsaved text by path relative to `dir`
21
+ # @return [Live] a booted workspace
22
+ # @raise [StandardError, ScriptError] whatever the parse or the boot refused with (a
23
+ # bluebook that is not valid Ruby raises a SyntaxError); the stage is removed
24
+ def self.open(dir:, buffers: {})
25
+ stage = Workspace.materialize(dir: dir, buffers: buffers)
26
+ begin
27
+ runtime = Hecks.boot(stage, install_driving: false, environment: "memory")
28
+ new(stage: stage, runtime: runtime, chapter_name: Parse.bluebooks(stage).keys.first)
29
+ rescue StandardError, ScriptError
30
+ FileUtils.rm_rf(stage)
31
+ raise
32
+ end
33
+ end
34
+
35
+ # @param stage [String] the staged directory this domain booted from
36
+ # @param runtime [Runtime::Dispatcher] the booted domain
37
+ # @param chapter_name [String] the chapter's name
38
+ def initialize(stage:, runtime:, chapter_name:)
39
+ @stage = stage
40
+ @runtime = runtime
41
+ @chapter_name = chapter_name
42
+ end
43
+
44
+ # The aggregate commands the chapter declares, each with a starter value for every argument.
45
+ #
46
+ # @return [Array<Hash>] per command: `verb`, `aggregate`, `command`, `goal`, `creates`,
47
+ # `args` (`{name:, type:, optional:}` each) and `template` (argument name to starter value)
48
+ def commands
49
+ aggregates.flat_map do |aggregate|
50
+ aggregate.commands.map { |command| describe(aggregate, command) }
51
+ end
52
+ end
53
+
54
+ # Runs one command against the booted domain.
55
+ #
56
+ # @param verb [String] the dispatcher's form, such as "Ledger::Account.Credit"
57
+ # @param to [String, nil] the identity of the record the command addresses
58
+ # @param with [Hash] the command's facts by argument name; keys may be strings
59
+ # @return [Hash] `{ok: true, events:, instance:}` with the event names and the plain row, or
60
+ # `{ok: false, kind:, error:}` with the refusal's class name and first message line
61
+ def dispatch(verb:, to: nil, with: {})
62
+ accepted(@runtime.dispatch(verb, to: to, with: symbolize(with)))
63
+ rescue StandardError => e
64
+ refused(e)
65
+ end
66
+
67
+ # @return [Hash{String => Array<Hash>}] each aggregate's name to its records as plain rows
68
+ def records
69
+ aggregates.to_h do |aggregate|
70
+ rows = @runtime.registry.repository(@chapter_name, aggregate).all
71
+ [aggregate.hecks_name, Plain.call(rows.to_a)]
72
+ end
73
+ end
74
+
75
+ # @param limit [Integer] how many of the latest events to return
76
+ # @return [Array<Hash>] the last `limit` events, oldest first, as plain Hashes with `name`,
77
+ # `aggregate`, `id`, `payload` and `occurred_at`
78
+ def history(limit = 200)
79
+ @runtime.events.to_a.last(limit).map { |event| Plain.call(event.to_h) }
80
+ end
81
+
82
+ # Removes the staged copy of the workspace. Calling it again does nothing.
83
+ #
84
+ # @return [void]
85
+ def close
86
+ FileUtils.rm_rf(@stage)
87
+ end
88
+
89
+ # @api private
90
+ def aggregates = @runtime.registry.bluebook(@chapter_name).aggregates
91
+
92
+ # @api private
93
+ def accepted(result)
94
+ { ok: true, events: result.events.map { |event| event.to_h[:name] },
95
+ instance: Plain.call(result.instance) }
96
+ end
97
+
98
+ # @api private
99
+ def refused(error)
100
+ { ok: false, kind: error.class.name.to_s.split("::").last,
101
+ error: error.message.lines.first.to_s.chomp }
102
+ end
103
+
104
+ # @api private
105
+ def describe(aggregate, command)
106
+ args = arguments(command)
107
+ { verb: "#{@chapter_name}::#{aggregate.hecks_name}.#{command.hecks_name}",
108
+ aggregate: aggregate.hecks_name, command: command.hecks_name, goal: command.goal.to_s,
109
+ creates: command.references.nil?, args: args, template: template(aggregate, args) }
110
+ end
111
+
112
+ # @api private
113
+ def template(aggregate, args)
114
+ args.to_h { |arg| [arg[:name], starter(aggregate, arg[:type])] }
115
+ end
116
+
117
+ # @api private
118
+ def arguments(command)
119
+ command.attributes.map do |attr|
120
+ { name: attr.name.to_s, type: attr.type.to_s, optional: attr.optional? }
121
+ end
122
+ end
123
+
124
+ # A value object of the aggregate gets a Hash of its attributes' defaults; any other type, "".
125
+ # @api private
126
+ def starter(aggregate, type)
127
+ shape = aggregate.value_object(type)
128
+ return "" unless shape
129
+
130
+ shape.attributes.to_h { |attr| [attr.name.to_s, default_for(attr)] }
131
+ end
132
+
133
+ # @api private
134
+ def default_for(attr)
135
+ return attr.default unless attr.default.nil?
136
+
137
+ attr.type.to_s == "Integer" ? 0 : ""
138
+ end
139
+
140
+ # @api private
141
+ def symbolize(item)
142
+ case item
143
+ when Hash then item.to_h { |key, inner| [key.to_sym, symbolize(inner)] }
144
+ when Array then item.map { |inner| symbolize(inner) }
145
+ else item
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,48 @@
1
+ module Hecks
2
+ module Composer
3
+ # Where each named thing in a workspace is declared, read from the text alone so it works on a
4
+ # draft that does not parse: the editor jumps to a definition and shows a line's `goal` or
5
+ # `description` on hover from it.
6
+ module Outline
7
+ # The constructs that name something a reader may want to jump to.
8
+ KINDS = %w[aggregate entity value_object command query policy process_manager].freeze
9
+
10
+ DECLARATION = /\A\s*(#{KINDS.join("|")})\s+"([^"]+)"/
11
+ SUMMARY = /\A\s*(?:goal|description)\s+"((?:[^"\\]|\\.)*)"/
12
+
13
+ # How many lines below a declaration its `goal` or `description` may sit.
14
+ REACH = 6
15
+
16
+ module_function
17
+
18
+ # @param stage [String] a staged directory (see {Workspace.stage})
19
+ # @param files [Array<String>] the staged files, relative to `stage`
20
+ # @return [Array<Hash>] `{file:, line:, kind:, name:, summary:}` for each declaration, in file
21
+ # then line order; `line` counts from 1 and `summary` is nil when none follows
22
+ def call(stage, files)
23
+ files.grep(/\.(bluebook|hecksagon|behaviors|world)\z/).flat_map do |file|
24
+ entries(file, File.readlines(File.join(stage, file), chomp: true))
25
+ end
26
+ end
27
+
28
+ # @api private
29
+ def entries(file, lines)
30
+ lines.each_with_index.filter_map do |text, index|
31
+ found = text.match(DECLARATION) or next
32
+ { file: file, line: index + 1, kind: found[1], name: found[2], summary: summary(lines, index) }
33
+ end
34
+ end
35
+
36
+ # @api private
37
+ def summary(lines, index)
38
+ lines[(index + 1)..(index + REACH)].to_a.each do |text|
39
+ break if text.match?(DECLARATION)
40
+
41
+ found = text.match(SUMMARY)
42
+ return found[1].gsub('\"', '"') if found
43
+ end
44
+ nil
45
+ end
46
+ end
47
+ end
48
+ end