hecks 3.10.0 → 3.11.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 (481) 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 +155 -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 +69 -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/ruby/aggregate_module.rb +171 -0
  40. data/lib/hecks/adapters/driving/ruby/chapter.rb +91 -0
  41. data/lib/hecks/adapters/driving/ruby.rb +83 -0
  42. data/lib/hecks/adapters/driving/usage_cache.rb +144 -0
  43. data/lib/hecks/{doors.rb → adapters/driving.rb} +10 -6
  44. data/lib/hecks/behaviors/expectations/boot.rb +1 -1
  45. data/lib/hecks/bench/ruby_runner.rb +1 -1
  46. data/lib/hecks/bluebook/ask_resolution.rb +130 -0
  47. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +8 -2
  48. data/lib/hecks/bluebook/assembly/contracts/rules.rb +4 -2
  49. data/lib/hecks/bluebook/assembly/contracts/structure.rb +3 -1
  50. data/lib/hecks/bluebook/assembly/marks.rb +1 -0
  51. data/lib/hecks/bluebook/attribute.rb +1 -1
  52. data/lib/hecks/bluebook/behaviour/command.rb +13 -8
  53. data/lib/hecks/bluebook/behaviour/policy.rb +30 -0
  54. data/lib/hecks/bluebook/capabilities/attribute_values.rb +72 -0
  55. data/lib/hecks/bluebook/capabilities.rb +91 -9
  56. data/lib/hecks/bluebook/domain_port.rb +16 -4
  57. data/lib/hecks/bluebook/dsl/adapter_builder.rb +18 -6
  58. data/lib/hecks/bluebook/dsl/binding_proxy.rb +13 -0
  59. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/computed_sources.rb +98 -0
  60. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/fan_out_rows.rb +42 -0
  61. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/projected_fields.rb +14 -2
  62. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/provisions.rb +69 -3
  63. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/with_projections.rb +17 -10
  64. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +5 -0
  65. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
  66. data/lib/hecks/bluebook/dsl/command_builder/mutations.rb +8 -1
  67. data/lib/hecks/bluebook/dsl/command_builder/operands.rb +121 -0
  68. data/lib/hecks/bluebook/dsl/command_builder.rb +2 -0
  69. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +10 -1
  70. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +45 -2
  71. data/lib/hecks/bluebook/dsl/policy_builder.rb +47 -10
  72. data/lib/hecks/bluebook/dsl/rule_reference.rb +7 -4
  73. data/lib/hecks/bluebook/dsl/world_builder.rb +1 -1
  74. data/lib/hecks/bluebook/expression/ast_json/emitters.rb +6 -0
  75. data/lib/hecks/bluebook/expression/ast_json.rb +9 -6
  76. data/lib/hecks/bluebook/expression/ast_reader.rb +14 -9
  77. data/lib/hecks/bluebook/expression/projection.json +36 -0
  78. data/lib/hecks/bluebook/expression/resolver/operations.rb +31 -0
  79. data/lib/hecks/bluebook/expression/resolver/parsing.rb +38 -5
  80. data/lib/hecks/bluebook/expression/resolver/scanning.rb +22 -5
  81. data/lib/hecks/bluebook/expression/resolver/values.rb +10 -0
  82. data/lib/hecks/bluebook/expression/resolver.rb +20 -1
  83. data/lib/hecks/bluebook/hecksagon.rb +23 -3
  84. data/lib/hecks/bluebook/lifecycle.rb +22 -3
  85. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +13 -1
  86. data/lib/hecks/bluebook/meta_validator/modes.rb +4 -1
  87. data/lib/hecks/bluebook/meta_validator/readings.rb +18 -1
  88. data/lib/hecks/bluebook/meta_validator/reconstruction/heads.rb +2 -1
  89. data/lib/hecks/bluebook/meta_validator/shapes.rb +5 -0
  90. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +1 -1
  91. data/lib/hecks/bluebook/meta_validator.rb +12 -12
  92. data/lib/hecks/bluebook/model_check/ask_checks.rb +71 -0
  93. data/lib/hecks/bluebook/model_check/policy_checks.rb +2 -1
  94. data/lib/hecks/bluebook/model_check/reference_checks.rb +70 -0
  95. data/lib/hecks/bluebook/model_check.rb +8 -2
  96. data/lib/hecks/bluebook/policy.rb +4 -2
  97. data/lib/hecks/bluebook/smoke_test.rb +1 -1
  98. data/lib/hecks/bluebook/synthesizer.rb +2 -0
  99. data/lib/hecks/bluebook.rb +1 -0
  100. data/lib/hecks/cli/command_table.rb +1 -1
  101. data/lib/hecks/cli/console.rb +2 -1
  102. data/lib/hecks/cli/document.rb +1 -1
  103. data/lib/hecks/cli/launcher_words.rb +4 -4
  104. data/lib/hecks/cli/mcp.rb +4 -4
  105. data/lib/hecks/cli/project_cli/launcher_source.rb +2 -2
  106. data/lib/hecks/cli/project_cli/templates.rb +5 -5
  107. data/lib/hecks/cli/project_cli.rb +9 -6
  108. data/lib/hecks/cli/run.rb +2 -2
  109. data/lib/hecks/cli/smoke_http/checks.rb +4 -1
  110. data/lib/hecks/cli/smoke_http/transport.rb +3 -3
  111. data/lib/hecks/cli/smoke_http.rb +3 -0
  112. data/lib/hecks/codemod.rb +5 -2
  113. data/lib/hecks/corpus/routes.rb +7 -0
  114. data/lib/hecks/corpus/rust.rb +3 -2
  115. data/lib/hecks/deploy/bluebook/deploy.bluebook +18 -2
  116. data/lib/hecks/doc/reference/rendering.rb +6 -3
  117. data/lib/hecks/forms/app/commands.rb +8 -7
  118. data/lib/hecks/forms/app/queries.rb +2 -1
  119. data/lib/hecks/forms/app.rb +1 -1
  120. data/lib/hecks/forms/record_renderer.rb +1 -1
  121. data/lib/hecks/framework/bluebook/privacy.bluebook +11 -0
  122. data/lib/hecks/fuzzing/concurrent_dispatch.rb +4 -1
  123. data/lib/hecks/fuzzing/failure_triage.rb +2 -1
  124. data/lib/hecks/fuzzing/mutation/run.rb +11 -4
  125. data/lib/hecks/fuzzing/properties/catalog.rb +6 -4
  126. data/lib/hecks/fuzzing/properties/guaranteed_by_construction.rb +18 -11
  127. data/lib/hecks/fuzzing/properties/guards.rb +1 -1
  128. data/lib/hecks/fuzzing/self_consistency/saga_redelivery.rb +1 -1
  129. data/lib/hecks/fuzzing/self_consistency.rb +6 -2
  130. data/lib/hecks/fuzzing/shrinker.rb +3 -1
  131. data/lib/hecks/fuzzing/value_generator.rb +19 -5
  132. data/lib/hecks/gate/stages.yml +4 -10
  133. data/lib/hecks/grammar/evolve.rb +1 -1
  134. data/lib/hecks/grammar/expression_operators.json +372 -0
  135. data/lib/hecks/grammar/operators.rb +3 -1
  136. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +10 -10
  137. data/lib/hecks/hecks/adapters/codebase/inspection/rubycritic.rb +35 -0
  138. data/lib/hecks/hecks/adapters/codebase/inspection.rb +104 -0
  139. data/lib/hecks/hecks/adapters/codebase/publishing.rb +2 -1
  140. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +12 -2
  141. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +2 -1
  142. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +1 -1
  143. data/lib/hecks/hecks/adapters/cost_explorer.rb +8 -5
  144. data/lib/hecks/hecks/adapters/deploy_toolchain/statuses.rb +2 -1
  145. data/lib/hecks/hecks/adapters/finding_github.rb +13 -6
  146. data/lib/hecks/hecks/adapters/first_admin.rb +2 -2
  147. data/lib/hecks/hecks/adapters/github_rulesets.rb +6 -3
  148. data/lib/hecks/hecks/adapters/in_process_boot.rb +1 -1
  149. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +1 -1
  150. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +10 -20
  151. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +28 -5
  152. data/lib/hecks/hecks/adapters/local_files.rb +2 -2
  153. data/lib/hecks/hecks/adapters/rehearsal/audit_report.rb +107 -0
  154. data/lib/hecks/hecks/adapters/rehearsal/census.rb +71 -0
  155. data/lib/hecks/hecks/adapters/rehearsal/declaration.rb +133 -0
  156. data/lib/hecks/hecks/adapters/rehearsal/environment.rb +83 -0
  157. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +142 -0
  158. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +96 -0
  159. data/lib/hecks/hecks/adapters/rehearsal/postgres_census.rb +100 -0
  160. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +69 -0
  161. data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +129 -0
  162. data/lib/hecks/hecks/adapters/rehearsal/rows.rb +41 -0
  163. data/lib/hecks/hecks/adapters/rehearsal/rust_audit.rb +83 -0
  164. data/lib/hecks/hecks/adapters/rehearsal/subject.rb +104 -0
  165. data/lib/hecks/hecks/adapters/rehearsal.rb +54 -0
  166. data/lib/hecks/hecks/adapters/rehearsal_store.adapter +7 -0
  167. data/lib/hecks/hecks/adapters/rehearsal_store.rb +81 -0
  168. data/lib/hecks/hecks/adapters/terminal.rb +13 -11
  169. data/lib/hecks/hecks/codebase.behaviors +13 -1
  170. data/lib/hecks/hecks/codebase.bluebook +239 -5
  171. data/lib/hecks/hecks/custodian.behaviors +24 -6
  172. data/lib/hecks/hecks/custodian.bluebook +6 -1227
  173. data/lib/hecks/hecks/deploy.behaviors +1 -1
  174. data/lib/hecks/hecks/deploy.world +1 -3
  175. data/lib/hecks/hecks/hecks.behaviors +1 -1
  176. data/lib/hecks/hecks/hecks.hecksagon +53 -18
  177. data/lib/hecks/hecks/hecks.world +10 -10
  178. data/lib/hecks/hecks/rehearsal.bluebook +477 -0
  179. data/lib/hecks/hecks/site.behaviors +1 -1
  180. data/lib/hecks/hecks/site.world +0 -1
  181. data/lib/hecks/hecks/tooling.bluebook +1238 -0
  182. data/lib/hecks/indifferent_key.rb +20 -0
  183. data/lib/hecks/language/adapter.bluebook +13 -3
  184. data/lib/hecks/language/bluebook/aggregate.bluebook +23 -0
  185. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -0
  186. data/lib/hecks/language/bluebook/entity.bluebook +18 -0
  187. data/lib/hecks/language/bluebook/policy.bluebook +9 -1
  188. data/lib/hecks/language/bluebook/query.bluebook +1 -1
  189. data/lib/hecks/language/bluebook/vocabulary.bluebook +11 -4
  190. data/lib/hecks/language/hecksagon/domain_port.bluebook +1 -1
  191. data/lib/hecks/language/hecksagon/hecksagon.bluebook +22 -0
  192. data/lib/hecks/language/oidc.json +10 -0
  193. data/lib/hecks/language/port.bluebook +4 -4
  194. data/lib/hecks/literal.rb +12 -3
  195. data/lib/hecks/ports/key_vault.rb +7 -2
  196. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +21 -10
  197. data/lib/hecks/ports/persistence/plugins/era/postgres_era/boot.rb +4 -2
  198. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +2 -1
  199. data/lib/hecks/ports/persistence/repository_factory.rb +6 -3
  200. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +29 -0
  201. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +1 -1
  202. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  203. data/lib/hecks/projections/deploy/shared.rb +5 -2
  204. data/lib/hecks/projections/model/deviations.rb +2 -2
  205. data/lib/hecks/projections/model.rb +2 -2
  206. data/lib/hecks/projections/oidc.rb +13 -1
  207. data/lib/hecks/projections/shape.rb +5 -2
  208. data/lib/hecks/projections/site/cms_editor/attributes.rb +26 -6
  209. data/lib/hecks/projections/site/cms_editor/blocks.rb +157 -0
  210. data/lib/hecks/projections/site/cms_editor/checks.rb +14 -1
  211. data/lib/hecks/projections/site/cms_editor/clearing.rb +70 -0
  212. data/lib/hecks/projections/site/cms_editor/color.rb +124 -0
  213. data/lib/hecks/projections/site/cms_editor/composition.rb +42 -0
  214. data/lib/hecks/projections/site/cms_editor/destructive.rb +61 -0
  215. data/lib/hecks/projections/site/cms_editor/drafts.rb +102 -0
  216. data/lib/hecks/projections/site/cms_editor/hints.rb +74 -0
  217. data/lib/hecks/projections/site/cms_editor/icons.rb +86 -0
  218. data/lib/hecks/projections/site/cms_editor/key_kinds.rb +31 -0
  219. data/lib/hecks/projections/site/cms_editor/limits.rb +72 -0
  220. data/lib/hecks/projections/site/cms_editor/media.rb +1 -1
  221. data/lib/hecks/projections/site/cms_editor/moment.rb +34 -0
  222. data/lib/hecks/projections/site/cms_editor/palette.rb +115 -0
  223. data/lib/hecks/projections/site/cms_editor/pickers.rb +85 -0
  224. data/lib/hecks/projections/site/cms_editor/preview.rb +91 -0
  225. data/lib/hecks/projections/site/cms_editor/relations.rb +92 -0
  226. data/lib/hecks/projections/site/cms_editor/scale_checks.rb +66 -0
  227. data/lib/hecks/projections/site/cms_editor/scheduling.rb +150 -0
  228. data/lib/hecks/projections/site/cms_editor/schema.rb +96 -19
  229. data/lib/hecks/projections/site/cms_editor/setting.rb +52 -9
  230. data/lib/hecks/projections/site/cms_editor/targets.rb +86 -0
  231. data/lib/hecks/projections/site/cms_editor/templates/package.json.tmpl +18 -1
  232. data/lib/hecks/projections/site/cms_editor/templates/src/app.ts.tmpl +251 -57
  233. data/lib/hecks/projections/site/cms_editor/templates/src/assets.ts.tmpl +98 -0
  234. data/lib/hecks/projections/site/cms_editor/templates/src/auth/membership.ts.tmpl +32 -24
  235. data/lib/hecks/projections/site/cms_editor/templates/src/browser/app.css.tmpl +412 -0
  236. data/lib/hecks/projections/site/cms_editor/templates/src/browser/autosave.js.tmpl +144 -0
  237. data/lib/hecks/projections/site/cms_editor/templates/src/browser/blocks.js.tmpl +500 -0
  238. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_doc.js.tmpl +175 -0
  239. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_editor.js.tmpl +418 -0
  240. data/lib/hecks/projections/site/cms_editor/templates/src/browser/epoch.js.tmpl +61 -0
  241. data/lib/hecks/projections/site/cms_editor/templates/src/browser/forms.js.tmpl +210 -0
  242. data/lib/hecks/projections/site/cms_editor/templates/src/browser/lists.js.tmpl +40 -0
  243. data/lib/hecks/projections/site/cms_editor/templates/src/browser/main.js.tmpl +26 -0
  244. data/lib/hecks/projections/site/cms_editor/templates/src/browser/media_picker.js.tmpl +232 -0
  245. data/lib/hecks/projections/site/cms_editor/templates/src/browser/moments.js.tmpl +73 -0
  246. data/lib/hecks/projections/site/cms_editor/templates/src/browser/pickers.js.tmpl +152 -0
  247. data/lib/hecks/projections/site/cms_editor/templates/src/browser/preview.js.tmpl +98 -0
  248. data/lib/hecks/projections/site/cms_editor/templates/src/browser/shell.js.tmpl +140 -0
  249. data/lib/hecks/projections/site/cms_editor/templates/src/browser/toast.js.tmpl +66 -0
  250. data/lib/hecks/projections/site/cms_editor/templates/src/choices.ts.tmpl +99 -0
  251. data/lib/hecks/projections/site/cms_editor/templates/src/commands.ts.tmpl +78 -24
  252. data/lib/hecks/projections/site/cms_editor/templates/src/config.ts.tmpl +6 -0
  253. data/lib/hecks/projections/site/cms_editor/templates/src/flash.ts.tmpl +52 -0
  254. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +19 -2
  255. data/lib/hecks/projections/site/cms_editor/templates/src/media/handler.ts.tmpl +31 -8
  256. data/lib/hecks/projections/site/cms_editor/templates/src/names.ts.tmpl +36 -0
  257. data/lib/hecks/projections/site/cms_editor/templates/src/schema.ts.tmpl +131 -2
  258. data/lib/hecks/projections/site/cms_editor/templates/src/ui/availability.ts.tmpl +15 -0
  259. data/lib/hecks/projections/site/cms_editor/templates/src/ui/block_list.js.tmpl +213 -0
  260. data/lib/hecks/projections/site/cms_editor/templates/src/ui/blocks.ts.tmpl +275 -0
  261. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_model.js.tmpl +8 -5
  262. data/lib/hecks/projections/site/cms_editor/templates/src/ui/detail.ts.tmpl +88 -0
  263. data/lib/hecks/projections/site/cms_editor/templates/src/ui/dialogs.ts.tmpl +43 -0
  264. data/lib/hecks/projections/site/cms_editor/templates/src/ui/fields.ts.tmpl +229 -34
  265. data/lib/hecks/projections/site/cms_editor/templates/src/ui/html.ts.tmpl +40 -81
  266. data/lib/hecks/projections/site/cms_editor/templates/src/ui/icons.ts.tmpl +15 -0
  267. data/lib/hecks/projections/site/cms_editor/templates/src/ui/input.ts.tmpl +29 -12
  268. data/lib/hecks/projections/site/cms_editor/templates/src/ui/links.ts.tmpl +100 -0
  269. data/lib/hecks/projections/site/cms_editor/templates/src/ui/moments.ts.tmpl +44 -0
  270. data/lib/hecks/projections/site/cms_editor/templates/src/ui/outline.ts.tmpl +53 -17
  271. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pages.ts.tmpl +178 -88
  272. data/lib/hecks/projections/site/cms_editor/templates/src/ui/paging.ts.tmpl +82 -0
  273. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pickers.ts.tmpl +110 -0
  274. data/lib/hecks/projections/site/cms_editor/templates/src/ui/preview.ts.tmpl +87 -0
  275. data/lib/hecks/projections/site/cms_editor/templates/src/ui/related.ts.tmpl +97 -0
  276. data/lib/hecks/projections/site/cms_editor/templates/src/ui/schedule.ts.tmpl +160 -0
  277. data/lib/hecks/projections/site/cms_editor/templates/src/ui/shell.ts.tmpl +211 -0
  278. data/lib/hecks/projections/site/cms_editor/templates/src/ui/table.ts.tmpl +151 -0
  279. data/lib/hecks/projections/site/cms_editor/templates/src/ui/theme.js.tmpl +16 -0
  280. data/lib/hecks/projections/site/cms_editor/templates/src/ui/values.ts.tmpl +69 -0
  281. data/lib/hecks/projections/site/cms_editor/templates/src/ui/words.ts.tmpl +97 -0
  282. data/lib/hecks/projections/site/cms_editor/templates/src/ui/world.ts.tmpl +35 -0
  283. data/lib/hecks/projections/site/cms_editor/templates/tsconfig.json.tmpl +2 -1
  284. data/lib/hecks/projections/site/cms_editor/theme.rb +29 -0
  285. data/lib/hecks/projections/site/cms_editor/theme_checks.rb +57 -0
  286. data/lib/hecks/projections/site/cms_editor.rb +49 -23
  287. data/lib/hecks/projections/site/role_probe.rb +2 -0
  288. data/lib/hecks/projector/cli_audience.rb +2 -1
  289. data/lib/hecks/projector/cli_projector/listing.rb +1 -1
  290. data/lib/hecks/projector/cli_projector/option_specs.rb +1 -1
  291. data/lib/hecks/projector/docs_projector/closing.rb +1 -1
  292. data/lib/hecks/projector/docs_projector.rb +1 -1
  293. data/lib/hecks/projector/exporter/bindings.rb +40 -10
  294. data/lib/hecks/projector/exporter/capability_values.rb +55 -0
  295. data/lib/hecks/projector/narrate_projector/reactions.rb +1 -1
  296. data/lib/hecks/projector/target.rb +11 -0
  297. data/lib/hecks/projector.rb +32 -5
  298. data/lib/hecks/quality_control/adapters/github_checks.rb +12 -8
  299. data/lib/hecks/quality_control/adapters/github_issues.rb +8 -4
  300. data/lib/hecks/quality_control/cli/qa_open_pr.rb +2 -2
  301. data/lib/hecks/quality_control/cli/qa_sweep/checks/shrinking.rb +1 -1
  302. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +1 -1
  303. data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
  304. data/lib/hecks/quality_control/quality_control.bluebook +6 -2
  305. data/lib/hecks/release/approval.rb +64 -0
  306. data/lib/hecks/router/{door.rb → aggregate_reader.rb} +5 -4
  307. data/lib/hecks/router/namespace_installer.rb +16 -16
  308. data/lib/hecks/router.rb +1 -1
  309. data/lib/hecks/runtime/best_effort.rb +22 -0
  310. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -2
  311. data/lib/hecks/runtime/command_interpreter.rb +3 -2
  312. data/lib/hecks/runtime/command_rules/emission.rb +1 -1
  313. data/lib/hecks/runtime/dispatcher.rb +4 -4
  314. data/lib/hecks/runtime/entity_element/location.rb +1 -1
  315. data/lib/hecks/runtime/event.rb +5 -0
  316. data/lib/hecks/runtime/identity.rb +7 -6
  317. data/lib/hecks/runtime/interpreting.rb +2 -1
  318. data/lib/hecks/runtime/invocation_builder.rb +1 -1
  319. data/lib/hecks/runtime/loader/boot_steps.rb +8 -5
  320. data/lib/hecks/runtime/loader.rb +10 -10
  321. data/lib/hecks/runtime/policy_interpreter/arguments.rb +7 -4
  322. data/lib/hecks/runtime/policy_interpreter/fan_out.rb +3 -3
  323. data/lib/hecks/runtime/policy_interpreter.rb +6 -6
  324. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -1
  325. data/lib/hecks/runtime/reaction_invocation.rb +1 -1
  326. data/lib/hecks/runtime/rebuild_sweep.rb +11 -1
  327. data/lib/hecks/runtime/registry/ask_verification.rb +35 -0
  328. data/lib/hecks/runtime/registry/hecksagon_rules.rb +10 -0
  329. data/lib/hecks/runtime/registry/merging.rb +1 -1
  330. data/lib/hecks/runtime/registry/providers.rb +16 -0
  331. data/lib/hecks/runtime/registry/read_repository.rb +6 -4
  332. data/lib/hecks/runtime/registry/verification.rb +4 -1
  333. data/lib/hecks/runtime/registry/world_defaults.rb +29 -0
  334. data/lib/hecks/runtime/registry.rb +1 -1
  335. data/lib/hecks/runtime/saga_interpreter/compensation.rb +1 -1
  336. data/lib/hecks/runtime/saga_interpreter/delivery.rb +4 -4
  337. data/lib/hecks/runtime/saga_interpreter/transitions.rb +1 -1
  338. data/lib/hecks/runtime/saga_interpreter.rb +3 -3
  339. data/lib/hecks/runtime/tenant_check.rb +5 -4
  340. data/lib/hecks/runtime/value/coercion.rb +2 -2
  341. data/lib/hecks/runtime/value/validation.rb +16 -4
  342. data/lib/hecks/runtime.rb +9 -9
  343. data/lib/hecks/rust_build/project_rust/target_ir.rb +1 -1
  344. data/lib/hecks/rust_build/wasm.rb +1 -1
  345. data/lib/hecks/rust_build/wasm_browser.rb +1 -1
  346. data/lib/hecks/rust_build.rb +11 -0
  347. data/lib/hecks/served_options.rb +53 -0
  348. data/lib/hecks/site/bluebook/site.bluebook +1 -0
  349. data/lib/hecks/storehouse/audit.rb +1 -1
  350. data/lib/hecks/storehouse/dispatching.rb +8 -6
  351. data/lib/hecks/storehouse/introspection.rb +2 -2
  352. data/lib/hecks/storehouse/querying.rb +6 -4
  353. data/lib/hecks/storehouse.rb +4 -4
  354. data/lib/hecks/tools/comment_style/source_file.rb +4 -1
  355. data/lib/hecks/tools/deploy_recipe_lint/fixtures.rb +4 -1
  356. data/lib/hecks/tools/lanes/rows.rb +6 -0
  357. data/lib/hecks/tools/oidc_manifests.rb +4 -5
  358. data/lib/hecks/tools/regeneration_run.rb +11 -4
  359. data/lib/hecks/tools/site_routes/editor_files.rb +12 -5
  360. data/lib/hecks/tools/tenant_provisioning.rb +2 -2
  361. data/lib/hecks/tools/tools_doc.rb +4 -4
  362. data/lib/hecks/version.rb +1 -1
  363. data/lib/hecks/vocabulary.rb +4 -4
  364. data/lib/hecks.rb +15 -15
  365. data/lib/rubocop/cop/hecks/env_dump.rb +69 -0
  366. data/lib/rubocop/cop/hecks/monotonic_duration.rb +53 -0
  367. data/lib/rubocop/cop/hecks/no_wall_clock_in_runtime.rb +34 -0
  368. data/lib/rubocop/cop/hecks/raw_shell_interpolation.rb +79 -0
  369. data/lib/rubocop/cop/hecks/sleep_in_spec.rb +34 -0
  370. data/lib/rubocop/cop/hecks/swallowed_rescue.rb +37 -0
  371. data/lib/rubocop/cop/hecks/symbol_string_key_mix.rb +50 -0
  372. data/lib/rubocop/cop/hecks/untyped_raise.rb +37 -0
  373. data/prebuilt/05c358d38d661a3774f813d49c66f5b68939af94432d66257a56f49ee2598994.marshal +0 -0
  374. data/prebuilt/26c40c61f09b09b11651c64668eda3ceb180f83cf2a3802d3033d2fd99ac841b.marshal +0 -0
  375. data/prebuilt/verdicts-230ac18c25abfd3d92ede032bee2661a4c2cd2773ab0cf0e2ae52546ba2f7af9.json +1 -0
  376. data/rust/.gitignore +6 -0
  377. data/rust/Cargo.toml +6 -0
  378. data/rust/build/src/binding_pass.rs +103 -0
  379. data/rust/build/src/cargo_sync.rs +6 -0
  380. data/rust/build/src/lineage_pass.rs +3 -3
  381. data/rust/build/src/main.rs +17 -5
  382. data/rust/build/src/pipeline.rs +48 -5
  383. data/rust/build/src/seams_pass.rs +123 -0
  384. data/rust/build/src/translation_pass.rs +167 -0
  385. data/rust/codegen/src/asks.rs +103 -0
  386. data/rust/codegen/src/commands.rs +13 -8
  387. data/rust/codegen/src/constraints.rs +25 -0
  388. data/rust/codegen/src/domain_generator.rs +2 -0
  389. data/rust/codegen/src/expr_emitter.rs +13 -0
  390. data/rust/codegen/src/json_codec.rs +54 -15
  391. data/rust/codegen/src/main.rs +1 -0
  392. data/rust/codegen/src/mutations.rs +128 -3
  393. data/rust/codegen/src/queries.rs +11 -2
  394. data/rust/codegen/src/reactions.rs +1 -1
  395. data/rust/codegen/src/types.rs +166 -8
  396. data/rust/host/HECKS_RELEASE +1 -1
  397. data/rust/host/src/api.rs +4 -4
  398. data/rust/host/src/bin/validate_harness.rs +43 -0
  399. data/rust/host/src/boundary_fuzz/expr_json.rs +2 -2
  400. data/rust/host/src/boundary_fuzz/ir.rs +1 -0
  401. data/rust/host/src/boundary_fuzz/server.rs +1 -1
  402. data/rust/host/src/checkout.rs +35 -19
  403. data/rust/host/src/commerce_ir.rs +408 -2
  404. data/rust/host/src/expr_json.rs +118 -0
  405. data/rust/host/src/mint.rs +1 -1
  406. data/rust/host/src/payments/tests.rs +27 -1
  407. data/rust/host/src/payments.rs +12 -4
  408. data/rust/host/src/query_step.rs +22 -4
  409. data/rust/host/src/reference_validate.rs +653 -108
  410. data/rust/host/src/resend.rs +2 -0
  411. data/rust/host/src/server.rs +19 -6
  412. data/rust/host/src/ui_schema.rs +132 -5
  413. data/rust/host/src/web/newsletter.rs +7 -8
  414. data/rust/host/src/web/newsletter_send.rs +38 -13
  415. data/rust/host/src/web/registrations.rs +229 -26
  416. data/rust/host/src/web.rs +3 -3
  417. data/rust/parser/src/emit.rs +38 -6
  418. data/rust/parser/src/expr/ast_json.rs +7 -1
  419. data/rust/parser/src/expr/resolver.rs +171 -6
  420. data/rust/parser/src/ir.rs +7 -0
  421. data/rust/parser/src/keywords.rs +24 -0
  422. data/rust/parser/src/main.rs +3 -0
  423. data/rust/parser/src/parse/aggregate.rs +2 -1
  424. data/rust/parser/src/parse/chapter.rs +2 -2
  425. data/rust/parser/src/parse/command.rs +2 -0
  426. data/rust/parser/src/parse/hecksagon.rs +5 -2
  427. data/rust/parser/src/parse/lifecycle.rs +81 -0
  428. data/rust/parser/src/parse/mod.rs +15 -1
  429. data/rust/parser/src/parse/policy.rs +13 -2
  430. data/rust/parser/src/parse/value_object.rs +13 -8
  431. data/rust/parser/src/ruby_value.rs +1 -1
  432. data/rust/src/exemplar/commands.rs +4 -4
  433. data/rust/src/exemplar/constraints.rs +1 -1
  434. data/rust/src/exemplar/json.rs +43 -1
  435. data/rust/src/kernel/cli.rs +7 -5
  436. data/rust/src/kernel/dispatch.rs +2 -2
  437. data/rust/src/kernel/dispatch_guard_tests.rs +1 -1
  438. data/rust/src/kernel/expr.rs +66 -3
  439. data/rust/src/kernel/expression_operators/arithmetic.rs +130 -5
  440. data/rust/src/kernel/expression_operators/text.rs +26 -2
  441. data/rust/src/kernel/json.rs +15 -0
  442. data/rust/src/kernel/mod.rs +1 -1
  443. data/rust/src/kernel/orchestrate.rs +54 -5
  444. data/rust/src/kernel/orchestrate_guard_tests.rs +67 -0
  445. data/rust/src/kernel/pattern.rs +60 -0
  446. data/rust/src/kernel/reference_lookup.rs +90 -7
  447. metadata +140 -38
  448. data/lib/hecks/doors/cli_door/nesting.rb +0 -34
  449. data/lib/hecks/doors/cli_door/shorthand.rb +0 -85
  450. data/lib/hecks/doors/cli_door.rb +0 -124
  451. data/lib/hecks/doors/cli_runner/answers.rb +0 -138
  452. data/lib/hecks/doors/cli_runner/resolution.rb +0 -146
  453. data/lib/hecks/doors/cli_runner/settling.rb +0 -115
  454. data/lib/hecks/doors/cli_runner/streaming.rb +0 -90
  455. data/lib/hecks/doors/cli_runner/suggestions.rb +0 -43
  456. data/lib/hecks/doors/cli_runner.rb +0 -66
  457. data/lib/hecks/doors/command_request.rb +0 -119
  458. data/lib/hecks/doors/handle.rb +0 -208
  459. data/lib/hecks/doors/json_door.rb +0 -132
  460. data/lib/hecks/doors/launcher_options.rb +0 -247
  461. data/lib/hecks/doors/mcp_door/properties.rb +0 -47
  462. data/lib/hecks/doors/mcp_door/tool_calls.rb +0 -113
  463. data/lib/hecks/doors/mcp_door/tool_definitions/auditing.rb +0 -51
  464. data/lib/hecks/doors/mcp_door/tool_definitions/calling.rb +0 -69
  465. data/lib/hecks/doors/mcp_door/tool_definitions/reading.rb +0 -97
  466. data/lib/hecks/doors/mcp_door/tool_definitions.rb +0 -15
  467. data/lib/hecks/doors/mcp_door.rb +0 -156
  468. data/lib/hecks/doors/mcp_door_scope/argument_policy.rb +0 -92
  469. data/lib/hecks/doors/mcp_door_scope/env_problems.rb +0 -55
  470. data/lib/hecks/doors/mcp_door_scope/presenter.rb +0 -78
  471. data/lib/hecks/doors/mcp_door_scope.rb +0 -201
  472. data/lib/hecks/doors/mcp_guide_cache.rb +0 -75
  473. data/lib/hecks/doors/ruby_door/aggregate_door.rb +0 -162
  474. data/lib/hecks/doors/ruby_door/chapter.rb +0 -89
  475. data/lib/hecks/doors/ruby_door.rb +0 -80
  476. data/lib/hecks/doors/usage_cache.rb +0 -139
  477. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_widget.js.tmpl +0 -282
  478. data/lib/hecks/projections/site/cms_editor/templates/src/ui/media_picker.js.tmpl +0 -109
  479. data/prebuilt/c28d6d9db14eea3e17161e9d9c262efee30b256cf97693a7cfd552bce24b2f7f.marshal +0 -0
  480. data/prebuilt/cb375cba61ca0dbe8ddb9739c306510fe05e5f4b7a9b6955caff8224c63ea0e0.marshal +0 -0
  481. data/prebuilt/verdicts-d6d584ad43b5f3cebf88aafbbdbf024c38574acf18e06adffbc555f7786b1c2f.json +0 -1
