hecks 3.13.0 → 4.0.0

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