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,110 @@
1
+ require_relative "agent"
2
+ require_relative "check"
3
+ require_relative "layout"
4
+ require_relative "session"
5
+
6
+ module Hecks
7
+ module Composer
8
+ # Carries one request from the author to the agent and back, on the hecksagon side: records it
9
+ # in the draft's conversation, has the prompt classified, asks the agent to change the
10
+ # workspace's files, judges what it left, asks once more when the check refused the result, and
11
+ # records the agent's reply and the verdict.
12
+ #
13
+ # The agent edits the workspace on disk, so the files it changes are read before and after to
14
+ # show the author what moved.
15
+ class Assistant
16
+ # How many times the agent may be asked about one request: the request, then a repair.
17
+ ROUNDS = 2
18
+
19
+ QUESTION_NOTE = "That reads as a question, so nothing was edited.".freeze
20
+
21
+ # @param workspace [String] the domain directory the agent edits
22
+ # @param agents [Runtime::Registry] a registry holding the `composer_agent` port and one
23
+ # adapter
24
+ # @param session [Session] the editor's in-memory draft bookkeeping
25
+ def initialize(workspace:, agents:, session:)
26
+ @workspace = workspace
27
+ @agents = agents
28
+ @session = session
29
+ end
30
+
31
+ # @param prompt [String] what the author asked for
32
+ # @return [Hash] `intent`, `note` (the agent's one-line account), `rounds` (how many times it
33
+ # was asked), `changed` (`{path => {before:, after:}}` for each file that differs), `check`
34
+ # (the whole check on what is now on disk) and `layout` (the panels to open, see {Layout})
35
+ def ask(prompt)
36
+ slug = File.basename(@workspace)
37
+ before = snapshot
38
+ open_draft(slug, before)
39
+ @session.ask(slug: slug, said: prompt)
40
+ intent = classify(prompt)
41
+ note, rounds = edit(intent, prompt)
42
+ outcome(slug, intent, note, rounds, before)
43
+ end
44
+
45
+ private
46
+
47
+ def open_draft(slug, files)
48
+ chapter = Parse.bluebooks(@workspace).keys.first || "Workspace"
49
+ @session.open(slug: slug, chapter_name: chapter, text: files.values.first.to_s)
50
+ end
51
+
52
+ # The prompt's intent; a classifier that cannot answer leaves it an edit.
53
+ def classify(prompt)
54
+ Agent.classify(@agents, prompt: prompt)
55
+ rescue Agent::Unavailable
56
+ "edit"
57
+ end
58
+
59
+ def edit(intent, prompt)
60
+ return [QUESTION_NOTE, 0] if intent == "question"
61
+
62
+ note = ask_agent(intent, prompt)
63
+ refusal = Check.call(dir: @workspace)[:diagnostic]
64
+ return [note, 1] unless refusal
65
+
66
+ [ask_agent(intent, repair(prompt, refusal)), ROUNDS]
67
+ end
68
+
69
+ def ask_agent(intent, prompt)
70
+ return Agent.wire(@agents, prompt: prompt, dir: @workspace) if intent == "wire"
71
+
72
+ Agent.draft(@agents, prompt: prompt, dir: @workspace)
73
+ end
74
+
75
+ def repair(prompt, refusal)
76
+ where = refusal[:line] ? "#{refusal[:file]} line #{refusal[:line]}: " : ""
77
+ expected = refusal[:expected].to_a.empty? ? "" : " (expected one of: #{refusal[:expected].join(", ")})"
78
+ "#{prompt}\n\nYour last edit was refused by the check, so fix it: #{where}#{refusal[:message]}#{expected}"
79
+ end
80
+
81
+ def outcome(slug, intent, note, rounds, before)
82
+ after = snapshot
83
+ check = Check.call(dir: @workspace)
84
+ @session.answer(slug: slug, said: note, text: after.values.first.to_s)
85
+ @session.judge(slug: slug, diagnostic: check[:diagnostic])
86
+ moved = changed(before, after)
87
+ record(moved)
88
+ { intent: intent, note: note, rounds: rounds, changed: moved, check: check,
89
+ layout: Layout.propose(intent: intent, check: check, changed: moved) }
90
+ end
91
+
92
+ # Keeps each file the agent changed in the timeline, as a save would.
93
+ def record(moved)
94
+ moved.each { |path, text| @session.record(path: path, text: text[:after]) if text[:after] }
95
+ end
96
+
97
+ def changed(before, after)
98
+ (before.keys | after.keys).sort.each_with_object({}) do |path, moved|
99
+ next if before[path] == after[path]
100
+
101
+ moved[path] = { before: before[path], after: after[path] }
102
+ end
103
+ end
104
+
105
+ def snapshot
106
+ Workspace.files(@workspace).to_h { |path| [path, File.read(File.join(@workspace, path))] }
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,53 @@
1
+ require "hecks"
2
+ require "hecks/behaviors"
3
+ require_relative "workspace"
4
+
5
+ module Hecks
6
+ module Composer
7
+ # Runs a workspace's `.behaviors` files as they stand on screen and reports each test as plain
8
+ # data. Like the other composer files it sits on the hecksagon side: it stages the text, calls
9
+ # the behaviors runner and changes nothing on disk.
10
+ module BehaviorRun
11
+ module_function
12
+
13
+ # @param dir [String] the workspace directory
14
+ # @param buffers [Hash{String => String}] unsaved text by path relative to `dir`
15
+ # @return [Hash] `files` (`{path:, parse_error:, runs: [{status:, description:, message:}]}`
16
+ # per behaviors file, paths relative to the workspace, status "pass", "fail" or "error")
17
+ # and `summary` (`total`, `passed`, `failed`, `errored` and `parse_errors` counts)
18
+ # @raise [Workspace::OutsideWorkspace] if a buffer's path leaves the workspace
19
+ def call(dir:, buffers: {})
20
+ Workspace.stage(dir: dir, buffers: buffers) do |stage|
21
+ files = Hecks::Behaviors.run_all(stage).files.map { |file| report(stage, file) }
22
+ { files: files, summary: tally(files) }
23
+ end
24
+ end
25
+
26
+ # @api private
27
+ def report(stage, file)
28
+ {
29
+ path: file.path.to_s.delete_prefix("#{File.realpath(stage)}/").delete_prefix("#{stage}/"),
30
+ parse_error: file.parse_error,
31
+ runs: file.runs.map { |run| run_hash(run) }
32
+ }
33
+ end
34
+
35
+ # @api private
36
+ def run_hash(run)
37
+ { status: run.status.to_s, description: run.description, message: run.message }
38
+ end
39
+
40
+ # @api private
41
+ def tally(files)
42
+ runs = files.flat_map { |file| file[:runs] }
43
+ {
44
+ total: runs.size,
45
+ passed: runs.count { |run| run[:status] == "pass" },
46
+ failed: runs.count { |run| run[:status] == "fail" },
47
+ errored: runs.count { |run| run[:status] == "error" },
48
+ parse_errors: files.count { |file| file[:parse_error] }
49
+ }
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,178 @@
1
+ # The Composer chapter: a bluebook being written in an editor, by hand or by asking an agent.
2
+ #
3
+ # A Draft is the editor's working copy of one chapter: the text on screen, the conversation that
4
+ # shaped it, and whether the last check of it passed. The files on disk stay the truth; a Draft
5
+ # only holds what is being changed and what was said about it. The check itself (parse, load,
6
+ # project, model-check) and the agent that rewrites the text are the outside, reached through the
7
+ # Composer's hecksagon, so nothing here knows how either is done.
8
+ Hecks.bluebook "Composer" do
9
+ vision "Write a bluebook in an editor and watch the domain draw itself, with an agent that can change the draft when asked."
10
+
11
+ aggregate "Draft" do
12
+ description "One chapter being edited: its text, the conversation about it, and whether its last check passed."
13
+
14
+ attribute :slug, DraftSlug
15
+ attribute :chapter_name, ChapterName
16
+ attribute :text, SourceText
17
+ attribute :note, Note, optional: true
18
+ attribute :turns, list_of(Turn)
19
+
20
+ identified_by :slug
21
+
22
+ value_object "DraftSlug" do
23
+ attribute :value, String, pattern: '[^ \t\n\r]'
24
+
25
+ invariant("a draft is named") { !value.to_s.empty? }
26
+ end
27
+
28
+ # The name a chapter's `Hecks.bluebook "Name"` header declares, which is also the name the
29
+ # check loads it under.
30
+ value_object "ChapterName" do
31
+ attribute :value, String, pattern: '[A-Z][A-Za-z0-9]*'
32
+
33
+ invariant("a chapter is named") { !value.to_s.empty? }
34
+ end
35
+
36
+ # What is on screen. A draft may be empty or half-written; the check judges it, not this.
37
+ value_object "SourceText" do
38
+ attribute :value, String
39
+ end
40
+
41
+ # The check's last word on the draft, or the agent's one-line account of what it changed.
42
+ value_object "Note" do
43
+ attribute :value, String
44
+ end
45
+
46
+ value_object "TurnSequence" do
47
+ attribute :value, Integer
48
+
49
+ invariant("a turn sequence is positive") { value.positive? }
50
+ end
51
+
52
+ value_object "Speaker" do
53
+ attribute :value, String, one_of: ["author", "agent"]
54
+ end
55
+
56
+ value_object "Utterance" do
57
+ attribute :value, String
58
+
59
+ invariant("a turn says something") { !value.to_s.empty? }
60
+ end
61
+
62
+ # The sequence is supplied by the caller, who already knows how many turns are on screen, so
63
+ # the order of a conversation is not a fact this chapter invents on write.
64
+ entity "Turn" do
65
+ description "One thing said in the conversation about the draft, by the author or by the agent."
66
+
67
+ attribute :sequence, TurnSequence
68
+
69
+ identified_by :sequence
70
+
71
+ attribute :speaker, Speaker
72
+ attribute :said, Utterance
73
+ end
74
+
75
+ lifecycle :status, default: "drafting" do
76
+ transition "Accept" => "valid", from: %w[drafting valid invalid]
77
+ transition "Reject" => "invalid", from: %w[drafting valid invalid]
78
+ transition "Revise" => "drafting", from: %w[drafting valid invalid]
79
+ end
80
+
81
+ command "Begin" do
82
+ role "Author"
83
+ goal "Open a chapter for editing, with the text it starts from"
84
+
85
+ attribute :slug, DraftSlug
86
+ attribute :chapter_name, ChapterName
87
+ attribute :text, SourceText
88
+
89
+ sets :slug
90
+ sets :chapter_name
91
+ sets :text
92
+
93
+ emits "DraftBegun"
94
+ end
95
+
96
+ command "Revise" do
97
+ role "Author"
98
+ goal "Replace the draft's text with what is now on screen"
99
+
100
+ reference_to Draft
101
+ attribute :text, SourceText
102
+
103
+ sets :text
104
+
105
+ emits "DraftRevised"
106
+ end
107
+
108
+ command "Accept" do
109
+ role "System"
110
+ goal "Record that the draft's text passed its check"
111
+
112
+ reference_to Draft
113
+ attribute :note, Note
114
+
115
+ sets :note
116
+
117
+ emits "DraftAccepted"
118
+ end
119
+
120
+ command "Reject" do
121
+ role "System"
122
+ goal "Record that the draft's text failed its check, and why"
123
+
124
+ reference_to Draft
125
+ attribute :note, Note
126
+
127
+ sets :note
128
+
129
+ emits "DraftRejected"
130
+ end
131
+
132
+ command "Ask" do
133
+ role "Author"
134
+ goal "Put a request for the agent to the draft's conversation"
135
+
136
+ reference_to Draft
137
+ attribute :sequence, TurnSequence
138
+ attribute :said, Utterance
139
+
140
+ sets :turns, append: { sequence: :sequence, speaker: { value: "author" }, said: :said }
141
+
142
+ emits "TurnAsked"
143
+ end
144
+
145
+ command "Answer" do
146
+ role "System"
147
+ goal "Record the agent's reply and the text it left the draft with"
148
+
149
+ reference_to Draft
150
+ attribute :sequence, TurnSequence
151
+ attribute :said, Utterance
152
+ attribute :text, SourceText
153
+
154
+ sets :text
155
+ sets :turns, append: { sequence: :sequence, speaker: { value: "agent" }, said: :said }
156
+
157
+ emits "TurnAnswered"
158
+ end
159
+
160
+ query "Drafting" do
161
+ description "Every draft still being edited."
162
+ where(status: "drafting")
163
+ order_by :slug
164
+ end
165
+
166
+ query "Valid" do
167
+ description "Every draft whose last check passed."
168
+ where(status: "valid")
169
+ order_by :slug
170
+ end
171
+
172
+ query "Invalid" do
173
+ description "Every draft whose last check failed."
174
+ where(status: "invalid")
175
+ order_by :slug
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,16 @@
1
+ # The Composer domain, bound so it boots standing alone (a fuzz or model_check boot of this
2
+ # directory). Drafts are working state; the files on disk are the truth, so nothing here outlives
3
+ # the process.
4
+ Hecks.hecksagon "Composer" do
5
+ attaches "Governance"
6
+ persisted_by "Memory"
7
+ # The editor reaches this domain through Web, and every tool that boots a corpus domain (fuzz,
8
+ # model_check, the Composer's own Session) through Ruby; the launcher, the forms and MCP do not.
9
+ driven_by "Web"
10
+ driven_by "Ruby"
11
+ end
12
+
13
+ # The anti-corruption layer for Governance, which the chapter's roles are checked against.
14
+ Hecks.hecksagon "Governance" do
15
+ persisted_by "Memory"
16
+ end
@@ -0,0 +1,65 @@
1
+ module Hecks
2
+ module Composer
3
+ # The event-storming board of a chapter, on the hecksagon side: its aggregates with their
4
+ # commands, the events each command emits, and the policies that make one event trigger another
5
+ # command. It is read off the booted chapter, so it always shows what the text declares; the
6
+ # board's own edits go through {BoardEdit}, which rewrites the text.
7
+ module Board
8
+ module_function
9
+
10
+ # @param chapter [Bluebook::Chapter] the booted chapter
11
+ # @param outline [Array<Hash>] the text outline (see {Outline}), which says where each note
12
+ # is declared
13
+ # @return [Hash] `aggregates` (each with `name`, `at` and its `commands`, each command with
14
+ # `name`, `role`, `goal`, `emits` and `at`) and `policies` (`name`, `on`, `trigger`, `at`),
15
+ # where `at` is `{file:, line:}` or nil, and `on` and `trigger` read `Aggregate.Name`
16
+ def call(chapter, outline:)
17
+ places = Places.new(outline)
18
+ { aggregates: chapter.aggregates.map { |aggregate| aggregate_note(aggregate, places) },
19
+ policies: chapter.policies.map { |policy| policy_note(policy, places) } }
20
+ end
21
+
22
+ # @api private
23
+ def aggregate_note(aggregate, places)
24
+ name = aggregate.hecks_name
25
+ { name: name, at: places.of("aggregate", name),
26
+ commands: aggregate.commands.map { |command| command_note(name, command, places) } }
27
+ end
28
+
29
+ # @api private
30
+ def command_note(owner, command, places)
31
+ name = command.hecks_name
32
+ { name: name, role: command.role, goal: command.goal, emits: Array(command.emits).map(&:to_s),
33
+ at: places.of("command", name, owner: owner) }
34
+ end
35
+
36
+ # @api private
37
+ def policy_note(policy, places)
38
+ { name: policy.name, on: policy.on_event, trigger: policy.trigger_command,
39
+ at: places.of("policy", policy.name) }
40
+ end
41
+
42
+ # Where each declaration sits in the text, found by kind and name, and for a command by the
43
+ # aggregate that holds it.
44
+ class Places
45
+ # @param outline [Array<Hash>] see {Outline}
46
+ def initialize(outline)
47
+ owner = nil
48
+ @entries = outline.map do |entry|
49
+ owner = entry[:name] if entry[:kind] == "aggregate"
50
+ entry.merge(owner: entry[:kind] == "command" ? owner : nil)
51
+ end
52
+ end
53
+
54
+ # @param kind [String] `aggregate`, `command` or `policy`
55
+ # @param name [String] the declared name
56
+ # @param owner [String, nil] the aggregate, for a command
57
+ # @return [Hash, nil] `{file:, line:}` of the first match, or nil when the text has none
58
+ def of(kind, name, owner: nil)
59
+ found = @entries.find { |e| e[:kind] == kind && e[:name] == name && e[:owner] == owner }
60
+ found && { file: found[:file], line: found[:line] }
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,39 @@
1
+ module Hecks
2
+ module Composer
3
+ module BoardEdit
4
+ # The text of a command the board adds. The first command of an aggregate creates it, setting
5
+ # the key the aggregate is known by; every later one acts on a record of it. A command with an
6
+ # event says so on a line of its own.
7
+ module CommandText
8
+ module_function
9
+
10
+ # @param block [Array<String>] the lines of the aggregate's block, after its declaration
11
+ # @param indent [Integer] how far the aggregate's declaration is indented
12
+ # @param aggregate [String] the aggregate's name
13
+ # @param name [String] the new command's name
14
+ # @param event [String] the event it emits, or an empty string for none
15
+ # @return [Array<String>] the command's lines, indented one step inside the aggregate
16
+ # @raise [Refused] if the aggregate's key cannot be read, for a command that must set it
17
+ def lines(block:, indent:, aggregate:, name:, event:)
18
+ pad = " " * (indent + 2)
19
+ body = acts_on_a_record?(block, indent) ? ["reference_to #{aggregate}"] : creates(block, aggregate)
20
+ inner = [%(goal "Describe #{name}."), *body, (event.empty? ? nil : "emits #{event}")].compact
21
+ ["", %(#{pad}command "#{name}" do), *inner.map { |line| "#{pad} #{line}" }, "#{pad}end"]
22
+ end
23
+
24
+ # @api private
25
+ def acts_on_a_record?(block, indent) = block.any? { |line| line.match?(/\A {#{indent + 2}}command\s+"/) }
26
+
27
+ # The attribute and `sets` that give a new record its key.
28
+ # @api private
29
+ def creates(block, aggregate)
30
+ field = block.filter_map { |line| line[/\A\s*identified_by\s+:(\w+)\s*\z/, 1] }.first
31
+ type = field && block.filter_map { |line| line[/\A\s*attribute\s+:#{field},\s*([A-Za-z:]+)/, 1] }.first
32
+ raise Refused, "#{aggregate} has no key the board can read; write its first command in the text" unless type
33
+
34
+ ["attribute :#{field}, #{type}", "sets :#{field}"]
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,84 @@
1
+ require_relative "../board"
2
+ require_relative "../outline"
3
+ require_relative "../workspace"
4
+
5
+ module Hecks
6
+ module Composer
7
+ module BoardEdit
8
+ # The bluebook text of a staged workspace as lines, with what a board edit needs to change
9
+ # it: where a declaration is, where its block ends (found by indentation, as the bluebooks
10
+ # are laid out), and a way to put a patched file back as one result.
11
+ class Source
12
+ # @return [Board::Places] where each aggregate, command and policy is declared
13
+ attr_reader :places
14
+
15
+ # @param stage [String] a staged directory (see {Workspace.stage})
16
+ def initialize(stage)
17
+ files = Workspace.files(stage)
18
+ @places = Board::Places.new(Outline.call(stage, files))
19
+ @lines = files.grep(/\.bluebook\z/).to_h { |file| [file, File.read(File.join(stage, file)).lines.map(&:chomp)] }
20
+ end
21
+
22
+ # @param at [Hash] `{file:, line:}` of a declaration
23
+ # @return [Integer] the 0-based index of the `end` that closes it
24
+ # @raise [Refused] if the block has no closing `end` at its own indentation
25
+ def block_end(at)
26
+ lines = @lines.fetch(at[:file])
27
+ start = at[:line] - 1
28
+ pattern = /\A {#{indent_of(lines[start])}}end\b/
29
+ ((start + 1)...lines.size).find { |index| lines[index].match?(pattern) } or
30
+ raise Refused, "cannot find where the block at #{at[:file]}:#{at[:line]} ends"
31
+ end
32
+
33
+ # @param at [Hash] `{file:, line:}` of a declaration
34
+ # @return [Integer] how many spaces its line is indented
35
+ def indent(at) = indent_of(@lines.fetch(at[:file])[at[:line] - 1])
36
+
37
+ # The file and `end` of the chapter's own block, where new aggregates and policies go.
38
+ # @return [Hash] `{file:, line:}` of the chapter header
39
+ # @raise [Refused] if no file declares the chapter
40
+ def chapter
41
+ file = @lines.keys.sort.find { |path| @lines[path].any? { |line| line.match?(/\A\s*Hecks\.bluebook\s+"/) } }
42
+ raise Refused, "no file declares the chapter" unless file
43
+
44
+ { file: file, line: @lines[file].index { |line| line.match?(/\A\s*Hecks\.bluebook\s+"/) } + 1 }
45
+ end
46
+
47
+ # @param file [String] a bluebook path
48
+ # @param from [Integer] first 0-based line to read
49
+ # @param upto [Integer] last 0-based line to read
50
+ # @return [Array<String>] those lines
51
+ def slice(file, from, upto) = @lines.fetch(file)[from..upto]
52
+
53
+ # Puts new lines before a line of a file, with a blank line above them unless one is there.
54
+ # @return [Hash] the patched file as `{path:, text:, line:}`
55
+ def insert(file, before, new_lines)
56
+ lines = @lines.fetch(file)
57
+ added = spaced(lines, before, new_lines)
58
+ lines.insert(before, *added)
59
+ patched(file, before + 1 + added.index { |line| !line.strip.empty? }.to_i)
60
+ end
61
+
62
+ # Takes lines out of a file, and the blank line above them when there is one.
63
+ # @return [Hash] the patched file as `{path:, text:, line:}`
64
+ def remove(file, from, upto)
65
+ lines = @lines.fetch(file)
66
+ from -= 1 if from.positive? && lines[from - 1].strip.empty?
67
+ lines.slice!(from..upto)
68
+ patched(file, nil)
69
+ end
70
+
71
+ private
72
+
73
+ def indent_of(line) = line[/\A */].size
74
+
75
+ # The new lines, without their leading blank line when one is already above the spot.
76
+ def spaced(lines, before, added)
77
+ added.first.to_s.empty? && lines[before - 1].to_s.strip.empty? ? added.drop(1) : added
78
+ end
79
+
80
+ def patched(file, line) = { path: file, text: "#{@lines.fetch(file).join("\n")}\n", line: line }
81
+ end
82
+ end
83
+ end
84
+ end