@@ -0,0 +1,103 @@
1
+ //! Resolution of a policy's `ask :name` to the port operation the hecksagon declared for it,
2
+ //! mirroring `Hecks::Bluebook::AskResolution`: by the event's aggregate and the ask's name, with
3
+ //! `chosen` (`ask_via`) breaking a tie between ports.
4
+
5
+ use crate::json::Json;
6
+ use crate::naming;
7
+
8
+ /// Policies whose `ask` is replaced by the `Aggregate::Port.Operation` trigger it resolves to,
9
+ /// so every table below reads the verb as it reads a port-naming `trigger`.
10
+ ///
11
+ /// Panics when an ask resolves to nothing or to several operations: the Ruby boot refuses both
12
+ /// before the IR is ever exported.
13
+ pub fn resolve_asks(policies: &[Json], aggregates: &[Json]) -> Vec<Json> {
14
+ policies
15
+ .iter()
16
+ .map(|policy| match policy.get("ask").map(Json::to_s) {
17
+ Some(ask) if !ask.is_empty() => with_trigger(policy, resolve(policy, &ask, aggregates)),
18
+ _ => policy.clone(),
19
+ })
20
+ .collect()
21
+ }
22
+
23
+ fn with_trigger(policy: &Json, verb: String) -> Json {
24
+ let mut resolved = policy.clone();
25
+ resolved.set("trigger_command", Json::String(verb));
26
+ resolved
27
+ }
28
+
29
+ fn resolve(policy: &Json, ask: &str, aggregates: &[Json]) -> String {
30
+ let name = policy.get("name").map(Json::to_s).unwrap_or_default();
31
+ let on_event = policy.get("on_event").map(Json::to_s).unwrap_or_default();
32
+ let aggregate = event_aggregate(&on_event, aggregates)
33
+ .unwrap_or_else(|| panic!("policy {name}: ask :{ask} reacts to {on_event:?}, whose aggregate is not declared"));
34
+ let agg_name = aggregate.get("name").map(Json::to_s).unwrap_or_default();
35
+ let mut found = candidates(aggregate, ask);
36
+ if found.len() > 1 && found.iter().any(|(_, op)| op.get("chosen").is_some_and(Json::as_bool)) {
37
+ found.retain(|(_, op)| op.get("chosen").is_some_and(Json::as_bool));
38
+ }
39
+ match found.as_slice() {
40
+ [(port, op)] => format!("{agg_name}::{port}.{}", op.get("name").map(Json::to_s).unwrap_or_default()),
41
+ [] => panic!("policy {name}: ask :{ask} matches no `asks` declared on {agg_name}"),
42
+ _ => panic!("policy {name}: ask :{ask} is asked on several ports of {agg_name}; pick one with ask_via"),
43
+ }
44
+ }
45
+
46
+ // The aggregate the event is qualified with, or else the one whose commands emit the bare event.
47
+ fn event_aggregate<'a>(on_event: &str, aggregates: &'a [Json]) -> Option<&'a Json> {
48
+ let named = |wanted: &str| aggregates.iter().find(|a| a.get("name").map(Json::to_s).unwrap_or_default() == wanted);
49
+ match on_event.split_once('.') {
50
+ Some((qualifier, _)) => named(qualifier.rsplit("::").next().unwrap_or(qualifier)),
51
+ None => aggregates.iter().find(|a| {
52
+ a.get("commands")
53
+ .map(Json::each)
54
+ .unwrap_or(&[])
55
+ .iter()
56
+ .any(|c| c.get("emits").map(Json::to_s).unwrap_or_default() == on_event)
57
+ }),
58
+ }
59
+ }
60
+
61
+ // Every outbound operation of the aggregate's ports whose snake-cased name is the ask.
62
+ fn candidates<'a>(aggregate: &'a Json, ask: &str) -> Vec<(String, &'a Json)> {
63
+ let mut found = Vec::new();
64
+ for port in aggregate.get("ports").map(Json::each).unwrap_or(&[]) {
65
+ let port_name = port.get("name").map(Json::to_s).unwrap_or_default();
66
+ for op in port.get("operations").map(Json::each).unwrap_or(&[]) {
67
+ let outbound = op.get("direction").map(Json::to_s).as_deref() == Some("outbound");
68
+ let spelled = naming::snake(&op.get("name").map(Json::to_s).unwrap_or_default());
69
+ if outbound && spelled == ask {
70
+ found.push((port_name.clone(), op));
71
+ }
72
+ }
73
+ }
74
+ found
75
+ }
76
+
77
+ #[cfg(test)]
78
+ mod tests {
79
+ use super::*;
80
+ use std::path::PathBuf;
81
+
82
+ // The neutral corpus fixture `spec/corpus/asks/errand.json`: the Ruby spec holds the same
83
+ // `targets` against the Ruby runtime's reaction log, so the two cannot drift.
84
+ fn corpus(name: &str) -> Json {
85
+ let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../spec/corpus/asks").join(name);
86
+ Json::parse(&std::fs::read_to_string(path).expect("corpus file")).expect("corpus json")
87
+ }
88
+
89
+ #[test]
90
+ fn every_ask_resolves_to_the_target_the_corpus_freezes() {
91
+ let ir = corpus("errand.ir.json");
92
+ let expect = corpus("errand.json");
93
+ let domain = ir.get("name").map(Json::to_s).unwrap_or_default();
94
+ let resolved = resolve_asks(ir.get("policies").unwrap().each(), ir.get("aggregates").unwrap().each());
95
+
96
+ for policy in &resolved {
97
+ let name = policy.get("name").map(Json::to_s).unwrap_or_default();
98
+ let target = format!("{domain}::{}", policy.get("trigger_command").map(Json::to_s).unwrap_or_default());
99
+ let frozen = expect.get("targets").and_then(|t| t.get(&name)).map(Json::to_s);
100
+ assert_eq!(frozen, Some(target), "{name}");
101
+ }
102
+ }
103
+ }
@@ -168,6 +168,11 @@ fn command_skip_reason_with(command: &Json, aggregate: &Json, value_objects_by_n
168
168
  return Some(skip("state_source", format!("sets state source(s): {}", state_problems.join("; "))));
169
169
  }
