hecks 3.10.0 → 3.12.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 (542) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/bluebook/pizzas.bluebook +29 -0
  3. data/examples/pizzas/bluebook/pizzas.hecksagon +7 -0
  4. data/exe/hecks +4 -4
  5. data/lib/hecks/adapters/driven/d1.rb +2 -10
  6. data/lib/hecks/adapters/driven/heki.adapter +1 -1
  7. data/lib/hecks/adapters/driven/heki.rb +2 -12
  8. data/lib/hecks/adapters/driven/lambda.adapter +1 -1
  9. data/lib/hecks/adapters/driven/lambda.rb +3 -18
  10. data/lib/hecks/adapters/driven/postgres.rb +4 -11
  11. data/lib/hecks/adapters/driven/sqlite.adapter +1 -1
  12. data/lib/hecks/adapters/driven/sqlite.rb +2 -10
  13. data/lib/hecks/adapters/driving/admission.rb +46 -0
  14. data/lib/hecks/adapters/driving/cli/nesting.rb +36 -0
  15. data/lib/hecks/adapters/driving/cli/shorthand.rb +88 -0
  16. data/lib/hecks/adapters/driving/cli.rb +127 -0
  17. data/lib/hecks/adapters/driving/cli_runner/answers.rb +160 -0
  18. data/lib/hecks/adapters/driving/cli_runner/resolution.rb +158 -0
  19. data/lib/hecks/adapters/driving/cli_runner/settling.rb +120 -0
  20. data/lib/hecks/adapters/driving/cli_runner/streaming.rb +93 -0
  21. data/lib/hecks/adapters/driving/cli_runner/suggestions.rb +47 -0
  22. data/lib/hecks/adapters/driving/cli_runner.rb +70 -0
  23. data/lib/hecks/adapters/driving/command_request.rb +121 -0
  24. data/lib/hecks/adapters/driving/handle.rb +214 -0
  25. data/lib/hecks/adapters/driving/json.rb +136 -0
  26. data/lib/hecks/adapters/driving/launcher_options.rb +267 -0
  27. data/lib/hecks/adapters/driving/mcp/properties.rb +49 -0
  28. data/lib/hecks/adapters/driving/mcp/tool_calls.rb +116 -0
  29. data/lib/hecks/adapters/driving/mcp/tool_definitions/auditing.rb +53 -0
  30. data/lib/hecks/adapters/driving/mcp/tool_definitions/calling.rb +71 -0
  31. data/lib/hecks/adapters/driving/mcp/tool_definitions/reading.rb +99 -0
  32. data/lib/hecks/adapters/driving/mcp/tool_definitions.rb +18 -0
  33. data/lib/hecks/adapters/driving/mcp.rb +159 -0
  34. data/lib/hecks/adapters/driving/mcp_guide_cache.rb +78 -0
  35. data/lib/hecks/adapters/driving/mcp_scope/argument_policy.rb +97 -0
  36. data/lib/hecks/adapters/driving/mcp_scope/env_problems.rb +69 -0
  37. data/lib/hecks/adapters/driving/mcp_scope/presenter.rb +83 -0
  38. data/lib/hecks/adapters/driving/mcp_scope.rb +213 -0
  39. data/lib/hecks/adapters/driving/newcomer.rb +50 -0
  40. data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +171 -0
  41. data/lib/hecks/adapters/driving/ruby/chapter.rb +91 -0
  42. data/lib/hecks/adapters/driving/ruby.rb +83 -0
  43. data/lib/hecks/adapters/driving/usage_cache.rb +144 -0
  44. data/lib/hecks/{doors.rb → adapters/driving.rb} +10 -6
  45. data/lib/hecks/behaviors/expectations/boot.rb +1 -1
  46. data/lib/hecks/bench/ruby_runner.rb +1 -1
  47. data/lib/hecks/bluebook/ask_resolution.rb +130 -0
  48. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +8 -2
  49. data/lib/hecks/bluebook/assembly/contracts/read_models.rb +1 -1
  50. data/lib/hecks/bluebook/assembly/contracts/rules.rb +4 -2
  51. data/lib/hecks/bluebook/assembly/contracts/structure.rb +3 -1
  52. data/lib/hecks/bluebook/assembly/marks.rb +4 -15
  53. data/lib/hecks/bluebook/assembly/scalars.rb +39 -0
  54. data/lib/hecks/bluebook/attribute.rb +1 -1
  55. data/lib/hecks/bluebook/behaviour/command.rb +13 -8
  56. data/lib/hecks/bluebook/behaviour/policy.rb +30 -0
  57. data/lib/hecks/bluebook/capabilities/attribute_values.rb +72 -0
  58. data/lib/hecks/bluebook/capabilities.rb +91 -9
  59. data/lib/hecks/bluebook/domain_port.rb +16 -4
  60. data/lib/hecks/bluebook/dsl/adapter_builder.rb +18 -6
  61. data/lib/hecks/bluebook/dsl/binding_proxy.rb +13 -0
  62. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/computed_sources.rb +98 -0
  63. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/fan_out_rows.rb +42 -0
  64. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/projected_fields.rb +14 -2
  65. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/provisions.rb +69 -3
  66. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/with_projections.rb +17 -10
  67. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +5 -0
  68. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
  69. data/lib/hecks/bluebook/dsl/command_builder/mutations.rb +8 -1
  70. data/lib/hecks/bluebook/dsl/command_builder/operands.rb +121 -0
  71. data/lib/hecks/bluebook/dsl/command_builder.rb +2 -0
  72. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +10 -1
  73. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +45 -2
  74. data/lib/hecks/bluebook/dsl/policy_builder.rb +47 -10
  75. data/lib/hecks/bluebook/dsl/rule_reference.rb +7 -4
  76. data/lib/hecks/bluebook/dsl/world_builder.rb +1 -1
  77. data/lib/hecks/bluebook/expression/ast_json/emitters.rb +6 -0
  78. data/lib/hecks/bluebook/expression/ast_json.rb +9 -6
  79. data/lib/hecks/bluebook/expression/ast_reader.rb +14 -9
  80. data/lib/hecks/bluebook/expression/projection.json +36 -0
  81. data/lib/hecks/bluebook/expression/resolver/operations.rb +31 -0
  82. data/lib/hecks/bluebook/expression/resolver/parsing.rb +38 -5
  83. data/lib/hecks/bluebook/expression/resolver/scanning.rb +22 -5
  84. data/lib/hecks/bluebook/expression/resolver/values.rb +10 -0
  85. data/lib/hecks/bluebook/expression/resolver.rb +20 -1
  86. data/lib/hecks/bluebook/hecksagon.rb +23 -3
  87. data/lib/hecks/bluebook/lifecycle.rb +22 -3
  88. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +13 -1
  89. data/lib/hecks/bluebook/meta_validator/modes.rb +4 -1
  90. data/lib/hecks/bluebook/meta_validator/readings.rb +24 -1
  91. data/lib/hecks/bluebook/meta_validator/reconstruction/heads.rb +2 -1
  92. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +6 -3
  93. data/lib/hecks/bluebook/meta_validator/shapes.rb +5 -0
  94. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +1 -1
  95. data/lib/hecks/bluebook/meta_validator.rb +12 -12
  96. data/lib/hecks/bluebook/model_check/ask_checks.rb +71 -0
  97. data/lib/hecks/bluebook/model_check/policy_checks.rb +2 -1
  98. data/lib/hecks/bluebook/model_check/reference_checks.rb +70 -0
  99. data/lib/hecks/bluebook/model_check.rb +8 -2
  100. data/lib/hecks/bluebook/policy.rb +4 -2
  101. data/lib/hecks/bluebook/smoke_test.rb +1 -1
  102. data/lib/hecks/bluebook/synthesizer.rb +2 -0
  103. data/lib/hecks/bluebook.rb +2 -0
  104. data/lib/hecks/cli/command_table.rb +1 -1
  105. data/lib/hecks/cli/console.rb +2 -1
  106. data/lib/hecks/cli/document.rb +1 -1
  107. data/lib/hecks/cli/launcher_words.rb +4 -4
  108. data/lib/hecks/cli/mcp.rb +4 -4
  109. data/lib/hecks/cli/project_cli/launcher_source.rb +2 -2
  110. data/lib/hecks/cli/project_cli/templates.rb +5 -5
  111. data/lib/hecks/cli/project_cli.rb +9 -6
  112. data/lib/hecks/cli/run.rb +2 -2
  113. data/lib/hecks/cli/smoke_http/checks.rb +4 -1
  114. data/lib/hecks/cli/smoke_http/transport.rb +3 -3
  115. data/lib/hecks/cli/smoke_http.rb +3 -0
  116. data/lib/hecks/codemod.rb +5 -2
  117. data/lib/hecks/corpus/routes.rb +7 -0
  118. data/lib/hecks/corpus/rust.rb +3 -2
  119. data/lib/hecks/deploy/bluebook/deploy.bluebook +34 -4
  120. data/lib/hecks/doc/reference/rendering.rb +6 -3
  121. data/lib/hecks/forms/app/commands.rb +8 -7
  122. data/lib/hecks/forms/app/queries.rb +2 -1
  123. data/lib/hecks/forms/app.rb +1 -1
  124. data/lib/hecks/forms/record_renderer.rb +1 -1
  125. data/lib/hecks/framework/bluebook/privacy.bluebook +11 -0
  126. data/lib/hecks/fuzzing/concurrent_dispatch.rb +4 -1
  127. data/lib/hecks/fuzzing/failure_triage.rb +2 -1
  128. data/lib/hecks/fuzzing/mutation/run.rb +11 -4
  129. data/lib/hecks/fuzzing/properties/catalog.rb +6 -4
  130. data/lib/hecks/fuzzing/properties/guaranteed_by_construction.rb +18 -11
  131. data/lib/hecks/fuzzing/properties/guards.rb +1 -1
  132. data/lib/hecks/fuzzing/self_consistency/saga_redelivery.rb +1 -1
  133. data/lib/hecks/fuzzing/self_consistency.rb +6 -2
  134. data/lib/hecks/fuzzing/shrinker.rb +3 -1
  135. data/lib/hecks/fuzzing/value_generator.rb +19 -5
  136. data/lib/hecks/gate/stages.yml +33 -7
  137. data/lib/hecks/grammar/evolve.rb +1 -1
  138. data/lib/hecks/grammar/expression_operators.json +372 -0
  139. data/lib/hecks/grammar/operators.rb +3 -1
  140. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +10 -10
  141. data/lib/hecks/hecks/adapters/codebase/inspection/rubycritic.rb +35 -0
  142. data/lib/hecks/hecks/adapters/codebase/inspection.rb +104 -0
  143. data/lib/hecks/hecks/adapters/codebase/publishing.rb +2 -1
  144. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +12 -2
  145. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +2 -1
  146. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +1 -1
  147. data/lib/hecks/hecks/adapters/cost_explorer.rb +8 -5
  148. data/lib/hecks/hecks/adapters/deploy_toolchain/statuses.rb +2 -1
  149. data/lib/hecks/hecks/adapters/finding_github.rb +13 -6
  150. data/lib/hecks/hecks/adapters/first_admin.rb +2 -2
  151. data/lib/hecks/hecks/adapters/github_rulesets.rb +6 -3
  152. data/lib/hecks/hecks/adapters/in_process_boot.rb +1 -1
  153. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +1 -1
  154. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +10 -20
  155. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +28 -5
  156. data/lib/hecks/hecks/adapters/local_files.rb +2 -2
  157. data/lib/hecks/hecks/adapters/rehearsal/audit_report.rb +107 -0
  158. data/lib/hecks/hecks/adapters/rehearsal/census.rb +71 -0
  159. data/lib/hecks/hecks/adapters/rehearsal/declaration.rb +133 -0
  160. data/lib/hecks/hecks/adapters/rehearsal/environment.rb +83 -0
  161. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +142 -0
  162. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +96 -0
  163. data/lib/hecks/hecks/adapters/rehearsal/postgres_census.rb +100 -0
  164. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +69 -0
  165. data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +133 -0
  166. data/lib/hecks/hecks/adapters/rehearsal/rows.rb +41 -0
  167. data/lib/hecks/hecks/adapters/rehearsal/rust_audit.rb +83 -0
  168. data/lib/hecks/hecks/adapters/rehearsal/subject.rb +104 -0
  169. data/lib/hecks/hecks/adapters/rehearsal.rb +54 -0
  170. data/lib/hecks/hecks/adapters/rehearsal_store.adapter +7 -0
  171. data/lib/hecks/hecks/adapters/rehearsal_store.rb +81 -0
  172. data/lib/hecks/hecks/adapters/terminal.rb +13 -11
  173. data/lib/hecks/hecks/codebase.behaviors +13 -1
  174. data/lib/hecks/hecks/codebase.bluebook +239 -5
  175. data/lib/hecks/hecks/custodian.behaviors +24 -6
  176. data/lib/hecks/hecks/custodian.bluebook +6 -1227
  177. data/lib/hecks/hecks/deploy.behaviors +1 -1
  178. data/lib/hecks/hecks/deploy.world +1 -3
  179. data/lib/hecks/hecks/hecks.behaviors +1 -1
  180. data/lib/hecks/hecks/hecks.hecksagon +53 -18
  181. data/lib/hecks/hecks/hecks.world +13 -10
  182. data/lib/hecks/hecks/rehearsal.bluebook +477 -0
  183. data/lib/hecks/hecks/site.behaviors +1 -1
  184. data/lib/hecks/hecks/site.world +0 -1
  185. data/lib/hecks/hecks/tooling.bluebook +1238 -0
  186. data/lib/hecks/indifferent_key.rb +20 -0
  187. data/lib/hecks/language/adapter.bluebook +13 -3
  188. data/lib/hecks/language/bluebook/aggregate.bluebook +23 -0
  189. data/lib/hecks/language/bluebook/bluebook.behaviors +23 -0
  190. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -0
  191. data/lib/hecks/language/bluebook/entity.bluebook +18 -0
  192. data/lib/hecks/language/bluebook/policy.behaviors +19 -0
  193. data/lib/hecks/language/bluebook/policy.bluebook +9 -1
  194. data/lib/hecks/language/bluebook/query.bluebook +1 -1
  195. data/lib/hecks/language/bluebook/vocabulary.bluebook +63 -4
  196. data/lib/hecks/language/hecksagon/domain_port.bluebook +1 -1
  197. data/lib/hecks/language/hecksagon/hecksagon.bluebook +22 -0
  198. data/lib/hecks/language/oidc.json +10 -0
  199. data/lib/hecks/language/port.bluebook +4 -4
  200. data/lib/hecks/literal.rb +12 -3
  201. data/lib/hecks/ports/key_vault.rb +7 -2
  202. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +21 -10
  203. data/lib/hecks/ports/persistence/plugins/era/postgres_era/boot.rb +4 -2
  204. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +2 -1
  205. data/lib/hecks/ports/persistence/repository_factory.rb +6 -3
  206. data/lib/hecks/projection_files/expression_tables.rb +7 -2
  207. data/lib/hecks/projections/deploy/box/compose.rb +6 -4
  208. data/lib/hecks/projections/deploy/box/edge.rb +62 -0
  209. data/lib/hecks/projections/deploy/box/logging.rb +99 -0
  210. data/lib/hecks/projections/deploy/box/roll_scripts.rb +16 -8
  211. data/lib/hecks/projections/deploy/box/settings/cdn.rb +40 -0
  212. data/lib/hecks/projections/deploy/box/settings/logs.rb +40 -0
  213. data/lib/hecks/projections/deploy/box/settings/patterns.rb +11 -0
  214. data/lib/hecks/projections/deploy/box/settings/types.rb +16 -1
  215. data/lib/hecks/projections/deploy/box/settings.rb +6 -2
  216. data/lib/hecks/projections/deploy/box/stack_yaml.rb +5 -2
  217. data/lib/hecks/projections/deploy/box/templates/Makefile.tmpl +16 -0
  218. data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +8 -4
  219. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +32 -0
  220. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +1 -1
  221. data/lib/hecks/projections/deploy/box/templates/edge.mk.tmpl +17 -0
  222. data/lib/hecks/projections/deploy/box/templates/edge.yaml.tmpl +65 -0
  223. data/lib/hecks/projections/deploy/box/templates/log-group.yaml.tmpl +12 -0
  224. data/lib/hecks/projections/deploy/box/templates/log-policy.yaml.tmpl +7 -0
  225. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +5 -4
  226. data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +5 -4
  227. data/lib/hecks/projections/deploy/box.rb +15 -3
  228. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  229. data/lib/hecks/projections/deploy/shared.rb +5 -2
  230. data/lib/hecks/projections/model/deviations.rb +2 -2
  231. data/lib/hecks/projections/model.rb +2 -2
  232. data/lib/hecks/projections/oidc.rb +13 -1
  233. data/lib/hecks/projections/shape.rb +5 -2
  234. data/lib/hecks/projections/site/cms_editor/attributes.rb +26 -6
  235. data/lib/hecks/projections/site/cms_editor/blocks.rb +157 -0
  236. data/lib/hecks/projections/site/cms_editor/checks.rb +14 -1
  237. data/lib/hecks/projections/site/cms_editor/clearing.rb +70 -0
  238. data/lib/hecks/projections/site/cms_editor/color.rb +124 -0
  239. data/lib/hecks/projections/site/cms_editor/composition.rb +42 -0
  240. data/lib/hecks/projections/site/cms_editor/destructive.rb +61 -0
  241. data/lib/hecks/projections/site/cms_editor/drafts.rb +102 -0
  242. data/lib/hecks/projections/site/cms_editor/hints.rb +74 -0
  243. data/lib/hecks/projections/site/cms_editor/icons.rb +86 -0
  244. data/lib/hecks/projections/site/cms_editor/key_kinds.rb +31 -0
  245. data/lib/hecks/projections/site/cms_editor/limits.rb +72 -0
  246. data/lib/hecks/projections/site/cms_editor/media.rb +1 -1
  247. data/lib/hecks/projections/site/cms_editor/moment.rb +34 -0
  248. data/lib/hecks/projections/site/cms_editor/palette.rb +115 -0
  249. data/lib/hecks/projections/site/cms_editor/pickers.rb +85 -0
  250. data/lib/hecks/projections/site/cms_editor/preview.rb +91 -0
  251. data/lib/hecks/projections/site/cms_editor/relations.rb +92 -0
  252. data/lib/hecks/projections/site/cms_editor/scale_checks.rb +66 -0
  253. data/lib/hecks/projections/site/cms_editor/scheduling.rb +150 -0
  254. data/lib/hecks/projections/site/cms_editor/schema.rb +96 -19
  255. data/lib/hecks/projections/site/cms_editor/setting.rb +52 -9
  256. data/lib/hecks/projections/site/cms_editor/targets.rb +86 -0
  257. data/lib/hecks/projections/site/cms_editor/templates/package.json.tmpl +18 -1
  258. data/lib/hecks/projections/site/cms_editor/templates/src/app.ts.tmpl +251 -57
  259. data/lib/hecks/projections/site/cms_editor/templates/src/assets.ts.tmpl +98 -0
  260. data/lib/hecks/projections/site/cms_editor/templates/src/auth/membership.ts.tmpl +32 -24
  261. data/lib/hecks/projections/site/cms_editor/templates/src/browser/app.css.tmpl +412 -0
  262. data/lib/hecks/projections/site/cms_editor/templates/src/browser/autosave.js.tmpl +144 -0
  263. data/lib/hecks/projections/site/cms_editor/templates/src/browser/blocks.js.tmpl +500 -0
  264. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_doc.js.tmpl +175 -0
  265. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_editor.js.tmpl +418 -0
  266. data/lib/hecks/projections/site/cms_editor/templates/src/browser/epoch.js.tmpl +61 -0
  267. data/lib/hecks/projections/site/cms_editor/templates/src/browser/forms.js.tmpl +210 -0
  268. data/lib/hecks/projections/site/cms_editor/templates/src/browser/lists.js.tmpl +40 -0
  269. data/lib/hecks/projections/site/cms_editor/templates/src/browser/main.js.tmpl +26 -0
  270. data/lib/hecks/projections/site/cms_editor/templates/src/browser/media_picker.js.tmpl +232 -0
  271. data/lib/hecks/projections/site/cms_editor/templates/src/browser/moments.js.tmpl +73 -0
  272. data/lib/hecks/projections/site/cms_editor/templates/src/browser/pickers.js.tmpl +152 -0
  273. data/lib/hecks/projections/site/cms_editor/templates/src/browser/preview.js.tmpl +98 -0
  274. data/lib/hecks/projections/site/cms_editor/templates/src/browser/shell.js.tmpl +140 -0
  275. data/lib/hecks/projections/site/cms_editor/templates/src/browser/toast.js.tmpl +66 -0
  276. data/lib/hecks/projections/site/cms_editor/templates/src/choices.ts.tmpl +99 -0
  277. data/lib/hecks/projections/site/cms_editor/templates/src/commands.ts.tmpl +78 -24
  278. data/lib/hecks/projections/site/cms_editor/templates/src/config.ts.tmpl +6 -0
  279. data/lib/hecks/projections/site/cms_editor/templates/src/flash.ts.tmpl +52 -0
  280. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +19 -2
  281. data/lib/hecks/projections/site/cms_editor/templates/src/media/handler.ts.tmpl +31 -8
  282. data/lib/hecks/projections/site/cms_editor/templates/src/names.ts.tmpl +36 -0
  283. data/lib/hecks/projections/site/cms_editor/templates/src/schema.ts.tmpl +131 -2
  284. data/lib/hecks/projections/site/cms_editor/templates/src/ui/availability.ts.tmpl +15 -0
  285. data/lib/hecks/projections/site/cms_editor/templates/src/ui/block_list.js.tmpl +213 -0
  286. data/lib/hecks/projections/site/cms_editor/templates/src/ui/blocks.ts.tmpl +275 -0
  287. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_model.js.tmpl +8 -5
  288. data/lib/hecks/projections/site/cms_editor/templates/src/ui/detail.ts.tmpl +88 -0
  289. data/lib/hecks/projections/site/cms_editor/templates/src/ui/dialogs.ts.tmpl +43 -0
  290. data/lib/hecks/projections/site/cms_editor/templates/src/ui/fields.ts.tmpl +229 -34
  291. data/lib/hecks/projections/site/cms_editor/templates/src/ui/html.ts.tmpl +40 -81
  292. data/lib/hecks/projections/site/cms_editor/templates/src/ui/icons.ts.tmpl +15 -0
  293. data/lib/hecks/projections/site/cms_editor/templates/src/ui/input.ts.tmpl +29 -12
  294. data/lib/hecks/projections/site/cms_editor/templates/src/ui/links.ts.tmpl +100 -0
  295. data/lib/hecks/projections/site/cms_editor/templates/src/ui/moments.ts.tmpl +44 -0
  296. data/lib/hecks/projections/site/cms_editor/templates/src/ui/outline.ts.tmpl +53 -17
  297. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pages.ts.tmpl +178 -88
  298. data/lib/hecks/projections/site/cms_editor/templates/src/ui/paging.ts.tmpl +82 -0
  299. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pickers.ts.tmpl +110 -0
  300. data/lib/hecks/projections/site/cms_editor/templates/src/ui/preview.ts.tmpl +87 -0
  301. data/lib/hecks/projections/site/cms_editor/templates/src/ui/related.ts.tmpl +97 -0
  302. data/lib/hecks/projections/site/cms_editor/templates/src/ui/schedule.ts.tmpl +160 -0
  303. data/lib/hecks/projections/site/cms_editor/templates/src/ui/shell.ts.tmpl +211 -0
  304. data/lib/hecks/projections/site/cms_editor/templates/src/ui/table.ts.tmpl +151 -0
  305. data/lib/hecks/projections/site/cms_editor/templates/src/ui/theme.js.tmpl +16 -0
  306. data/lib/hecks/projections/site/cms_editor/templates/src/ui/values.ts.tmpl +69 -0
  307. data/lib/hecks/projections/site/cms_editor/templates/src/ui/words.ts.tmpl +97 -0
  308. data/lib/hecks/projections/site/cms_editor/templates/src/ui/world.ts.tmpl +35 -0
  309. data/lib/hecks/projections/site/cms_editor/templates/tsconfig.json.tmpl +2 -1
  310. data/lib/hecks/projections/site/cms_editor/theme.rb +29 -0
  311. data/lib/hecks/projections/site/cms_editor/theme_checks.rb +57 -0
  312. data/lib/hecks/projections/site/cms_editor.rb +49 -23
  313. data/lib/hecks/projections/site/role_probe.rb +2 -0
  314. data/lib/hecks/projections/site/site_root/workflow.rb +5 -0
  315. data/lib/hecks/projector/cli_audience.rb +22 -1
  316. data/lib/hecks/projector/cli_projector/listing.rb +1 -1
  317. data/lib/hecks/projector/cli_projector/option_specs.rb +1 -1
  318. data/lib/hecks/projector/cli_projector/usage.rb +15 -4
  319. data/lib/hecks/projector/docs_projector/closing.rb +1 -1
  320. data/lib/hecks/projector/docs_projector.rb +1 -1
  321. data/lib/hecks/projector/exporter/bindings.rb +40 -10
  322. data/lib/hecks/projector/exporter/capability_values.rb +55 -0
  323. data/lib/hecks/projector/narrate_projector/reactions.rb +1 -1
  324. data/lib/hecks/projector/target.rb +11 -0
  325. data/lib/hecks/projector.rb +32 -5
  326. data/lib/hecks/quality_control/adapters/github_checks.rb +12 -8
  327. data/lib/hecks/quality_control/adapters/github_issues.rb +8 -4
  328. data/lib/hecks/quality_control/cli/qa_open_pr.rb +2 -2
  329. data/lib/hecks/quality_control/cli/qa_sweep/checks/shrinking.rb +1 -1
  330. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +1 -1
  331. data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
  332. data/lib/hecks/quality_control/quality_control.bluebook +6 -2
  333. data/lib/hecks/release/approval.rb +64 -0
  334. data/lib/hecks/router/{door.rb → aggregate_reader.rb} +5 -4
  335. data/lib/hecks/router/namespace_installer.rb +16 -16
  336. data/lib/hecks/router.rb +1 -1
  337. data/lib/hecks/runtime/best_effort.rb +22 -0
  338. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -2
  339. data/lib/hecks/runtime/command_interpreter.rb +3 -2
  340. data/lib/hecks/runtime/command_rules/emission.rb +1 -1
  341. data/lib/hecks/runtime/dispatcher.rb +4 -4
  342. data/lib/hecks/runtime/entity_element/location.rb +1 -1
  343. data/lib/hecks/runtime/event.rb +5 -0
  344. data/lib/hecks/runtime/identity.rb +7 -6
  345. data/lib/hecks/runtime/interpreting.rb +2 -1
  346. data/lib/hecks/runtime/invocation_builder.rb +1 -1
  347. data/lib/hecks/runtime/loader/boot_steps.rb +8 -5
  348. data/lib/hecks/runtime/loader.rb +10 -10
  349. data/lib/hecks/runtime/policy_interpreter/arguments.rb +7 -4
  350. data/lib/hecks/runtime/policy_interpreter/fan_out.rb +3 -3
  351. data/lib/hecks/runtime/policy_interpreter.rb +6 -6
  352. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -1
  353. data/lib/hecks/runtime/reaction_invocation.rb +1 -1
  354. data/lib/hecks/runtime/rebuild_sweep.rb +11 -1
  355. data/lib/hecks/runtime/registry/ask_verification.rb +35 -0
  356. data/lib/hecks/runtime/registry/hecksagon_rules.rb +10 -0
  357. data/lib/hecks/runtime/registry/merging.rb +1 -1
  358. data/lib/hecks/runtime/registry/providers.rb +16 -0
  359. data/lib/hecks/runtime/registry/read_repository.rb +6 -4
  360. data/lib/hecks/runtime/registry/verification.rb +4 -1
  361. data/lib/hecks/runtime/registry/world_defaults.rb +29 -0
  362. data/lib/hecks/runtime/registry.rb +1 -1
  363. data/lib/hecks/runtime/saga_interpreter/compensation.rb +1 -1
  364. data/lib/hecks/runtime/saga_interpreter/delivery.rb +4 -4
  365. data/lib/hecks/runtime/saga_interpreter/transitions.rb +1 -1
  366. data/lib/hecks/runtime/saga_interpreter.rb +3 -3
  367. data/lib/hecks/runtime/tenant_check.rb +5 -4
  368. data/lib/hecks/runtime/value/coercion.rb +2 -2
  369. data/lib/hecks/runtime/value/validation.rb +16 -4
  370. data/lib/hecks/runtime.rb +9 -9
  371. data/lib/hecks/rust_build/host.rb +11 -4
  372. data/lib/hecks/rust_build/prebuilt_binary.rb +101 -0
  373. data/lib/hecks/rust_build/project_rust/target_ir.rb +1 -1
  374. data/lib/hecks/rust_build/wasm.rb +1 -1
  375. data/lib/hecks/rust_build/wasm_browser.rb +1 -1
  376. data/lib/hecks/rust_build.rb +11 -0
  377. data/lib/hecks/served_options.rb +53 -0
  378. data/lib/hecks/site/bluebook/site.bluebook +1 -0
  379. data/lib/hecks/storehouse/audit.rb +1 -1
  380. data/lib/hecks/storehouse/dispatching.rb +8 -6
  381. data/lib/hecks/storehouse/introspection.rb +2 -2
  382. data/lib/hecks/storehouse/querying.rb +6 -4
  383. data/lib/hecks/storehouse.rb +4 -4
  384. data/lib/hecks/tools/ci_gate_decision.rb +35 -0
  385. data/lib/hecks/tools/ci_gates.rb +20 -1
  386. data/lib/hecks/tools/comment_style/source_file.rb +4 -1
  387. data/lib/hecks/tools/deploy_recipe_lint/fixtures.rb +4 -1
  388. data/lib/hecks/tools/lanes/files.rb +8 -0
  389. data/lib/hecks/tools/lanes/prebuilt_workflow.rb +81 -0
  390. data/lib/hecks/tools/lanes/rows.rb +6 -0
  391. data/lib/hecks/tools/lanes.rb +1 -0
  392. data/lib/hecks/tools/oidc_manifests.rb +4 -5
  393. data/lib/hecks/tools/regeneration_run.rb +11 -4
  394. data/lib/hecks/tools/release_only.rb +111 -0
  395. data/lib/hecks/tools/site_routes/editor_files.rb +12 -5
  396. data/lib/hecks/tools/tenant_provisioning.rb +2 -2
  397. data/lib/hecks/tools/tools_doc.rb +4 -4
  398. data/lib/hecks/version.rb +1 -1
  399. data/lib/hecks/vocabulary.rb +19 -5
  400. data/lib/hecks.rb +15 -15
  401. data/lib/rubocop/cop/hecks/env_dump.rb +69 -0
  402. data/lib/rubocop/cop/hecks/monotonic_duration.rb +53 -0
  403. data/lib/rubocop/cop/hecks/no_wall_clock_in_runtime.rb +34 -0
  404. data/lib/rubocop/cop/hecks/raw_shell_interpolation.rb +79 -0
  405. data/lib/rubocop/cop/hecks/sleep_in_spec.rb +34 -0
  406. data/lib/rubocop/cop/hecks/swallowed_rescue.rb +37 -0
  407. data/lib/rubocop/cop/hecks/symbol_string_key_mix.rb +50 -0
  408. data/lib/rubocop/cop/hecks/untyped_raise.rb +37 -0
  409. data/prebuilt/9ee54e2f0d4d6d3fe046827779b221e02de665fba911a048ae5dd8b8fbfb7d90.marshal +0 -0
  410. data/prebuilt/d74a00e996222c1d8b676d5959092effc46b4dc59e0555f3bf9554ac58e3094a.marshal +0 -0
  411. data/prebuilt/verdicts-df19f35868d3284a70f55e42dcef87ef4e65e6a678e5135f72e1f6cff4c1357b.json +1 -0
  412. data/rust/.gitignore +6 -0
  413. data/rust/Cargo.toml +6 -0
  414. data/rust/build/src/binding_pass.rs +103 -0
  415. data/rust/build/src/cargo_sync.rs +6 -0
  416. data/rust/build/src/lineage_pass.rs +3 -3
  417. data/rust/build/src/main.rs +17 -5
  418. data/rust/build/src/pipeline.rs +48 -5
  419. data/rust/build/src/seams_pass.rs +123 -0
  420. data/rust/build/src/translation_pass.rs +167 -0
  421. data/rust/codegen/src/asks.rs +103 -0
  422. data/rust/codegen/src/commands.rs +13 -8
  423. data/rust/codegen/src/constraints.rs +25 -0
  424. data/rust/codegen/src/domain_generator.rs +2 -0
  425. data/rust/codegen/src/expr_emitter.rs +13 -0
  426. data/rust/codegen/src/json_codec.rs +54 -15
  427. data/rust/codegen/src/main.rs +1 -0
  428. data/rust/codegen/src/mutations.rs +128 -3
  429. data/rust/codegen/src/queries.rs +11 -2
  430. data/rust/codegen/src/reactions.rs +1 -1
  431. data/rust/codegen/src/types.rs +166 -8
  432. data/rust/host/HECKS_RELEASE +1 -1
  433. data/rust/host/src/api.rs +4 -4
  434. data/rust/host/src/bin/validate_harness.rs +43 -0
  435. data/rust/host/src/boundary_fuzz/expr_json.rs +2 -2
  436. data/rust/host/src/boundary_fuzz/ir.rs +1 -0
  437. data/rust/host/src/boundary_fuzz/server.rs +1 -1
  438. data/rust/host/src/checkout.rs +35 -19
  439. data/rust/host/src/commerce_ir.rs +408 -2
  440. data/rust/host/src/expr_json.rs +118 -0
  441. data/rust/host/src/extension.rs +13 -1
  442. data/rust/host/src/lib.rs +1 -0
  443. data/rust/host/src/mint.rs +1 -1
  444. data/rust/host/src/payments/tests.rs +27 -1
  445. data/rust/host/src/payments.rs +12 -4
  446. data/rust/host/src/platform_signin/attack_tests.rs +1100 -0
  447. data/rust/host/src/platform_signin/config.rs +512 -0
  448. data/rust/host/src/platform_signin/cookie.rs +237 -0
  449. data/rust/host/src/platform_signin/extension.rs +190 -0
  450. data/rust/host/src/platform_signin/fetcher.rs +322 -0
  451. data/rust/host/src/platform_signin/handler.rs +251 -0
  452. data/rust/host/src/platform_signin/handler_tests.rs +554 -0
  453. data/rust/host/src/platform_signin/jti.rs +196 -0
  454. data/rust/host/src/platform_signin/jwks.rs +542 -0
  455. data/rust/host/src/platform_signin/jws.rs +249 -0
  456. data/rust/host/src/platform_signin/known_answer_tests.rs +237 -0
  457. data/rust/host/src/platform_signin/mapping_tests.rs +254 -0
  458. data/rust/host/src/platform_signin/mod.rs +75 -0
  459. data/rust/host/src/platform_signin/mode_tests.rs +382 -0
  460. data/rust/host/src/platform_signin/pages.rs +204 -0
  461. data/rust/host/src/platform_signin/query.rs +233 -0
  462. data/rust/host/src/platform_signin/session.rs +151 -0
  463. data/rust/host/src/platform_signin/settings.rs +385 -0
  464. data/rust/host/src/platform_signin/strict_json.rs +351 -0
  465. data/rust/host/src/platform_signin/test_support.rs +235 -0
  466. data/rust/host/src/platform_signin/ttl_tests.rs +94 -0
  467. data/rust/host/src/platform_signin/verify.rs +604 -0
  468. data/rust/host/src/platform_signin/wiring_support.rs +319 -0
  469. data/rust/host/src/query_step.rs +22 -4
  470. data/rust/host/src/reference_validate.rs +653 -108
  471. data/rust/host/src/resend.rs +2 -0
  472. data/rust/host/src/server.rs +19 -6
  473. data/rust/host/src/ui_schema.rs +132 -5
  474. data/rust/host/src/web/newsletter.rs +7 -8
  475. data/rust/host/src/web/newsletter_send.rs +38 -13
  476. data/rust/host/src/web/registrations.rs +229 -26
  477. data/rust/host/src/web.rs +53 -9
  478. data/rust/parser/src/emit.rs +38 -6
  479. data/rust/parser/src/expr/ast_json.rs +7 -1
  480. data/rust/parser/src/expr/resolver.rs +171 -6
  481. data/rust/parser/src/ir.rs +7 -0
  482. data/rust/parser/src/keywords.rs +24 -0
  483. data/rust/parser/src/main.rs +3 -0
  484. data/rust/parser/src/parse/aggregate.rs +2 -1
  485. data/rust/parser/src/parse/chapter.rs +2 -2
  486. data/rust/parser/src/parse/command.rs +2 -0
  487. data/rust/parser/src/parse/hecksagon.rs +5 -2
  488. data/rust/parser/src/parse/lifecycle.rs +81 -0
  489. data/rust/parser/src/parse/mod.rs +15 -1
  490. data/rust/parser/src/parse/policy.rs +13 -2
  491. data/rust/parser/src/parse/value_object.rs +34 -17
  492. data/rust/parser/src/ruby_value.rs +1 -1
  493. data/rust/src/exemplar/commands.rs +4 -4
  494. data/rust/src/exemplar/constraints.rs +1 -1
  495. data/rust/src/exemplar/json.rs +43 -1
  496. data/rust/src/kernel/cli.rs +7 -5
  497. data/rust/src/kernel/dispatch.rs +2 -2
  498. data/rust/src/kernel/dispatch_guard_tests.rs +1 -1
  499. data/rust/src/kernel/expr.rs +66 -3
  500. data/rust/src/kernel/expression_operators/arithmetic.rs +130 -5
  501. data/rust/src/kernel/expression_operators/text.rs +26 -2
  502. data/rust/src/kernel/json.rs +15 -0
  503. data/rust/src/kernel/mod.rs +1 -1
  504. data/rust/src/kernel/orchestrate.rs +54 -5
  505. data/rust/src/kernel/orchestrate_guard_tests.rs +67 -0
  506. data/rust/src/kernel/pattern.rs +60 -0
  507. data/rust/src/kernel/reference_lookup.rs +90 -7
  508. metadata +178 -38
  509. data/lib/hecks/doors/cli_door/nesting.rb +0 -34
  510. data/lib/hecks/doors/cli_door/shorthand.rb +0 -85
  511. data/lib/hecks/doors/cli_door.rb +0 -124
  512. data/lib/hecks/doors/cli_runner/answers.rb +0 -138
  513. data/lib/hecks/doors/cli_runner/resolution.rb +0 -146
  514. data/lib/hecks/doors/cli_runner/settling.rb +0 -115
  515. data/lib/hecks/doors/cli_runner/streaming.rb +0 -90
  516. data/lib/hecks/doors/cli_runner/suggestions.rb +0 -43
  517. data/lib/hecks/doors/cli_runner.rb +0 -66
  518. data/lib/hecks/doors/command_request.rb +0 -119
  519. data/lib/hecks/doors/handle.rb +0 -208
  520. data/lib/hecks/doors/json_door.rb +0 -132
  521. data/lib/hecks/doors/launcher_options.rb +0 -247
  522. data/lib/hecks/doors/mcp_door/properties.rb +0 -47
  523. data/lib/hecks/doors/mcp_door/tool_calls.rb +0 -113
  524. data/lib/hecks/doors/mcp_door/tool_definitions/auditing.rb +0 -51
  525. data/lib/hecks/doors/mcp_door/tool_definitions/calling.rb +0 -69
  526. data/lib/hecks/doors/mcp_door/tool_definitions/reading.rb +0 -97
  527. data/lib/hecks/doors/mcp_door/tool_definitions.rb +0 -15
  528. data/lib/hecks/doors/mcp_door.rb +0 -156
  529. data/lib/hecks/doors/mcp_door_scope/argument_policy.rb +0 -92
  530. data/lib/hecks/doors/mcp_door_scope/env_problems.rb +0 -55
  531. data/lib/hecks/doors/mcp_door_scope/presenter.rb +0 -78
  532. data/lib/hecks/doors/mcp_door_scope.rb +0 -201
  533. data/lib/hecks/doors/mcp_guide_cache.rb +0 -75
  534. data/lib/hecks/doors/ruby_door/aggregate_door.rb +0 -162
  535. data/lib/hecks/doors/ruby_door/chapter.rb +0 -89
  536. data/lib/hecks/doors/ruby_door.rb +0 -80
  537. data/lib/hecks/doors/usage_cache.rb +0 -139
  538. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_widget.js.tmpl +0 -282
  539. data/lib/hecks/projections/site/cms_editor/templates/src/ui/media_picker.js.tmpl +0 -109
  540. data/prebuilt/c28d6d9db14eea3e17161e9d9c262efee30b256cf97693a7cfd552bce24b2f7f.marshal +0 -0
  541. data/prebuilt/cb375cba61ca0dbe8ddb9739c306510fe05e5f4b7a9b6955caff8224c63ea0e0.marshal +0 -0
  542. data/prebuilt/verdicts-d6d584ad43b5f3cebf88aafbbdbf024c38574acf18e06adffbc555f7786b1c2f.json +0 -1