170
170
 
171
+ let expression_problems = mutations::expression_source_problems(command, aggregate, value_objects_by_name);
172
+ if !expression_problems.is_empty() {
173
+ return Some(skip("expression_source", format!("sets expression source(s): {}", expression_problems.join("; "))));
174
+ }
175
+
171
176
  let remove_problems = mutations::remove_field_problems(command, aggregate, value_objects_by_name);
172
177
  if !remove_problems.is_empty() {
173
178
  return Some(skip("remove_field", format!("sets remove field(s): {}", remove_problems.join("; "))));
@@ -678,37 +683,37 @@ pub fn delegate_skip_reason(command: &Json, aggregate: &Json, value_objects_by_n
678
683
  }
679
684
  let mapping = delegate_mapping(delegation);
680
685
  let source_name_for = |name: &str| mapping.iter().find(|(t, _)| t == name).map(|(_, s)| s.clone()).unwrap_or_else(|| name.to_string());
681
- let door_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
686
+ let argument_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
682
687
  for attr in target.get("attributes").map(Json::each).unwrap_or(&[]) {
683
688
  let name = crate::attr::name(attr);
684
689
  let source_name = source_name_for(name);
685
- let source = door_attrs.iter().find(|a| crate::attr::name(a) == source_name);
690
+ let source = argument_attrs.iter().find(|a| crate::attr::name(a) == source_name);
686
691
  let Some(source) = source else {
687
692
  if crate::attr::optional(attr) {
688
693
  continue;
689
694
  }
690
- return Some(format!("{label}: target argument {name} has no source on the door"));
695
+ return Some(format!("{label}: target argument {name} has no source on the entry point"));
691
696
  };
692
697
  if crate::attr::list(source) != crate::attr::list(attr) {
693
- return Some(format!("{label}: door argument {source_name} is a list, target wants a scalar (or vice versa) — not generated yet"));
698
+ return Some(format!("{label}: entry point argument {source_name} is a list, target wants a scalar (or vice versa) — not generated yet"));
694
699
  }
695
700
  // Bridgeable, not exact type-name equality: the alias-then-deserialize delegate never
696
701
  // compares declared type names (docs/decisions/0045).
697
702
  if !crate::bridging::bridgeable_value_types(crate::attr::type_name(source), crate::attr::type_name(attr), value_objects_by_name) {
698
- return Some(format!("{label}: door argument {source_name} is {}, target wants {} — not generated yet", crate::attr::type_name(source), crate::attr::type_name(attr)));
703
+ return Some(format!("{label}: entry point argument {source_name} is {}, target wants {} — not generated yet", crate::attr::type_name(source), crate::attr::type_name(attr)));
699
704
  }
700
705
  if crate::attr::optional(source) && !crate::attr::optional(attr) {
701
- return Some(format!("{label}: optional door argument {source_name} feeds required {name}"));
706
+ return Some(format!("{label}: optional entry point argument {source_name} feeds required {name}"));
702
707
  }
703
708
  }
704
709
  for path in entity.get("identified_by").map(Json::each).unwrap_or(&[]) {
705
710
  let path = path.to_s();
706
711
  let head = path.split('.').next().unwrap_or("").to_string();
707
712
  let source_name = source_name_for(&head);
708
- if door_attrs.iter().any(|a| crate::attr::name(a) == source_name && !crate::attr::optional(a)) {
713
+ if argument_attrs.iter().any(|a| crate::attr::name(a) == source_name && !crate::attr::optional(a)) {
709
714
  continue;
710
715
  }
711
- return Some(format!("{label}: the element's identity {head} has no source on the door"));
716
+ return Some(format!("{label}: the element's identity {head} has no source on the entry point"));
712
717
  }
713
718
  None
714
719
  }
@@ -107,3 +107,28 @@ pub fn emit_pattern_check(exemplar: &Exemplar, value_expr: &str, attr: &Json, ow
107
107
  );
108
108
  Some(wrap_if_optional(check, optional_source))
109
109
  }
110
+
111
+ /// A `pattern:` on a list of Strings, checked once per member: Ruby's `check_patterns` walks
112
+ /// `offered_items`, so every element of the list must match, and an optional list left unset has
113
+ /// none to check. Lists of anything but plain Strings get no check.
114
+ pub fn emit_list_pattern_check(exemplar: &Exemplar, value_expr: &str, attr: &Json, owner_type_name: &str) -> Option<String> {
115
+ let pattern = crate::attr::pattern(attr)?;
116
+ if !crate::attr::list(attr) || naming::effective_scalar_type(crate::attr::type_name(attr)) != Some("String") {
117
+ return None;
118
+ }
119
+
120
+ let check = exemplar.render(
121
+ "pattern_check",
122
+ &[
123
+ ("\"tmpl_pattern_text\"", naming::ruby_inspect_string(pattern)),
124
+ ("tmpl_scalar", "item".to_string()),
125
+ ("\"tmpl_pattern_owner\"", naming::ruby_inspect_string(owner_type_name)),
126
+ ("\"tmpl_pattern_field\"", naming::ruby_inspect_string(crate::attr::name(attr))),
127
+ ],
128
+ );
129
+ Some(if crate::attr::optional(attr) {
130
+ format!("if let Some(items) = &{value_expr} {{ for item in items {{ {check} }} }}")
131
+ } else {
132
+ format!("for item in &{value_expr} {{ {check} }}")
133
+ })
134
+ }
@@ -289,6 +289,8 @@ pub fn generate(
289
289
  source_label: &str,
290
290
  mod_name: &str,
291
291
  ) -> GeneratedDomain {
292
+ let resolved_ir = types::with_resolved_projection_types(ir);
293
+ let ir = &resolved_ir;
292
294
  let domain_name = ir.get("name").and_then(Json::as_str).unwrap_or("");
293
295
  let all_aggregates = ir.get("aggregates").map(Json::each).unwrap_or(&[]);
294
296
  let aggregates_by_name: HashMap<String, &Json> = all_aggregates
@@ -23,6 +23,9 @@ pub fn emit_ast(node: &Json) -> String {
23
23
  "nil" => "Expr::Nil".to_string(),
24
24
  "lookup" => format!("Expr::Lookup({})", ruby_inspect_string(&path_of(node, op).join("."))),
25
25
  "add" => format!("Expr::Add({}, {})", boxed("left"), boxed("right")),
26
+ "sub" => format!("Expr::Sub({}, {})", boxed("left"), boxed("right")),
27
+ "mul" => format!("Expr::Mul({}, {})", boxed("left"), boxed("right")),
28
+ "div" => format!("Expr::Div({}, {})", boxed("left"), boxed("right")),
26
29
  "sign_test" => format!("Expr::SignTest {{ op: {}, receiver: {} }}", emit_comparison(field(node, op, "cmp")), boxed("receiver")),
27
30
  "empty" => format!("Expr::Empty({})", boxed("receiver")),
28
31
  "to_s" => format!("Expr::ToS({})", boxed("receiver")),
@@ -50,6 +53,7 @@ pub fn emit_ast(node: &Json) -> String {
50
53
  "presence" => format!("Expr::Presence {{ receiver: {}, negated: {} }}", boxed("receiver"), field(node, op, "negated").to_s()),
51
54
  "assignment" => format!("Expr::Assignment {{ receiver: {}, negated: {} }}", boxed("receiver"), field(node, op, "negated").to_s()),
52
55
  "split" => format!("Expr::Split {{ receiver: {}, separator: {}.to_string() }}", boxed("receiver"), text("separator")),
56
+ "strip" => format!("Expr::Strip {{ receiver: {}, side: crate::kernel::StripSide::{} }}", boxed("receiver"), strip_side(node, op)),
53
57
  "starts_with" => format!("Expr::StartsWith {{ receiver: {}, substring: {}.to_string() }}", boxed("receiver"), text("substring")),
54
58
  "ends_with" => format!("Expr::EndsWith {{ receiver: {}, substring: {}.to_string() }}", boxed("receiver"), text("substring")),
55
59
  "first" => format!("Expr::First({})", boxed("receiver")),
@@ -77,6 +81,15 @@ fn path_of(node: &Json, op: &str) -> Vec<String> {
77
81
  .collect()
78
82
  }
79
83
 
84
+ fn strip_side(node: &Json, op: &str) -> &'static str {
85
+ match field(node, op, "side").as_str() {
86
+ Some("both") => "Both",
87
+ Some("left") => "Left",
88
+ Some("right") => "Right",
89
+ other => panic!("strip's side {other:?} is none of both/left/right: {node:?}"),
90
+ }
91
+ }
92
+
80
93
  fn block_mode(node: &Json, op: &str) -> &'static str {
81
94
  match field(node, op, "mode").as_str() {
82
95
  Some("all") => "All",
@@ -17,20 +17,22 @@ pub fn list_shape_error(struct_name: &str, key: &str, element_type: &str, value_
17
17
  format!("crate::kernel::Refusal::TypeMismatch(format!({}, {value_var}.inspect()))", naming::ruby_inspect_string(&template))
18
18
  }
19
19
 
20
- // A String field refuses only composite-shaped (or null) values, as Ruby's
21
- // `Value::Coercion#check_scalar_shapes` does; the offered shape is only known at runtime.
20
+ // A String field refuses any value that is not a String, and an Integer or Float field any that
21
+ // is not a number, as Ruby's `Value::Coercion#check_scalar_shapes` and `check_numeric_item!`
22
+ // do; the offered shape is only known at runtime, so the message renders it. A whole number
23
+ // beyond signed 64 bits words as Ruby's `integer_range` site.
22
24
  pub fn scalar_type_error(struct_name: &str, key: &str, scalar_type: &str, value_var: &str) -> String {
23
25
  if scalar_type != "Integer" && scalar_type != "Float" && scalar_type != "String" {
24
26
  return json_type_error(struct_name, key, scalar_type);
25
27
  }
26
28
  let template = format!("{struct_name}.{key} expects {scalar_type}, got {{}}");
27
29
  let proper = format!("crate::kernel::Refusal::TypeMismatch(format!({}, {value_var}.inspect()))", naming::ruby_inspect_string(&template));
28
- if scalar_type != "String" {
30
+ if scalar_type != "Integer" {
29
31
  return proper;
30
32
  }
31
- let generic = json_type_error(struct_name, key, scalar_type);
32
- // `Json::Null` words like a composite: Ruby's `check_required_fields` says "got nil".
33
- format!("if matches!({value_var}, crate::kernel::Json::Array(_) | crate::kernel::Json::Object(_) | crate::kernel::Json::Null) {{ {proper} }} else {{ {generic} }}")
33
+ let range_template = format!("{struct_name}.{key} must fit in a 64-bit integer, got {{}}");
34
+ let range = format!("crate::kernel::Refusal::TypeMismatch(format!({}, {value_var}.inspect()))", naming::ruby_inspect_string(&range_template));
35
+ format!("if matches!({value_var}, crate::kernel::Json::Num(n, _) if n.is_finite() && n.fract() == 0.0 && n.abs() >= 9.223372036854775808e18) {{ {range} }} else {{ {proper} }}")
34
36
  }
35
37
 
36
38
  /// A missing non-optional field refuses as "{type}.{field} expects {expected}, got nil".
@@ -274,6 +276,9 @@ pub fn emit_from_json_flat(
274
276
  aggregates_by_name: Option<&HashMap<String, &Json>>,
275
277
  ) -> String {
276
278
  let command_name = command_name.unwrap_or(struct_name);
279
+ // A refusal names the command (`Write.slugs expects ...`), as Ruby's `Value.for_attribute`
280
+ // does, not the generated `WriteArgs` struct that holds its arguments.
281
+ let error_name = if struct_name == format!("{command_name}Args") { command_name } else { struct_name };
277
282
  let idents: Vec<String> = attributes.iter().map(|attr| naming::rust_ident_field(crate::attr::name(attr))).collect();
278
283
  let field_exprs: Vec<String> = attributes
279
284
  .iter()
@@ -285,33 +290,33 @@ pub fn emit_from_json_flat(
285
290
  let optional = crate::attr::optional(attr);
286
291
 
287
292
  let rhs = if list && optional {
288
- let mapper = list_element_from_json_mapper(struct_name, &key, attr, value_objects_by_name);
289
- let array_error = json_type_error(struct_name, &key, "an array");
293
+ let mapper = list_element_from_json_mapper(error_name, &key, attr, value_objects_by_name);
294
+ let array_error = json_type_error(error_name, &key, "an array");
290
295
  format!(
291
296
  "match v.get({}) {{ Some(crate::kernel::Json::Null) | None => None, Some(x) => Some(x.as_array().ok_or_else(|| {array_error})?.iter().map({mapper}).collect::<Result<Vec<_>, crate::kernel::Refusal>>()?) }}",
292
297
  naming::ruby_inspect_string(&key)
293
298
  )
294
299
  } else if list {
295
- let mapper = list_element_from_json_mapper(struct_name, &key, attr, value_objects_by_name);
300
+ let mapper = list_element_from_json_mapper(error_name, &key, attr, value_objects_by_name);
296
301
  // A list argument is an array: a lone scalar is refused, as Ruby's
297
302
  // `Value.refuse_scalar_list` does; null and an absent key are the empty list.
298
- let shape_error = list_shape_error(struct_name, &key, crate::attr::type_name(attr), "x");
303
+ let shape_error = list_shape_error(error_name, &key, crate::attr::type_name(attr), "x");
299
304
  format!(
300
305
  "match v.get({}) {{ Some(crate::kernel::Json::Null) | None => Vec::new(), Some(x) => x.as_array().ok_or_else(|| {shape_error})?.iter().map({mapper}).collect::<Result<Vec<_>, crate::kernel::Refusal>>()?, }}",
301
306
  naming::ruby_inspect_string(&key)
302
307
  )
303
308
  } else if optional && scalar.is_some() {
304
309
  // An optional argument offered as null is the same absence as an omitted key.
305
- format!("match v.get({}) {{ Some(crate::kernel::Json::Null) | None => None, Some(x) => Some({}) }}", naming::ruby_inspect_string(&key), scalar_from_json_value_expr(struct_name, &key, scalar.unwrap(), "x"))
310
+ format!("match v.get({}) {{ Some(crate::kernel::Json::Null) | None => None, Some(x) => Some({}) }}", naming::ruby_inspect_string(&key), scalar_from_json_value_expr(error_name, &key, scalar.unwrap(), "x"))
306
311
  } else if optional {
307
312
  format!("match v.get({}) {{ Some(crate::kernel::Json::Null) | None => None, Some(x) => Some({}) }}", naming::ruby_inspect_string(&key), composite_from_json_expr(attr, value_objects_by_name, "x"))
308
313
  } else if let Some(scalar) = scalar {
309
- scalar_from_json_expr(struct_name, &key, scalar, crate::attr::default(attr))
314
+ scalar_from_json_expr(error_name, &key, scalar, crate::attr::default(attr))
310
315
  } else if absent_argument_check {
311
- // The argument door only; see `required_composite_argument_expr`.
312
- required_composite_argument_expr(struct_name, &key, attr, value_objects_by_name)
316
+ // The argument entry point only; see `required_composite_argument_expr`.
317
+ required_composite_argument_expr(error_name, &key, attr, value_objects_by_name)
313
318
  } else {
314
- composite_from_json_expr(attr, value_objects_by_name, &required_field_expr(struct_name, &key, crate::attr::type_name(attr)))
319
+ composite_from_json_expr(attr, value_objects_by_name, &required_field_expr(error_name, &key, crate::attr::type_name(attr)))
315
320
  };
316
321
 
317
322
  if interleave_checks {
@@ -588,6 +593,35 @@ pub fn emit_closed_set_table_codec(exemplar: &Exemplar, vo: &Json) -> String {
588
593
  })
589
594
  .collect();
590
595
 
596
+ let type_name = vo.get("name").and_then(Json::as_str).unwrap_or("").to_string();
597
+ let field_names: Vec<String> = attributes.iter().map(|attr| naming::ruby_inspect_string(&naming::rust_field(crate::attr::name(attr)))).collect();
598
+ // A field left out (or null) refuses as "{type}.{field} expects {expected}, got nil", before
599
+ // membership is judged.
600
+ let absent_checks: Vec<String> = attributes
601
+ .iter()
602
+ .map(|attr| {
603
+ let key = naming::rust_field(crate::attr::name(attr));
604
+ let message = format!("{type_name}.{} expects {}, got nil", crate::attr::name(attr), crate::attr::type_name(attr));
605
+ format!(
606
+ " if matches!(v.get({}), None | Some(crate::kernel::Json::Null)) {{\n return Err(crate::kernel::Refusal::TypeMismatch({}.to_string()));\n }}",
607
+ naming::ruby_inspect_string(&key),
608
+ naming::ruby_inspect_string(&message)
609
+ )
610
+ })
611
+ .collect();
612
+ // The refusal quotes the first column of every row, as Ruby's `refuse_non_member!` does.
613
+ let admitted: Vec<String> = vo
614
+ .get("members")
615
+ .map(Json::each)
616
+ .unwrap_or(&[])
617
+ .iter()
618
+ .map(|row| {
619
+ let first = row.as_array().unwrap_or(&[]).first().and_then(Json::as_array).unwrap_or(&[]);
620
+ naming::ruby_inspect_string(&first.get(1).map(Json::to_s).unwrap_or_default())
621
+ })
622
+ .collect();
623
+ let first_field = attributes.first().map(|attr| naming::rust_field(crate::attr::name(attr))).unwrap_or_default();
624
+
591
625
  exemplar.render(
592
626
  "closed_set_table_codec",
593
627
  &[
@@ -595,6 +629,11 @@ pub fn emit_closed_set_table_codec(exemplar: &Exemplar, vo: &Json) -> String {
595
629
  ("tmpl_to_json_fields_block()", to_json_fields_block),
596
630
  ("TMPL_TABLE", const_name),
597
631
  ("tmpl_from_json_conditions()", match_conditions.join(" && ")),
632
+ ("\"tmpl_table_type\"", naming::ruby_inspect_string(&type_name)),
633
+ ("TMPL_DECLARED_NAMES", format!("[{}]", field_names.join(", "))),
634
+ ("TMPL_ADMITTED", format!("[{}]", admitted.join(", "))),
635
+ ("tmpl_absent_field_checks();", absent_checks.join("\n")),
636
+ ("\"tmpl_first_field\"", naming::ruby_inspect_string(&first_field)),
598
637
  ],
599
638
  )
600
639
  }
@@ -3,6 +3,7 @@
3
3
  // Some ported `naming.rs` helpers are not called yet.
4
4
  #![allow(dead_code)]
5
5
 
6
+ mod asks;
6
7
  mod attr;
7
8
  mod bridging;
8
9
  mod commands;
@@ -176,10 +176,11 @@ pub fn append_field_source(source: &str) -> Literal {
176
176
  literal::read(source)
177
177
  }
178
178
 
179
- /// Whether any effect reads the record's own state, so the closure must bind `let pre`.
179
+ /// Whether any effect reads the record's own state (a `state(:field)` source, or an expression that
180
+ /// may name a field), so the closure must bind `let pre`.
180
181
  pub fn reads_pre_state(mutations: &[Json]) -> bool {
181
182
  mutations.iter().any(|m| match m.get("op").map(Json::to_s).unwrap_or_default().as_str() {
182
- "set" => m.get("source").and_then(|s| s.get("kind")).map(Json::to_s).unwrap_or_default() == "state",
183
+ "set" => matches!(m.get("source").and_then(|s| s.get("kind")).map(Json::to_s).unwrap_or_default().as_str(), "state" | "expression"),
183
184
  "append" => match m.get("fields") {
184
185
  Some(Json::Object(pairs)) => pairs.iter().any(|(_, source)| source.to_s().starts_with("state(:")),
185
186
  _ => false,
@@ -251,6 +252,66 @@ pub fn state_source_problems(command: &Json, aggregate: &Json, value_objects_by_
251
252
  .collect()
252
253
  }
253
254
 
255
+ /// Problems with `set` mutations sourced from arithmetic: the target must be a scalar
256
+ /// (non-list) attribute typed by a single-field Integer or Float value object, the one shape the
257
+ /// generated code can rebuild a computed number into.
258
+ pub fn expression_source_problems(command: &Json, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Vec<String> {
259
+ let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
260
+ command
261
+ .get("mutations")
262
+ .map(Json::each)
263
+ .unwrap_or(&[])
264
+ .iter()
265
+ .filter(|m| m.get("op").map(Json::to_s).as_deref() == Some("set"))
266
+ .filter(|m| m.get("source").and_then(|s| s.get("kind")).map(Json::to_s).as_deref() == Some("expression"))
267
+ .filter_map(|m| {
268
+ let target = m.get("target").map(Json::to_s).unwrap_or_default();
269
+ let attr = attrs.iter().find(|a| crate::attr::name(a) == target);
270
+ match attr.and_then(|a| computed_field_type(a, value_objects_by_name)) {
271
+ Some(_) => None,
272
+ None => Some(format!("{target}: a computed value needs a scalar attribute typed by a single-field Integer or Float value object — not generated yet")),
273
+ }
274
+ })
275
+ .collect()
276
+ }
277
+
278
+ // The scalar type (`Integer` or `Float`) of the one field of a non-list, single-field value object.
279
+ fn computed_field_type<'a>(attr: &Json, value_objects_by_name: &HashMap<String, &'a Json>) -> Option<(&'a Json, &'static str)> {
280
+ if crate::attr::list(attr) {
281
+ return None;
282
+ }
283
+ let vo = value_objects_by_name.get(crate::attr::type_name(attr)).copied()?;
284
+ let fields = vo.get("attributes").map(Json::each).unwrap_or(&[]);
285
+ if vo.get("closed_set").map(Json::as_bool).unwrap_or(false) || fields.len() != 1 || crate::attr::list(&fields[0]) {
286
+ return None;
287
+ }
288
+ match crate::attr::type_name(&fields[0]) {
289
+ "Integer" => Some((vo, "Integer")),
290
+ "Float" => Some((vo, "Float")),
291
+ _ => None,
292
+ }
293
+ }
294
+
295
+ /// Right-hand side for a `set` sourced from arithmetic: evaluates the expression through the
296
+ /// kernel over the command's arguments and then the pre-dispatch record, and rebuilds the number
297
+ /// into the target's single-field value object. A fault returns from the mutation closure.
298
+ pub fn expression_set_rhs(source: &Json, target_attr: &Json, value_objects_by_name: &HashMap<String, &Json>) -> String {
299
+ let (vo, scalar) = computed_field_type(target_attr, value_objects_by_name).expect("expression_source_problems already confirmed the target");
300
+ let ast = source.get("ast").expect("an expression source carries its ast");
301
+ let field = naming::rust_ident_field(crate::attr::name(&vo.get("attributes").map(Json::each).unwrap_or(&[])[0]));
302
+ let vo_type = naming::rust_ident(crate::attr::type_name(target_attr));
303
+ let arms = if scalar == "Integer" {
304
+ "crate::kernel::Value::Int(number) => number,".to_string()
305
+ } else {
306
+ "crate::kernel::Value::Float(number) => number,\n crate::kernel::Value::Int(number) => number as f64,".to_string()
307
+ };
308
+ let target = crate::attr::name(target_attr);
309
+ format!(
310
+ "{{\n let number = match crate::kernel::interpret(&{}, &crate::kernel::EvalContext {{ args: &args, instance: &pre }})? {{\n {arms}\n other => return Err(crate::kernel::Refusal::TypeMismatch(format!(\"{target} expects a {scalar}, computed {{other:?}}\"))),\n }};\n {vo_type} {{ {field}: number }}\n }}",
311
+ crate::expr_emitter::emit_ast(ast)
312
+ )
313
+ }
314
+
254
315
  fn state_source_problem(label: &str, state_name: &str, aggregate: &Json, target_attr: Option<&Json>) -> Option<String> {
255
316
  let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
256
317
  let Some(state_attr) = attrs.iter().find(|a| crate::attr::name(a) == state_name) else {
@@ -722,8 +783,12 @@ fn emit_mutation_line_body(
722
783
  } else {
723
784
  let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
724
785
  let target_attr = attrs.iter().find(|a| crate::attr::name(a) == target_name).expect("set target must be a declared aggregate attribute");
725
- let rhs = mutation_set_rhs(mutation.get("source").unwrap_or(&Json::Null), crate::attr::type_name(target_attr), command, value_objects_by_name, crate::attr::list(target_attr));
726
786
  let source = mutation.get("source");
787
+ let rhs = if source.and_then(|s| s.get("kind")).map(Json::to_s).as_deref() == Some("expression") {
788
+ expression_set_rhs(source.unwrap(), target_attr, value_objects_by_name)
789
+ } else {
790
+ mutation_set_rhs(mutation.get("source").unwrap_or(&Json::Null), crate::attr::type_name(target_attr), command, value_objects_by_name, crate::attr::list(target_attr))
791
+ };
727
792
  let source_attr = if source.map(|s| s.get("kind").map(Json::to_s).unwrap_or_default()) == Some("argument".to_string()) {
728
793
  let name = source.unwrap().get("name").map(Json::to_s).unwrap_or_default();
729
794
  let cmd_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
@@ -896,3 +961,63 @@ mod state_source_tests {
896
961
  assert!(!state_source_is_option(Some(&argument), &aggregate(), true));
897
962
  }
898
963
  }
964
+
965
+ #[cfg(test)]
966
+ mod expression_source_tests {
967
+ use super::*;
968
+
969
+ fn aggregate() -> Json {
970
+ Json::parse(r#"{"name":"Settlement","attributes":[
971
+ {"name":"paid","type":"Cents","list":false},
972
+ {"name":"refund","type":"Cents","list":false},
973
+ {"name":"note","type":"Note","list":false}]}"#)
974
+ .unwrap()
975
+ }
976
+
977
+ fn vos() -> Vec<Json> {
978
+ vec![
979
+ Json::parse(r#"{"name":"Cents","attributes":[{"name":"value","type":"Integer","list":false}]}"#).unwrap(),
980
+ Json::parse(r#"{"name":"Note","attributes":[{"name":"value","type":"String","list":false}]}"#).unwrap(),
981
+ ]
982
+ }
983
+
984
+ fn command(target: &str) -> Json {
985
+ let source = r#"{"kind":"expression","text":"paid * 2","ast":{"op":"mul","left":{"op":"lookup","path":["paid"]},"right":{"op":"int","value":2}}}"#;
986
+ Json::parse(&format!(r#"{{"mutations":[{{"target":"{target}","op":"set","source":{source}}}]}}"#)).unwrap()
987
+ }
988
+
989
+ fn by_name(vos: &[Json]) -> HashMap<String, &Json> {
990
+ vos.iter().map(|vo| (vo.get("name").map(Json::to_s).unwrap_or_default(), vo)).collect()
991
+ }
992
+
993
+ #[test]
994
+ fn an_expression_source_binds_the_pre_dispatch_record() {
995
+ assert!(reads_pre_state(command("refund").get("mutations").map(Json::each).unwrap_or(&[])));
996
+ }
997
+
998
+ #[test]
999
+ fn a_target_typed_by_an_integer_value_object_is_generated() {
1000
+ let vos = vos();
1001
+
1002
+ assert!(expression_source_problems(&command("refund"), &aggregate(), &by_name(&vos)).is_empty());
1003
+ }
1004
+
1005
+ #[test]
1006
+ fn a_target_typed_by_a_text_value_object_is_not_generated_yet() {
1007
+ let vos = vos();
1008
+
1009
+ assert_eq!(expression_source_problems(&command("note"), &aggregate(), &by_name(&vos)).len(), 1);
1010
+ }
1011
+
1012
+ #[test]
1013
+ fn the_right_hand_side_evaluates_through_the_kernel_and_rebuilds_the_value_object() {
1014
+ let vos = vos();
1015
+ let aggregate = aggregate();
1016
+ let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
1017
+ let source = command("refund").get("mutations").map(Json::each).unwrap_or(&[])[0].get("source").cloned().unwrap();
1018
+
1019
+ let rhs = expression_set_rhs(&source, &attrs[1], &by_name(&vos));
1020
+
1021
+ assert!(rhs.contains("crate::kernel::interpret(&Expr::Mul(") && rhs.contains("Cents { value: number }"));
1022
+ }
1023
+ }
@@ -313,8 +313,17 @@ pub fn query_skip_reason(query: &Json, aggregate: &Json, value_objects_by_name:
313
313
  // field itself is validated afterward by `declared_authorization_skip_reason`.
314
314
  let declared_tenant = query.get("authorization").and_then(|a| a.get("tenant")).is_some();
315
315
  let wheres = query.get("wheres").map(Json::each).unwrap_or(&[]);
316
- if wheres.is_empty() && !declared_tenant {
317
- return Some(skip("no_wheres", "declares no where clauses at all — nothing for filter_entries to bake in"));
316
+ // A query with no `where` answers every record, ordered and bounded as declared, as Ruby's
317
+ // `QueryInterpreter` does. Ruby refuses at boot one that takes arguments yet filters, orders
318
+ // and bounds nothing (`Registry::QueryVerification#derivable?`: it reads none of them), so no
319
+ // answer exists to generate for it.
320
+ let selects_nothing = query.get("order_by").is_none() && query.get("limit").is_none() && query.get("offset").is_none();
321
+ let takes_arguments = query.get("attributes").map(Json::each).is_some_and(|attributes| !attributes.is_empty());
322
+ if wheres.is_empty() && !declared_tenant && selects_nothing && takes_arguments {
323
+ return Some(skip(
324
+ "no_wheres",
325
+ "takes arguments but declares no where, order_by, limit or offset — it selects nothing, so Ruby refuses it at boot and there is no answer to generate",
326
+ ));
318
327
  }
319
328
 
320
329
  for where_clause in wheres {
@@ -60,7 +60,7 @@ fn where_fns(policies: &[Json]) -> Vec<String> {
60
60
  }
61
61
 
62
62
  fn local_policy_rows(domain_name: &str, policies: &[Json], aggregates: &[Json]) -> Vec<String> {
63
- policies
63
+ crate::asks::resolve_asks(policies, aggregates)
64
64
  .iter()
65
65
  .filter_map(|policy| {
66
66
  let target_domain = policy.get("target_domain").map(Json::to_s).unwrap_or_else(|| domain_name.to_string());