@@ -1,7 +1,7 @@
1
1
  //! Parses value expressions into a `Resolver` tree (port of `resolver.rb#parse`).
2
2
 
3
3
  use super::evaluator::Evaluator;
4
- use super::{find_operator, top_level_index, Operator};
4
+ use super::{find_operator, Operator};
5
5
 
6
6
  /// Which of `all?`, `any?` or `none?` a block predicate uses.
7
7
  #[derive(Debug, Clone, Copy, PartialEq)]
@@ -39,6 +39,10 @@ pub enum Resolver {
39
39
  BoolLiteral(bool),
40
40
  NilLiteral,
41
41
  Addition(Box<Resolver>, Box<Resolver>),
42
+ Subtraction(Box<Resolver>, Box<Resolver>),
43
+ Multiplication(Box<Resolver>, Box<Resolver>),
44
+ /// Integer operands floor toward negative infinity; a zero divisor is an evaluation fault.
45
+ Division(Box<Resolver>, Box<Resolver>),
42
46
  SignTest { operator: Operator, receiver: Box<Resolver> },
43
47
  Empty(Box<Resolver>),
44
48
  ToS(Box<Resolver>),
@@ -57,6 +61,8 @@ pub enum Resolver {
57
61
  Assignment { receiver: Box<Resolver>, negated: bool },
58
62
  /// `receiver.split("sep")`: the separator is sliced verbatim between the quotes.
59
63
  Split { receiver: Box<Resolver>, separator: String },
64
+ /// `receiver.strip` / `.lstrip` / `.rstrip`; `side` is `both`, `left` or `right`.
65
+ Strip { receiver: Box<Resolver>, side: &'static str },
60
66
  /// `receiver.first`.
61
67
  First(Box<Resolver>),
62
68
  /// `receiver.last`.
@@ -100,8 +106,16 @@ pub fn parse(expr: &str) -> Resolver {
100
106
  return Resolver::ArrayLiteral(elements.iter().map(|element| parse(element)).collect());
101
107
  }
102
108
 
103
- if let Some((left, right)) = split_addition(expr) {
104
- return Resolver::Addition(Box::new(parse(left)), Box::new(parse(right)));
109
+ if let Some((operator, left, right)) = split_last_binary(expr, b"+-") {
110
+ let (left, right) = (Box::new(parse(left)), Box::new(parse(right)));
111
+ return if operator == b'+' { Resolver::Addition(left, right) } else { Resolver::Subtraction(left, right) };
112
+ }
113
+ if let Some((operator, left, right)) = split_last_binary(expr, b"*/") {
114
+ let (left, right) = (Box::new(parse(left)), Box::new(parse(right)));
115
+ return if operator == b'*' { Resolver::Multiplication(left, right) } else { Resolver::Division(left, right) };
116
+ }
117
+ if grouped(expr) {
118
+ return parse(&expr[1..expr.len() - 1]);
105
119
  }
106
120
 
107
121
  if let Some((receiver, test)) = match_suffix(expr, &SIGN_TESTS.map(|(s, _)| s)) {
@@ -139,6 +153,12 @@ pub fn parse(expr: &str) -> Resolver {
139
153
  }
140
154
  }
141
155
 
156
+ for (suffix, side) in [("strip", "both"), ("lstrip", "left"), ("rstrip", "right")] {
157
+ if let Some(inner) = strip_suffix_dotted(expr, suffix) {
158
+ return Resolver::Strip { receiver: Box::new(parse(inner)), side };
159
+ }
160
+ }
161
+
142
162
  if let Some((receiver, separator)) = match_string_call(expr, "split") {
143
163
  return Resolver::Split { receiver: Box::new(parse(receiver)), separator: separator.to_string() };
144
164
  }
@@ -351,9 +371,118 @@ fn array_elements(expr: &str) -> Option<Vec<String>> {
351
371
  Some(elements.into_iter().filter(|element| !element.is_empty()).collect())
352
372
  }
353
373
 
354
- fn split_addition(expr: &str) -> Option<(&str, &str)> {
355
- let index = top_level_index(expr, "+", |_| true)?;
356
- Some((expr[..index].trim(), expr[index + 1..].trim()))
374
+ /// The canonical text of an arithmetic expression over names and numbers, as the Ruby DSL prints
375
+ /// it: one space around each operator, parentheses only where the grouping needs them. `None` when
376
+ /// `expr` is not arithmetic, or holds anything but names and numbers.
377
+ pub fn arithmetic_text(expr: &str) -> Option<String> {
378
+ let node = parse(expr);
379
+ match node {
380
+ Resolver::Addition(..) | Resolver::Subtraction(..) | Resolver::Multiplication(..) | Resolver::Division(..) => render_arithmetic(&node),
381
+ _ => None,
382
+ }
383
+ }
384
+
385
+ fn render_arithmetic(node: &Resolver) -> Option<String> {
386
+ match node {
387
+ Resolver::Addition(l, r) => render_binary(l, '+', r),
388
+ Resolver::Subtraction(l, r) => render_binary(l, '-', r),
389
+ Resolver::Multiplication(l, r) => render_binary(l, '*', r),
390
+ Resolver::Division(l, r) => render_binary(l, '/', r),
391
+ Resolver::Lookup(path) => Some(path.clone()),
392
+ Resolver::IntegerLiteral(n) => Some(n.to_string()),
393
+ Resolver::FloatLiteral(f) => Some(crate::ruby_value::format_ruby_float(*f)),
394
+ _ => None,
395
+ }
396
+ }
397
+
398
+ fn binding_strength(operator: char) -> u8 {
399
+ if matches!(operator, '*' | '/') { 2 } else { 1 }
400
+ }
401
+
402
+ fn node_strength(node: &Resolver) -> u8 {
403
+ match node {
404
+ Resolver::Addition(..) | Resolver::Subtraction(..) => 1,
405
+ Resolver::Multiplication(..) | Resolver::Division(..) => 2,
406
+ _ => 0,
407
+ }
408
+ }
409
+
410
+ // A joined side is parenthesised when it binds looser than the operator, or equally on the right,
411
+ // so the printed text parses back to the same grouping.
412
+ fn render_binary(left: &Resolver, operator: char, right: &Resolver) -> Option<String> {
413
+ let strength = binding_strength(operator);
414
+ let side = |node: &Resolver, on_right: bool| -> Option<String> {
415
+ let text = render_arithmetic(node)?;
416
+ let held = node_strength(node);
417
+ Some(if held != 0 && (held < strength || (on_right && held == strength)) { format!("({text})") } else { text })
418
+ };
419
+ Some(format!("{} {operator} {}", side(left, false)?, side(right, true)?))
420
+ }
421
+
422
+ /// The operator and the texts either side of the LAST top-level binary operator in `operators`, so
423
+ /// a chain splits left-associatively. A `-` is binary only after an operand (`a - b`, not `a - -b`
424
+ /// or a leading `-5`). Brackets of every kind count toward depth, quotes hide their contents.
425
+ fn split_last_binary<'a>(expr: &'a str, operators: &[u8]) -> Option<(u8, &'a str, &'a str)> {
426
+ let bytes = expr.as_bytes();
427
+ let mut depth: i32 = 0;
428
+ let mut quote: Option<u8> = None;
429
+ let mut found: Option<usize> = None;
430
+
431
+ for (index, &byte) in bytes.iter().enumerate() {
432
+ if let Some(open) = quote {
433
+ if byte == open {
434
+ quote = None;
435
+ }
436
+ continue;
437
+ }
438
+ match byte {
439
+ b'"' | b'\'' => quote = Some(byte),
440
+ b'(' | b'{' | b'[' => depth += 1,
441
+ b')' | b'}' | b']' => depth -= 1,
442
+ _ if depth == 0 && operators.contains(&byte) && binary_position(bytes, index) => found = Some(index),
443
+ _ => {}
444
+ }
445
+ }
446
+ found.map(|index| (bytes[index], expr[..index].trim(), expr[index + 1..].trim()))
447
+ }
448
+
449
+ // Only `-` can also be a sign, so it needs an operand ending on its left.
450
+ fn binary_position(bytes: &[u8], index: usize) -> bool {
451
+ if bytes[index] != b'-' {
452
+ return true;
453
+ }
454
+ let before = bytes[..index].iter().rev().find(|b| !b.is_ascii_whitespace());
455
+ matches!(before, Some(b) if b.is_ascii_alphanumeric() || matches!(b, b'_' | b')' | b']' | b'"' | b'\''))
456
+ }
457
+
458
+ /// Whether `expr` is one parenthesised group: its first `(` closes at its last character.
459
+ fn grouped(expr: &str) -> bool {
460
+ let bytes = expr.as_bytes();
461
+ if bytes.first() != Some(&b'(') || bytes.last() != Some(&b')') {
462
+ return false;
463
+ }
464
+ let mut depth: i32 = 0;
465
+ let mut quote: Option<u8> = None;
466
+ for (index, &byte) in bytes.iter().enumerate() {
467
+ if let Some(open) = quote {
468
+ if byte == open {
469
+ quote = None;
470
+ }
471
+ continue;
472
+ }
473
+ match byte {
474
+ b'"' | b'\'' => quote = Some(byte),
475
+ b'(' => depth += 1,
476
+ b')' => {
477
+ depth -= 1;
478
+ if depth == 0 {
479
+ return index == bytes.len() - 1;
480
+ }
481
+ }
482
+ _ => {}
483
+ }
484
+ }
485
+ false
357
486
  }
358
487
 
359
488
  fn match_suffix<'a>(expr: &'a str, suffixes: &[&'a str]) -> Option<(&'a str, &'a str)> {
@@ -485,6 +614,9 @@ mod tests {
485
614
  ("a.set?", format!(r#"{{"op":"assignment","receiver":{a},"negated":false}}"#)),
486
615
  ("a.unset?", format!(r#"{{"op":"assignment","receiver":{a},"negated":true}}"#)),
487
616
  ("a.split(\"/\")", format!(r#"{{"op":"split","receiver":{a},"separator":"/"}}"#)),
617
+ ("a.strip", format!(r#"{{"op":"strip","receiver":{a},"side":"both"}}"#)),
618
+ ("a.lstrip", format!(r#"{{"op":"strip","receiver":{a},"side":"left"}}"#)),
619
+ ("a.rstrip", format!(r#"{{"op":"strip","receiver":{a},"side":"right"}}"#)),
488
620
  ("a.first", format!(r#"{{"op":"first","receiver":{a}}}"#)),
489
621
  ("a.last", format!(r#"{{"op":"last","receiver":{a}}}"#)),
490
622
  ("a.start_with?(\"x\")", format!(r#"{{"op":"starts_with","receiver":{a},"substring":"x"}}"#)),
@@ -494,4 +626,37 @@ mod tests {
494
626
  assert_eq!(compact(text), expected, "{text}");
495
627
  }
496
628
  }
629
+
630
+ #[test]
631
+ fn splits_arithmetic_by_level_and_left_to_right() {
632
+ assert!(matches!(parse("a + b * c"), Resolver::Addition(..)));
633
+ assert!(matches!(parse("a - b - c"), Resolver::Subtraction(left, _) if matches!(*left, Resolver::Subtraction(..))));
634
+ assert!(matches!(parse("a * b / c"), Resolver::Division(left, _) if matches!(*left, Resolver::Multiplication(..))));
635
+ }
636
+
637
+ #[test]
638
+ fn a_minus_is_binary_only_after_an_operand() {
639
+ assert!(matches!(parse("a - -5"), Resolver::Subtraction(_, right) if matches!(*right, Resolver::IntegerLiteral(-5))));
640
+ assert!(matches!(parse("-5"), Resolver::IntegerLiteral(-5)));
641
+ }
642
+
643
+ #[test]
644
+ fn a_parenthesised_group_reads_as_what_it_wraps() {
645
+ assert!(matches!(parse("(a + b) * c"), Resolver::Multiplication(left, _) if matches!(*left, Resolver::Addition(..))));
646
+ }
647
+
648
+ #[test]
649
+ fn prints_arithmetic_with_parentheses_only_where_the_grouping_needs_them() {
650
+ assert_eq!(arithmetic_text("a*b/100").as_deref(), Some("a * b / 100"));
651
+ assert_eq!(arithmetic_text("(a+b)*2").as_deref(), Some("(a + b) * 2"));
652
+ assert_eq!(arithmetic_text("a-(b-c)").as_deref(), Some("a - (b - c)"));
653
+ assert_eq!(arithmetic_text("(a*b)+c").as_deref(), Some("a * b + c"));
654
+ }
655
+
656
+ #[test]
657
+ fn text_that_is_not_arithmetic_over_names_and_numbers_prints_nothing() {
658
+ assert_eq!(arithmetic_text("paid"), None);
659
+ assert_eq!(arithmetic_text("a.size + 1"), None);
660
+ assert_eq!(arithmetic_text("\"x\""), None);
661
+ }
497
662
  }
@@ -60,6 +60,8 @@ pub enum MutationSource {
60
60
  Literal(crate::ruby_value::Value),
61
61
  /// `state(:field)`: the record's own field, read from the pre-dispatch state.
62
62
  State(String),
63
+ /// Arithmetic over argument names, record fields and numbers: its canonical text.
64
+ Expression(String),
63
65
  }
64
66
 
65
67
  #[derive(Debug, Clone)]
@@ -120,6 +122,8 @@ pub struct Lifecycle {
120
122
  pub field: String,
121
123
  pub default: String,
122
124
  pub transitions: Vec<StateTransitionRow>,
125
+ // `mark :name, "state", ...`: each meaning and its states, in declared order.
126
+ pub marks: Vec<(String, Vec<String>)>,
123
127
  }
124
128
 
125
129
  #[derive(Debug, Clone, Default)]
@@ -285,6 +289,9 @@ pub struct Policy {
285
289
  pub name: String,
286
290
  pub on_event: Option<String>,
287
291
  pub trigger_command: Option<String>,
292
+ // `ask :name`: the need, in the domain's words. Emitted in place of `trigger_command`; the
293
+ // hecksagon's declared asks say which port operation answers.
294
+ pub ask: Option<String>,
288
295
  pub target_domain: Option<String>,
289
296
  // `across "X", expect_undelivered: true`; emitted as a boolean, `false` when not declared.
290
297
  pub expect_undelivered: bool,
@@ -82,6 +82,7 @@ pub static KEYWORDS: &[KeywordRow] = &[
82
82
  KeywordRow { word: "given", context: "Aggregate", body: "source", inner: "", opens: "", fills: "preconditions", status: "admitted", was: "", resolves_via: "owner_keyed", disambiguator: "declared_by" },
83
83
  KeywordRow { word: "projects", context: "Aggregate", body: "none", inner: "", opens: "", fills: "projected_fields", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
84
84
  KeywordRow { word: "transition", context: "Lifecycle", body: "none", inner: "", opens: "", fills: "transitions", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
85
+ KeywordRow { word: "mark", context: "Lifecycle", body: "none", inner: "", opens: "", fills: "marks", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
85
86
  KeywordRow { word: "bluebook", context: "File", body: "keywords", inner: "Bluebook", opens: "Bluebook", fills: "", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
86
87
  KeywordRow { word: "vision", context: "Bluebook", body: "none", inner: "", opens: "", fills: "vision", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
87
88
  KeywordRow { word: "formerly_known_as", context: "Bluebook", body: "none", inner: "", opens: "", fills: "formerly_known_as", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
@@ -125,6 +126,7 @@ pub static KEYWORDS: &[KeywordRow] = &[
125
126
  KeywordRow { word: "entity", context: "Entity", body: "keywords", inner: "Entity", opens: "Entity", fills: "", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
126
127
  KeywordRow { word: "on", context: "Policy", body: "none", inner: "", opens: "", fills: "on_event", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
127
128
  KeywordRow { word: "trigger", context: "Policy", body: "none", inner: "", opens: "", fills: "trigger_command", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
129
+ KeywordRow { word: "ask", context: "Policy", body: "none", inner: "", opens: "", fills: "ask", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
128
130
  KeywordRow { word: "across", context: "Policy", body: "none", inner: "", opens: "", fills: "target_domain", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
129
131
  KeywordRow { word: "where", context: "Policy", body: "source", inner: "", opens: "", fills: "where", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
130
132
  KeywordRow { word: "for_each", context: "Policy", body: "none", inner: "", opens: "", fills: "for_each", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
@@ -184,6 +186,7 @@ pub static KEYWORDS: &[KeywordRow] = &[
184
186
  KeywordRow { word: "hecksagon", context: "File", body: "keywords", inner: "Hecksagon", opens: "Hecksagon", fills: "", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
185
187
  KeywordRow { word: "subscribe", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "subscriptions", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
186
188
  KeywordRow { word: "attaches", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "attachments", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
189
+ KeywordRow { word: "driven_by", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "driving", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
187
190
  KeywordRow { word: "port", context: "Hecksagon", body: "keywords", inner: "DomainPort", opens: "DomainPort", fills: "", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
188
191
  KeywordRow { word: "translates", context: "Hecksagon", body: "keywords", inner: "Policy", opens: "Policy", fills: "", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
189
192
  KeywordRow { word: "bounded", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
@@ -263,6 +266,8 @@ pub static ARGUMENTS: &[ArgumentRow] = &[
263
266
  ArgumentRow { keyword: "transition", context: "Lifecycle", at: "1", named: "", kind: "pairs", required: "true", fills: "", selects: "", pair_key_fills: "command", pair_value_fills: "to_state", pairs_shape: "fields", status: "admitted", variadic: "", minimum: "" },
264
267
  ArgumentRow { keyword: "transition", context: "Lifecycle", at: "", named: "from", kind: "text", required: "false", fills: "from_state", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
265
268
  ArgumentRow { keyword: "transition", context: "Lifecycle", at: "", named: "from", kind: "list", required: "false", fills: "from_state", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
269
+ ArgumentRow { keyword: "mark", context: "Lifecycle", at: "1", named: "", kind: "symbol", required: "true", fills: "name", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
270
+ ArgumentRow { keyword: "mark", context: "Lifecycle", at: "2", named: "", kind: "text", required: "true", fills: "state", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "true", minimum: "" },
266
271
  ArgumentRow { keyword: "bluebook", context: "File", at: "1", named: "", kind: "text", required: "true", fills: "name", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
267
272
  ArgumentRow { keyword: "bluebook", context: "File", at: "", named: "version", kind: "text", required: "false", fills: "version", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
268
273
  ArgumentRow { keyword: "vision", context: "Bluebook", at: "1", named: "", kind: "text", required: "true", fills: "vision", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
@@ -282,13 +287,24 @@ pub static ARGUMENTS: &[ArgumentRow] = &[
282
287
  ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "add_name", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
283
288
  ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "confirm", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
284
289
  ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "unsubscribe", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
290
+ ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "awaiting_confirmation", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
291
+ ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "receives_issues", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
292
+ ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "left", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
293
+ ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "confirm_window", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
294
+ ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "unsubscribe_window", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
295
+ ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "webhook_tolerance", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
296
+ ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "session_hold", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
285
297
  ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "initiate", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
286
298
  ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "succeeded", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
287
299
  ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "failed", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
300
+ ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "lapse_reason", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
301
+ ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "holds_seat", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
288
302
  ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "send_issue", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
289
303
  ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "record_delivery", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
290
304
  ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "schedule", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
291
305
  ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "request", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
306
+ ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "registered_at", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
307
+ ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "frees_seat", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
292
308
  ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "connect", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
293
309
  ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "reconnect", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
294
310
  ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "disconnect", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
@@ -296,6 +312,10 @@ pub static ARGUMENTS: &[ArgumentRow] = &[
296
312
  ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "resume", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
297
313
  ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "enable", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
298
314
  ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "disable", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
315
+ ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "mark_sensitive", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
316
+ ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "markings_for", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
317
+ ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "key_for", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
318
+ ArgumentRow { keyword: "provides", context: "Bluebook", at: "", named: "shred", kind: "text", required: "false", fills: "provides", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
299
319
  ArgumentRow { keyword: "aggregate", context: "Bluebook", at: "1", named: "", kind: "text", required: "true", fills: "name", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
300
320
  ArgumentRow { keyword: "read_model", context: "Bluebook", at: "1", named: "", kind: "text", required: "true", fills: "name", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
301
321
  ArgumentRow { keyword: "policy", context: "Bluebook", at: "1", named: "", kind: "text", required: "true", fills: "name", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
@@ -377,10 +397,12 @@ pub static ARGUMENTS: &[ArgumentRow] = &[
377
397
  ArgumentRow { keyword: "on", context: "Policy", at: "1", named: "", kind: "text", required: "true", fills: "on_event", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
378
398
  ArgumentRow { keyword: "trigger", context: "Policy", at: "1", named: "", kind: "constant", required: "true", fills: "trigger_command", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
379
399
  ArgumentRow { keyword: "trigger", context: "Policy", at: "1", named: "", kind: "text", required: "true", fills: "trigger_command", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
400
+ ArgumentRow { keyword: "ask", context: "Policy", at: "1", named: "", kind: "symbol", required: "true", fills: "ask", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
380
401
  ArgumentRow { keyword: "across", context: "Policy", at: "1", named: "", kind: "text", required: "true", fills: "target_domain", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
381
402
  ArgumentRow { keyword: "across", context: "Policy", at: "", named: "expect_undelivered", kind: "flag", required: "false", fills: "expect_undelivered", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
382
403
  ArgumentRow { keyword: "for_each", context: "Policy", at: "1", named: "", kind: "text", required: "true", fills: "for_each", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
383
404
  ArgumentRow { keyword: "trigger", context: "Policy", at: "", named: "with", kind: "pairs", required: "false", fills: "with_spec", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "verbatim", status: "admitted", variadic: "", minimum: "" },
405
+ ArgumentRow { keyword: "ask", context: "Policy", at: "", named: "with", kind: "pairs", required: "false", fills: "with_spec", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "verbatim", status: "admitted", variadic: "", minimum: "" },
384
406
  ArgumentRow { keyword: "correlates_by", context: "ProcessManager", at: "1", named: "", kind: "symbol", required: "true", fills: "correlates_by", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
385
407
  ArgumentRow { keyword: "starts_on", context: "ProcessManager", at: "1", named: "", kind: "constant", required: "true", fills: "starts_on", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
386
408
  ArgumentRow { keyword: "starts_on", context: "ProcessManager", at: "1", named: "", kind: "text", required: "true", fills: "starts_on", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
@@ -468,6 +490,7 @@ pub static ARGUMENTS: &[ArgumentRow] = &[
468
490
  ArgumentRow { keyword: "subscribe", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "subscriptions", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
469
491
  ArgumentRow { keyword: "attaches", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "attachments", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
470
492
  ArgumentRow { keyword: "attaches", context: "Hecksagon", at: "", named: "from", kind: "symbol", required: "false", fills: "source", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
493
+ ArgumentRow { keyword: "driven_by", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "driving", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
471
494
  ArgumentRow { keyword: "port", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "name", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
472
495
  ArgumentRow { keyword: "translates", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "name", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
473
496
  ArgumentRow { keyword: "reference_to", context: "PortOperation", at: "1", named: "", kind: "constant", required: "true", fills: "type", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
@@ -489,6 +512,7 @@ pub static ARGUMENTS: &[ArgumentRow] = &[
489
512
  ArgumentRow { keyword: "adapter", context: "File", at: "1", named: "", kind: "text", required: "true", fills: "name", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
490
513
  ArgumentRow { keyword: "port", context: "Adapter", at: "1", named: "", kind: "text", required: "true", fills: "port", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
491
514
  ArgumentRow { keyword: "field", context: "Adapter", at: "1", named: "", kind: "symbol", required: "true", fills: "fields", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
515
+ ArgumentRow { keyword: "field", context: "Adapter", at: "", named: "default", kind: "literal", required: "false", fills: "default", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
492
516
  ArgumentRow { keyword: "secret", context: "Adapter", at: "1", named: "", kind: "symbol", required: "true", fills: "secrets", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
493
517
  ArgumentRow { keyword: "data_translation", context: "File", at: "1", named: "", kind: "text", required: "true", fills: "name", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
494
518
  ArgumentRow { keyword: "data_translation", context: "File", at: "", named: "from", kind: "text", required: "true", fills: "from", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
@@ -198,6 +198,7 @@ const COVERED_PAIRS: &[(&str, &str)] = &[
198
198
  ("invariant", "ValueObject"),
199
199
  ("member", "OneOf"),
200
200
  ("transition", "Lifecycle"),
201
+ ("mark", "Lifecycle"),
201
202
  ("description", "Query"),
202
203
  ("attribute", "Query"),
203
204
  ("where", "Query"),
@@ -208,6 +209,7 @@ const COVERED_PAIRS: &[(&str, &str)] = &[
208
209
  ("needs", "Query"),
209
210
  ("on", "Policy"),
210
211
  ("trigger", "Policy"),
212
+ ("ask", "Policy"),
211
213
  ("across", "Policy"),
212
214
  ("where", "Policy"),
213
215
  ("for_each", "Policy"),
@@ -220,6 +222,7 @@ const COVERED_PAIRS: &[(&str, &str)] = &[
220
222
  ("translates", "Hecksagon"),
221
223
  ("bounded", "Hecksagon"),
222
224
  ("attaches", "Hecksagon"),
225
+ ("driven_by", "Hecksagon"),
223
226
  ("operation", "DomainPort"),
224
227
  ("reference_to", "PortOperation"),
225
228
  ("attribute", "PortOperation"),
@@ -425,7 +425,7 @@ pub fn parse_body(
425
425
  .chain(closed_sets.iter())
426
426
  .cloned()
427
427
  .collect();
428
- let vo = super::parse_nested_body(
428
+ let (vo, nested_sets) = super::parse_nested_body(
429
429
  file,
430
430
  lines,
431
431
  pos,
@@ -434,6 +434,7 @@ pub fn parse_body(
434
434
  |f, l, p| value_object::parse_body(f, l, p, &vo_name, &owner_value_objects),
435
435
  )?;
436
436
  aggregate.value_objects.push(vo);
437
+ aggregate.value_objects.extend(nested_sets);
437
438
  }
438
439
  }
439
440
  "lifecycle" => {
@@ -297,7 +297,7 @@ pub fn parse_chapter(chapter_name: &str, files: &[(String, String)]) -> ParseRes
297
297
  &mut discarded,
298
298
  &mut Vec::new(),
299
299
  &mut Vec::new(),
300
- true,
300
+ false,
301
301
  )?;
302
302
  }
303
303
  }
@@ -355,7 +355,7 @@ pub fn resolve_hecksagon_dependencies(
355
355
  &mut discarded,
356
356
  &mut Vec::new(),
357
357
  &mut Vec::new(),
358
- true,
358
+ false,
359
359
  )?;
360
360
  }
361
361
  }
@@ -544,8 +544,10 @@ fn build_mutation(
544
544
  }
545
545
  // `state(:field)` is the record's own field, neither argument nor literal; classified
546
546
  // before the Symbol/literal split.
547
+ let computed = if op == "set" { crate::expr::resolver::arithmetic_text(raw.trim()) } else { None };
547
548
  let source = match state_ref(raw.trim()) {
548
549
  Some(name) => ir::MutationSource::State(name),
550
+ None if computed.is_some() => ir::MutationSource::Expression(computed.unwrap_or_default()),
549
551
  None => match value {
550
552
  ruby_value::Value::Symbol(name) => ir::MutationSource::Argument(name),
551
553
  other => ir::MutationSource::Literal(other),
@@ -1,5 +1,6 @@
1
1
  //! The `Hecksagon` construct: a `.hecksagon` file's `port` blocks and `attaches` words.
2
- //! Adapter binds (`persisted_by`, `projected_by`, `subscribe`) are shape-matched and dropped.
2
+ //! Adapter binds (`persisted_by`, `projected_by`, `subscribe`, `driven_by`) are shape-matched and
3
+ //! dropped.
3
4
 
4
5
  use super::domain_port;
5
6
  use super::policy;
@@ -64,7 +65,7 @@ pub fn apply(
64
65
  if let LineShape::Call(call) = lex::classify(file, &line)? {
65
66
  if !matches!(
66
67
  call.word.as_str(),
67
- "port" | "subscribe" | "attaches" | "translates" | "bounded" | "end"
68
+ "port" | "subscribe" | "attaches" | "translates" | "bounded" | "driven_by" | "end"
68
69
  ) {
69
70
  let instead = match call.word.as_str() {
70
71
  "uses_framework" => Some("attaches \"Name\""),
@@ -130,6 +131,8 @@ pub fn apply(
130
131
  }
131
132
  // Consumer-owned bounded-context mark; a wiring fact, dropped like `subscribe`.
132
133
  "bounded" => {}
134
+ // A driving adapter the domain admits; Ruby's boot enforces it, `ir.json` carries none.
135
+ "driven_by" => {}
133
136
  _ => {
134
137
  return Err(super::not_built_yet(
135
138
  "Hecksagon",
@@ -22,11 +22,13 @@ pub fn parse_body(
22
22
  field: field.to_string(),
23
23
  default: default.to_string(),
24
24
  transitions: Vec::new(),
25
+ marks: Vec::new(),
25
26
  };
26
27
 
27
28
  loop {
28
29
  let Some(gated) = super::next_line(file, lines, pos, "Lifecycle")? else {
29
30
  refuse_ambiguity(file, *pos, &lifecycle)?;
31
+ refuse_unknown_mark_states(file, *pos, &lifecycle)?;
30
32
  return Ok(lifecycle);
31
33
  };
32
34
 
@@ -52,6 +54,47 @@ pub fn parse_body(
52
54
  });
53
55
  }
54
56
  }
57
+ "mark" => {
58
+ let line = gated.line.number;
59
+ let name = super::positional_symbol_raw(file, line, "mark", &gated.args, 1)?;
60
+ if !is_lowercase_word(&name) {
61
+ return Err(Diagnostic::new(
62
+ file,
63
+ line,
64
+ format!(
65
+ "lifecycle :{field} mark {:?} is not a lowercase word (such as :holds_seat)",
66
+ format!(":{name}")
67
+ ),
68
+ ));
69
+ }
70
+ if lifecycle.marks.iter().any(|(held, _)| *held == name) {
71
+ return Err(Diagnostic::new(
72
+ file,
73
+ line,
74
+ format!("lifecycle :{field} declares mark :{name} twice"),
75
+ ));
76
+ }
77
+ let mut states: Vec<String> = Vec::new();
78
+ for (_, raw) in gated.args.positional.iter().filter(|(idx, _)| *idx > 1) {
79
+ let state = text_value(raw);
80
+ if states.contains(&state) {
81
+ return Err(Diagnostic::new(
82
+ file,
83
+ line,
84
+ format!("lifecycle :{field} mark :{name} names state {state:?} twice"),
85
+ ));
86
+ }
87
+ states.push(state);
88
+ }
89
+ if states.is_empty() {
90
+ return Err(Diagnostic::new(
91
+ file,
92
+ line,
93
+ format!("lifecycle :{field} mark :{name} names no states"),
94
+ ));
95
+ }
96
+ lifecycle.marks.push((name, states));
97
+ }
55
98
  _ => {
56
99
  return Err(super::not_built_yet(
57
100
  "Lifecycle",
@@ -65,6 +108,44 @@ pub fn parse_body(
65
108
  }
66
109
  }
67
110
 
111
+ // A lowercase word: a letter a-z, then lowercase letters, digits and underscores.
112
+ fn is_lowercase_word(name: &str) -> bool {
113
+ let mut chars = name.chars();
114
+ matches!(chars.next(), Some('a'..='z'))
115
+ && chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
116
+ }
117
+
118
+ // Refuses a mark naming a state that is neither the default nor a transition target. Wording
119
+ // matches Ruby's.
120
+ fn refuse_unknown_mark_states(file: &str, line: usize, lifecycle: &ir::Lifecycle) -> ParseResult<()> {
121
+ let mut known: Vec<&str> = vec![lifecycle.default.as_str()];
122
+ for row in &lifecycle.transitions {
123
+ if !known.contains(&row.to_state.as_str()) {
124
+ known.push(row.to_state.as_str());
125
+ }
126
+ }
127
+ for (name, states) in &lifecycle.marks {
128
+ let unknown: Vec<String> = states
129
+ .iter()
130
+ .filter(|s| !known.contains(&s.as_str()))
131
+ .map(|s| format!("{s:?}"))
132
+ .collect();
133
+ if !unknown.is_empty() {
134
+ return Err(Diagnostic::new(
135
+ file,
136
+ line,
137
+ format!(
138
+ "lifecycle :{} mark :{name} names {}, which is not a state of the lifecycle (states: {})",
139
+ lifecycle.field,
140
+ unknown.join(", "),
141
+ known.join(", ")
142
+ ),
143
+ ));
144
+ }
145
+ }
146
+ Ok(())
147
+ }
148
+
68
149
  // Refuses one command reaching two targets from overlapping `from:` states (`None` overlaps all).
69
150
  // Rows are already expanded one per `from:` state. A `from:` naming an undeclared state is left
70
151
  // to `hecks model_check`, since a bluebook may exhibit it on purpose. Wording matches Ruby's.
@@ -1070,7 +1070,9 @@ pub(crate) fn parse_identified_by(
1070
1070
  "'identified_by' cannot combine a value-object type with a block",
1071
1071
  ));
1072
1072
  }
1073
- let value_object = parse_nested_body(file, lines, pos, opener, line, |f, l, p| {
1073
+ // An identity block's own `one_of(...)` sets are not installed (its builder is not
1074
+ // the aggregate's `value_object` verb).
1075
+ let (value_object, _) = parse_nested_body(file, lines, pos, opener, line, |f, l, p| {
1074
1076
  value_object::parse_body(f, l, p, inline_type_name, owner_value_objects)
1075
1077
  })?;
1076
1078
  if value_object.attributes.is_empty() {
@@ -1090,6 +1092,18 @@ pub(crate) fn parse_identified_by(
1090
1092
  }
1091
1093
  }
1092
1094
 
1095
+ /// The N'th positional as a symbol name (`:holds_seat` gives `holds_seat`).
1096
+ pub(crate) fn positional_symbol_raw(
1097
+ file: &str,
1098
+ line: usize,
1099
+ word: &str,
1100
+ args: &ArgumentGateResult,
1101
+ at: usize,
1102
+ ) -> ParseResult<String> {
1103
+ let raw = positional_raw(file, line, word, args, at)?;
1104
+ positional_symbol_text(file, line, word, raw)
1105
+ }
1106
+
1093
1107
  /// Symbol name for one raw positional token, for variadic callers such as `identified_by`.
1094
1108
  fn positional_symbol_text(file: &str, line: usize, word: &str, raw: &str) -> ParseResult<String> {
1095
1109
  let trimmed = raw.trim();