hecks 3.9.1 → 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 (504) 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 +124 -2
  116. data/lib/hecks/deploy/bluebook/deploy.hecksagon +8 -0
  117. data/lib/hecks/deploy/oidc.json +16 -3
  118. data/lib/hecks/doc/reference/rendering.rb +6 -3
  119. data/lib/hecks/forms/app/commands.rb +8 -7
  120. data/lib/hecks/forms/app/queries.rb +2 -1
  121. data/lib/hecks/forms/app.rb +1 -1
  122. data/lib/hecks/forms/record_renderer.rb +1 -1
  123. data/lib/hecks/framework/bluebook/privacy.bluebook +11 -0
  124. data/lib/hecks/fuzzing/concurrent_dispatch.rb +4 -1
  125. data/lib/hecks/fuzzing/failure_triage.rb +2 -1
  126. data/lib/hecks/fuzzing/mutation/run.rb +11 -4
  127. data/lib/hecks/fuzzing/properties/catalog.rb +6 -4
  128. data/lib/hecks/fuzzing/properties/guaranteed_by_construction.rb +18 -11
  129. data/lib/hecks/fuzzing/properties/guards.rb +1 -1
  130. data/lib/hecks/fuzzing/self_consistency/saga_redelivery.rb +1 -1
  131. data/lib/hecks/fuzzing/self_consistency.rb +6 -2
  132. data/lib/hecks/fuzzing/shrinker.rb +3 -1
  133. data/lib/hecks/fuzzing/value_generator.rb +19 -5
  134. data/lib/hecks/gate/stages.yml +4 -10
  135. data/lib/hecks/grammar/evolve.rb +1 -1
  136. data/lib/hecks/grammar/expression_operators.json +372 -0
  137. data/lib/hecks/grammar/operators.rb +3 -1
  138. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +10 -10
  139. data/lib/hecks/hecks/adapters/codebase/inspection/rubycritic.rb +35 -0
  140. data/lib/hecks/hecks/adapters/codebase/inspection.rb +104 -0
  141. data/lib/hecks/hecks/adapters/codebase/publishing.rb +2 -1
  142. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +12 -2
  143. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +2 -1
  144. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +1 -1
  145. data/lib/hecks/hecks/adapters/cost_explorer.rb +8 -5
  146. data/lib/hecks/hecks/adapters/deploy_toolchain/clearing.rb +51 -0
  147. data/lib/hecks/hecks/adapters/deploy_toolchain/statuses.rb +2 -1
  148. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +2 -0
  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 +129 -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/site_toolchain/role_prober.rb +78 -0
  173. data/lib/hecks/hecks/adapters/site_toolchain.rb +29 -4
  174. data/lib/hecks/hecks/adapters/terminal.rb +13 -11
  175. data/lib/hecks/hecks/adapters/world_clearing.rb +65 -0
  176. data/lib/hecks/hecks/codebase.behaviors +13 -1
  177. data/lib/hecks/hecks/codebase.bluebook +239 -5
  178. data/lib/hecks/hecks/custodian.behaviors +24 -6
  179. data/lib/hecks/hecks/custodian.bluebook +6 -1227
  180. data/lib/hecks/hecks/deploy.behaviors +1 -1
  181. data/lib/hecks/hecks/deploy.world +1 -3
  182. data/lib/hecks/hecks/hecks.behaviors +1 -1
  183. data/lib/hecks/hecks/hecks.hecksagon +61 -18
  184. data/lib/hecks/hecks/hecks.world +11 -10
  185. data/lib/hecks/hecks/rehearsal.bluebook +477 -0
  186. data/lib/hecks/hecks/site.behaviors +1 -1
  187. data/lib/hecks/hecks/site.world +0 -1
  188. data/lib/hecks/hecks/tooling.bluebook +1238 -0
  189. data/lib/hecks/indifferent_key.rb +20 -0
  190. data/lib/hecks/language/adapter.bluebook +13 -3
  191. data/lib/hecks/language/bluebook/aggregate.bluebook +23 -0
  192. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -0
  193. data/lib/hecks/language/bluebook/entity.bluebook +18 -0
  194. data/lib/hecks/language/bluebook/policy.bluebook +9 -1
  195. data/lib/hecks/language/bluebook/query.bluebook +1 -1
  196. data/lib/hecks/language/bluebook/vocabulary.bluebook +11 -4
  197. data/lib/hecks/language/hecksagon/domain_port.bluebook +1 -1
  198. data/lib/hecks/language/hecksagon/hecksagon.bluebook +22 -0
  199. data/lib/hecks/language/oidc.json +10 -0
  200. data/lib/hecks/language/port.bluebook +4 -4
  201. data/lib/hecks/literal.rb +12 -3
  202. data/lib/hecks/ports/key_vault.rb +7 -2
  203. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +21 -10
  204. data/lib/hecks/ports/persistence/plugins/era/postgres_era/boot.rb +4 -2
  205. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +2 -1
  206. data/lib/hecks/ports/persistence/repository_factory.rb +6 -3
  207. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +29 -0
  208. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +1 -1
  209. data/lib/hecks/projections/deploy/fargate/settings/tuning.rb +15 -0
  210. data/lib/hecks/projections/deploy/fargate/settings.rb +3 -1
  211. data/lib/hecks/projections/deploy/fargate/stack/readers.rb +1 -0
  212. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  213. data/lib/hecks/projections/deploy/shared.rb +5 -2
  214. data/lib/hecks/projections/deploy/templates/fargate/Makefile.tmpl +7 -4
  215. data/lib/hecks/projections/model/deviations.rb +2 -2
  216. data/lib/hecks/projections/model.rb +2 -2
  217. data/lib/hecks/projections/oidc.rb +13 -1
  218. data/lib/hecks/projections/shape.rb +5 -2
  219. data/lib/hecks/projections/site/cms_editor/attributes.rb +95 -0
  220. data/lib/hecks/projections/site/cms_editor/blocks.rb +157 -0
  221. data/lib/hecks/projections/site/cms_editor/checks.rb +77 -0
  222. data/lib/hecks/projections/site/cms_editor/clearing.rb +70 -0
  223. data/lib/hecks/projections/site/cms_editor/color.rb +124 -0
  224. data/lib/hecks/projections/site/cms_editor/composition.rb +42 -0
  225. data/lib/hecks/projections/site/cms_editor/destructive.rb +61 -0
  226. data/lib/hecks/projections/site/cms_editor/drafts.rb +102 -0
  227. data/lib/hecks/projections/site/cms_editor/hints.rb +74 -0
  228. data/lib/hecks/projections/site/cms_editor/icons.rb +86 -0
  229. data/lib/hecks/projections/site/cms_editor/key_kinds.rb +31 -0
  230. data/lib/hecks/projections/site/cms_editor/limits.rb +72 -0
  231. data/lib/hecks/projections/site/cms_editor/media.rb +85 -0
  232. data/lib/hecks/projections/site/cms_editor/moment.rb +34 -0
  233. data/lib/hecks/projections/site/cms_editor/palette.rb +115 -0
  234. data/lib/hecks/projections/site/cms_editor/pickers.rb +85 -0
  235. data/lib/hecks/projections/site/cms_editor/pretty.rb +40 -0
  236. data/lib/hecks/projections/site/cms_editor/preview.rb +91 -0
  237. data/lib/hecks/projections/site/cms_editor/relations.rb +92 -0
  238. data/lib/hecks/projections/site/cms_editor/scale_checks.rb +66 -0
  239. data/lib/hecks/projections/site/cms_editor/scheduling.rb +150 -0
  240. data/lib/hecks/projections/site/cms_editor/schema.rb +161 -0
  241. data/lib/hecks/projections/site/cms_editor/setting.rb +130 -0
  242. data/lib/hecks/projections/site/cms_editor/targets.rb +86 -0
  243. data/lib/hecks/projections/site/cms_editor/templates/package.json.tmpl +36 -0
  244. data/lib/hecks/projections/site/cms_editor/templates/src/app.ts.tmpl +370 -0
  245. data/lib/hecks/projections/site/cms_editor/templates/src/assets.ts.tmpl +98 -0
  246. data/lib/hecks/projections/site/cms_editor/templates/src/auth/membership.ts.tmpl +87 -0
  247. data/lib/hecks/projections/site/cms_editor/templates/src/auth/session.ts.tmpl +36 -0
  248. data/lib/hecks/projections/site/cms_editor/templates/src/auth/sso.ts.tmpl +20 -0
  249. data/lib/hecks/projections/site/cms_editor/templates/src/browser/app.css.tmpl +412 -0
  250. data/lib/hecks/projections/site/cms_editor/templates/src/browser/autosave.js.tmpl +144 -0
  251. data/lib/hecks/projections/site/cms_editor/templates/src/browser/blocks.js.tmpl +500 -0
  252. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_doc.js.tmpl +175 -0
  253. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_editor.js.tmpl +418 -0
  254. data/lib/hecks/projections/site/cms_editor/templates/src/browser/epoch.js.tmpl +61 -0
  255. data/lib/hecks/projections/site/cms_editor/templates/src/browser/forms.js.tmpl +210 -0
  256. data/lib/hecks/projections/site/cms_editor/templates/src/browser/lists.js.tmpl +40 -0
  257. data/lib/hecks/projections/site/cms_editor/templates/src/browser/main.js.tmpl +26 -0
  258. data/lib/hecks/projections/site/cms_editor/templates/src/browser/media_picker.js.tmpl +232 -0
  259. data/lib/hecks/projections/site/cms_editor/templates/src/browser/moments.js.tmpl +73 -0
  260. data/lib/hecks/projections/site/cms_editor/templates/src/browser/pickers.js.tmpl +152 -0
  261. data/lib/hecks/projections/site/cms_editor/templates/src/browser/preview.js.tmpl +98 -0
  262. data/lib/hecks/projections/site/cms_editor/templates/src/browser/shell.js.tmpl +140 -0
  263. data/lib/hecks/projections/site/cms_editor/templates/src/browser/toast.js.tmpl +66 -0
  264. data/lib/hecks/projections/site/cms_editor/templates/src/choices.ts.tmpl +99 -0
  265. data/lib/hecks/projections/site/cms_editor/templates/src/commands.ts.tmpl +133 -0
  266. data/lib/hecks/projections/site/cms_editor/templates/src/config.ts.tmpl +29 -0
  267. data/lib/hecks/projections/site/cms_editor/templates/src/flash.ts.tmpl +52 -0
  268. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +43 -0
  269. data/lib/hecks/projections/site/cms_editor/templates/src/media/handler.ts.tmpl +177 -0
  270. data/lib/hecks/projections/site/cms_editor/templates/src/media/multipart.ts.tmpl +53 -0
  271. data/lib/hecks/projections/site/cms_editor/templates/src/media/sniff.ts.tmpl +94 -0
  272. data/lib/hecks/projections/site/cms_editor/templates/src/media/storage.ts.tmpl +50 -0
  273. data/lib/hecks/projections/site/cms_editor/templates/src/names.ts.tmpl +36 -0
  274. data/lib/hecks/projections/site/cms_editor/templates/src/schema.ts.tmpl +214 -0
  275. data/lib/hecks/projections/site/cms_editor/templates/src/server.ts.tmpl +65 -0
  276. data/lib/hecks/projections/site/cms_editor/templates/src/ui/availability.ts.tmpl +15 -0
  277. data/lib/hecks/projections/site/cms_editor/templates/src/ui/block_list.js.tmpl +213 -0
  278. data/lib/hecks/projections/site/cms_editor/templates/src/ui/blocks.ts.tmpl +275 -0
  279. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_model.js.tmpl +123 -0
  280. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_parse.js.tmpl +320 -0
  281. data/lib/hecks/projections/site/cms_editor/templates/src/ui/detail.ts.tmpl +88 -0
  282. data/lib/hecks/projections/site/cms_editor/templates/src/ui/dialogs.ts.tmpl +43 -0
  283. data/lib/hecks/projections/site/cms_editor/templates/src/ui/fields.ts.tmpl +275 -0
  284. data/lib/hecks/projections/site/cms_editor/templates/src/ui/html.ts.tmpl +61 -0
  285. data/lib/hecks/projections/site/cms_editor/templates/src/ui/icons.ts.tmpl +15 -0
  286. data/lib/hecks/projections/site/cms_editor/templates/src/ui/input.ts.tmpl +92 -0
  287. data/lib/hecks/projections/site/cms_editor/templates/src/ui/links.ts.tmpl +100 -0
  288. data/lib/hecks/projections/site/cms_editor/templates/src/ui/moments.ts.tmpl +44 -0
  289. data/lib/hecks/projections/site/cms_editor/templates/src/ui/outline.ts.tmpl +63 -0
  290. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pages.ts.tmpl +213 -0
  291. data/lib/hecks/projections/site/cms_editor/templates/src/ui/paging.ts.tmpl +82 -0
  292. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pickers.ts.tmpl +110 -0
  293. data/lib/hecks/projections/site/cms_editor/templates/src/ui/preview.ts.tmpl +87 -0
  294. data/lib/hecks/projections/site/cms_editor/templates/src/ui/related.ts.tmpl +97 -0
  295. data/lib/hecks/projections/site/cms_editor/templates/src/ui/schedule.ts.tmpl +160 -0
  296. data/lib/hecks/projections/site/cms_editor/templates/src/ui/shell.ts.tmpl +211 -0
  297. data/lib/hecks/projections/site/cms_editor/templates/src/ui/table.ts.tmpl +151 -0
  298. data/lib/hecks/projections/site/cms_editor/templates/src/ui/theme.js.tmpl +16 -0
  299. data/lib/hecks/projections/site/cms_editor/templates/src/ui/values.ts.tmpl +69 -0
  300. data/lib/hecks/projections/site/cms_editor/templates/src/ui/words.ts.tmpl +97 -0
  301. data/lib/hecks/projections/site/cms_editor/templates/src/ui/world.ts.tmpl +35 -0
  302. data/lib/hecks/projections/site/cms_editor/templates/tsconfig.json.tmpl +17 -0
  303. data/lib/hecks/projections/site/cms_editor/theme.rb +29 -0
  304. data/lib/hecks/projections/site/cms_editor/theme_checks.rb +57 -0
  305. data/lib/hecks/projections/site/cms_editor.rb +133 -0
  306. data/lib/hecks/projections/site/role_probe.rb +74 -0
  307. data/lib/hecks/projector/cli_audience.rb +2 -1
  308. data/lib/hecks/projector/cli_projector/listing.rb +1 -1
  309. data/lib/hecks/projector/cli_projector/option_specs.rb +1 -1
  310. data/lib/hecks/projector/docs_projector/closing.rb +1 -1
  311. data/lib/hecks/projector/docs_projector.rb +1 -1
  312. data/lib/hecks/projector/exporter/bindings.rb +40 -10
  313. data/lib/hecks/projector/exporter/capability_values.rb +55 -0
  314. data/lib/hecks/projector/narrate_projector/reactions.rb +1 -1
  315. data/lib/hecks/projector/target.rb +11 -0
  316. data/lib/hecks/projector.rb +32 -5
  317. data/lib/hecks/quality_control/adapters/github_checks.rb +12 -8
  318. data/lib/hecks/quality_control/adapters/github_issues.rb +8 -4
  319. data/lib/hecks/quality_control/cli/qa_open_pr.rb +2 -2
  320. data/lib/hecks/quality_control/cli/qa_sweep/checks/shrinking.rb +1 -1
  321. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +1 -1
  322. data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
  323. data/lib/hecks/quality_control/quality_control.bluebook +6 -2
  324. data/lib/hecks/release/approval.rb +64 -0
  325. data/lib/hecks/router/{door.rb → aggregate_reader.rb} +5 -4
  326. data/lib/hecks/router/namespace_installer.rb +16 -16
  327. data/lib/hecks/router.rb +1 -1
  328. data/lib/hecks/runtime/best_effort.rb +22 -0
  329. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -2
  330. data/lib/hecks/runtime/command_interpreter.rb +3 -2
  331. data/lib/hecks/runtime/command_rules/emission.rb +1 -1
  332. data/lib/hecks/runtime/dispatcher.rb +4 -4
  333. data/lib/hecks/runtime/entity_element/location.rb +1 -1
  334. data/lib/hecks/runtime/event.rb +5 -0
  335. data/lib/hecks/runtime/identity.rb +7 -6
  336. data/lib/hecks/runtime/interpreting.rb +2 -1
  337. data/lib/hecks/runtime/invocation_builder.rb +1 -1
  338. data/lib/hecks/runtime/loader/boot_steps.rb +8 -5
  339. data/lib/hecks/runtime/loader.rb +10 -10
  340. data/lib/hecks/runtime/policy_interpreter/arguments.rb +7 -4
  341. data/lib/hecks/runtime/policy_interpreter/fan_out.rb +3 -3
  342. data/lib/hecks/runtime/policy_interpreter.rb +6 -6
  343. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -1
  344. data/lib/hecks/runtime/reaction_invocation.rb +1 -1
  345. data/lib/hecks/runtime/rebuild_sweep.rb +11 -1
  346. data/lib/hecks/runtime/registry/ask_verification.rb +35 -0
  347. data/lib/hecks/runtime/registry/hecksagon_rules.rb +10 -0
  348. data/lib/hecks/runtime/registry/merging.rb +1 -1
  349. data/lib/hecks/runtime/registry/providers.rb +16 -0
  350. data/lib/hecks/runtime/registry/read_repository.rb +6 -4
  351. data/lib/hecks/runtime/registry/verification.rb +4 -1
  352. data/lib/hecks/runtime/registry/world_defaults.rb +29 -0
  353. data/lib/hecks/runtime/registry.rb +1 -1
  354. data/lib/hecks/runtime/saga_interpreter/compensation.rb +1 -1
  355. data/lib/hecks/runtime/saga_interpreter/delivery.rb +4 -4
  356. data/lib/hecks/runtime/saga_interpreter/transitions.rb +1 -1
  357. data/lib/hecks/runtime/saga_interpreter.rb +3 -3
  358. data/lib/hecks/runtime/tenant_check.rb +5 -4
  359. data/lib/hecks/runtime/value/coercion.rb +2 -2
  360. data/lib/hecks/runtime/value/validation.rb +16 -4
  361. data/lib/hecks/runtime.rb +9 -9
  362. data/lib/hecks/rust_build/project_rust/target_ir.rb +1 -1
  363. data/lib/hecks/rust_build/wasm.rb +1 -1
  364. data/lib/hecks/rust_build/wasm_browser.rb +1 -1
  365. data/lib/hecks/rust_build.rb +11 -0
  366. data/lib/hecks/served_options.rb +53 -0
  367. data/lib/hecks/site/bluebook/site.bluebook +29 -1
  368. data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
  369. data/lib/hecks/storehouse/audit.rb +1 -1
  370. data/lib/hecks/storehouse/dispatching.rb +8 -6
  371. data/lib/hecks/storehouse/introspection.rb +2 -2
  372. data/lib/hecks/storehouse/querying.rb +6 -4
  373. data/lib/hecks/storehouse.rb +4 -4
  374. data/lib/hecks/tools/comment_style/source_file.rb +4 -1
  375. data/lib/hecks/tools/deploy_recipe_lint/fixtures.rb +4 -1
  376. data/lib/hecks/tools/lanes/rows.rb +6 -0
  377. data/lib/hecks/tools/oidc_manifests.rb +4 -5
  378. data/lib/hecks/tools/regeneration_run.rb +11 -4
  379. data/lib/hecks/tools/site_routes/cli.rb +1 -1
  380. data/lib/hecks/tools/site_routes/editor_files.rb +45 -0
  381. data/lib/hecks/tools/site_routes.rb +14 -7
  382. data/lib/hecks/tools/tenant_provisioning.rb +2 -2
  383. data/lib/hecks/tools/tools_doc.rb +4 -4
  384. data/lib/hecks/version.rb +1 -1
  385. data/lib/hecks/vocabulary.rb +4 -4
  386. data/lib/hecks.rb +15 -15
  387. data/lib/rubocop/cop/hecks/env_dump.rb +69 -0
  388. data/lib/rubocop/cop/hecks/monotonic_duration.rb +53 -0
  389. data/lib/rubocop/cop/hecks/no_wall_clock_in_runtime.rb +34 -0
  390. data/lib/rubocop/cop/hecks/raw_shell_interpolation.rb +79 -0
  391. data/lib/rubocop/cop/hecks/sleep_in_spec.rb +34 -0
  392. data/lib/rubocop/cop/hecks/swallowed_rescue.rb +37 -0
  393. data/lib/rubocop/cop/hecks/symbol_string_key_mix.rb +50 -0
  394. data/lib/rubocop/cop/hecks/untyped_raise.rb +37 -0
  395. data/prebuilt/05c358d38d661a3774f813d49c66f5b68939af94432d66257a56f49ee2598994.marshal +0 -0
  396. data/prebuilt/26c40c61f09b09b11651c64668eda3ceb180f83cf2a3802d3033d2fd99ac841b.marshal +0 -0
  397. data/prebuilt/verdicts-230ac18c25abfd3d92ede032bee2661a4c2cd2773ab0cf0e2ae52546ba2f7af9.json +1 -0
  398. data/rust/.gitignore +6 -0
  399. data/rust/Cargo.toml +9 -1
  400. data/rust/build/src/binding_pass.rs +103 -0
  401. data/rust/build/src/cargo_sync.rs +6 -0
  402. data/rust/build/src/lineage_pass.rs +3 -3
  403. data/rust/build/src/main.rs +17 -5
  404. data/rust/build/src/pipeline.rs +48 -5
  405. data/rust/build/src/seams_pass.rs +123 -0
  406. data/rust/build/src/translation_pass.rs +167 -0
  407. data/rust/codegen/src/asks.rs +103 -0
  408. data/rust/codegen/src/bridging.rs +1 -1
  409. data/rust/codegen/src/commands.rs +13 -8
  410. data/rust/codegen/src/constraints.rs +25 -0
  411. data/rust/codegen/src/domain_generator.rs +11 -9
  412. data/rust/codegen/src/expr_emitter.rs +13 -0
  413. data/rust/codegen/src/json_codec.rs +54 -15
  414. data/rust/codegen/src/main.rs +2 -1
  415. data/rust/codegen/src/mutations.rs +174 -4
  416. data/rust/codegen/src/naming.rs +30 -0
  417. data/rust/codegen/src/queries.rs +11 -2
  418. data/rust/codegen/src/reactions.rs +1 -1
  419. data/rust/codegen/src/types.rs +166 -8
  420. data/rust/host/HECKS_RELEASE +1 -1
  421. data/rust/host/src/api.rs +4 -4
  422. data/rust/host/src/bin/validate_harness.rs +43 -0
  423. data/rust/host/src/boundary_fuzz/expr_json.rs +2 -2
  424. data/rust/host/src/boundary_fuzz/ir.rs +1 -0
  425. data/rust/host/src/boundary_fuzz/server.rs +1 -1
  426. data/rust/host/src/checkout.rs +35 -19
  427. data/rust/host/src/commerce_ir.rs +408 -2
  428. data/rust/host/src/expr_json.rs +118 -0
  429. data/rust/host/src/mint.rs +1 -1
  430. data/rust/host/src/payments/tests.rs +27 -1
  431. data/rust/host/src/payments.rs +12 -4
  432. data/rust/host/src/query_step.rs +22 -4
  433. data/rust/host/src/reference_validate.rs +653 -108
  434. data/rust/host/src/resend.rs +2 -0
  435. data/rust/host/src/server.rs +19 -6
  436. data/rust/host/src/ui_schema.rs +132 -5
  437. data/rust/host/src/web/newsletter.rs +7 -8
  438. data/rust/host/src/web/newsletter_send.rs +38 -13
  439. data/rust/host/src/web/registrations.rs +229 -26
  440. data/rust/host/src/web.rs +3 -3
  441. data/rust/parser/src/emit.rs +38 -6
  442. data/rust/parser/src/expr/ast_json.rs +7 -1
  443. data/rust/parser/src/expr/resolver.rs +171 -6
  444. data/rust/parser/src/ir.rs +7 -0
  445. data/rust/parser/src/keywords.rs +24 -0
  446. data/rust/parser/src/main.rs +3 -0
  447. data/rust/parser/src/parse/aggregate.rs +2 -1
  448. data/rust/parser/src/parse/chapter.rs +2 -2
  449. data/rust/parser/src/parse/command.rs +2 -0
  450. data/rust/parser/src/parse/hecksagon.rs +5 -2
  451. data/rust/parser/src/parse/lifecycle.rs +81 -0
  452. data/rust/parser/src/parse/mod.rs +15 -1
  453. data/rust/parser/src/parse/policy.rs +13 -2
  454. data/rust/parser/src/parse/value_object.rs +13 -8
  455. data/rust/parser/src/ruby_value.rs +1 -1
  456. data/rust/src/exemplar/commands.rs +4 -4
  457. data/rust/src/exemplar/constraints.rs +1 -1
  458. data/rust/src/exemplar/json.rs +43 -1
  459. data/rust/src/kernel/cli.rs +7 -5
  460. data/rust/src/kernel/dispatch.rs +2 -2
  461. data/rust/src/kernel/dispatch_guard_tests.rs +1 -1
  462. data/rust/src/kernel/expr.rs +73 -4
  463. data/rust/src/kernel/expression_operators/arithmetic.rs +130 -5
  464. data/rust/src/kernel/expression_operators/presence.rs +41 -3
  465. data/rust/src/kernel/expression_operators/text.rs +26 -2
  466. data/rust/src/kernel/json.rs +15 -0
  467. data/rust/src/kernel/mod.rs +1 -1
  468. data/rust/src/kernel/orchestrate.rs +54 -5
  469. data/rust/src/kernel/orchestrate_guard_tests.rs +67 -0
  470. data/rust/src/kernel/pattern.rs +60 -0
  471. data/rust/src/kernel/reference_lookup.rs +90 -7
  472. metadata +174 -36
  473. data/lib/hecks/doors/cli_door/nesting.rb +0 -34
  474. data/lib/hecks/doors/cli_door/shorthand.rb +0 -85
  475. data/lib/hecks/doors/cli_door.rb +0 -124
  476. data/lib/hecks/doors/cli_runner/answers.rb +0 -138
  477. data/lib/hecks/doors/cli_runner/resolution.rb +0 -146
  478. data/lib/hecks/doors/cli_runner/settling.rb +0 -115
  479. data/lib/hecks/doors/cli_runner/streaming.rb +0 -90
  480. data/lib/hecks/doors/cli_runner/suggestions.rb +0 -43
  481. data/lib/hecks/doors/cli_runner.rb +0 -66
  482. data/lib/hecks/doors/command_request.rb +0 -119
  483. data/lib/hecks/doors/handle.rb +0 -208
  484. data/lib/hecks/doors/json_door.rb +0 -132
  485. data/lib/hecks/doors/launcher_options.rb +0 -247
  486. data/lib/hecks/doors/mcp_door/properties.rb +0 -47
  487. data/lib/hecks/doors/mcp_door/tool_calls.rb +0 -113
  488. data/lib/hecks/doors/mcp_door/tool_definitions/auditing.rb +0 -51
  489. data/lib/hecks/doors/mcp_door/tool_definitions/calling.rb +0 -69
  490. data/lib/hecks/doors/mcp_door/tool_definitions/reading.rb +0 -97
  491. data/lib/hecks/doors/mcp_door/tool_definitions.rb +0 -15
  492. data/lib/hecks/doors/mcp_door.rb +0 -156
  493. data/lib/hecks/doors/mcp_door_scope/argument_policy.rb +0 -92
  494. data/lib/hecks/doors/mcp_door_scope/env_problems.rb +0 -55
  495. data/lib/hecks/doors/mcp_door_scope/presenter.rb +0 -78
  496. data/lib/hecks/doors/mcp_door_scope.rb +0 -201
  497. data/lib/hecks/doors/mcp_guide_cache.rb +0 -75
  498. data/lib/hecks/doors/ruby_door/aggregate_door.rb +0 -162
  499. data/lib/hecks/doors/ruby_door/chapter.rb +0 -89
  500. data/lib/hecks/doors/ruby_door.rb +0 -80
  501. data/lib/hecks/doors/usage_cache.rb +0 -139
  502. data/prebuilt/0ee7f83cba59ce7d1c45c65ba166b071779f70143449f584f33ff7989f64eccb.marshal +0 -0
  503. data/prebuilt/6e4a6bcf0aa880e2e6b056e1846b70e5a768090b9dc998bbadeaf5d6bcfd3db8.marshal +0 -0
  504. data/prebuilt/verdicts-91aaf93ce8981bd9a754948f075d95650dc2ec6435015376f5c60992b36a24b1.json +0 -1
@@ -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 {
@@ -455,6 +516,16 @@ pub fn lifecycle_transition_for(command: &Json, aggregate: &Json) -> Option<Tran
455
516
  Some(Transition { field, to_state, from_states, unconstrained })
456
517
  }
457
518
 
519
+ /// Whether a `state(:field)` source reads an `Option` out of the pre-dispatch record: the field
520
+ /// is declared optional, or the record holds every field as an `Option` (`record_optional`).
521
+ /// Such a source already has the target's wrapping, so it must not be wrapped in `Some` again.
522
+ fn state_source_is_option(source: Option<&Json>, aggregate: &Json, record_optional: bool) -> bool {
523
+ let Some(source) = source.filter(|s| s.get("kind").map(Json::to_s).unwrap_or_default() == "state") else { return false };
524
+ let name = source.get("name").map(Json::to_s).unwrap_or_default();
525
+ let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
526
+ record_optional || attrs.iter().any(|a| crate::attr::name(a) == name && crate::attr::optional(a))
527
+ }
528
+
458
529
  /// Right-hand side for a `set`, coercing the source into the target attribute's declared type.
459
530
  ///
460
531
  /// `target_list` routes list-to-list sets through `list_value_rhs`; `value_rhs` is scalar-only.
@@ -712,8 +783,12 @@ fn emit_mutation_line_body(
712
783
  } else {
713
784
  let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
714
785
  let target_attr = attrs.iter().find(|a| crate::attr::name(a) == target_name).expect("set target must be a declared aggregate attribute");
715
- 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));
716
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
+ };
717
792
  let source_attr = if source.map(|s| s.get("kind").map(Json::to_s).unwrap_or_default()) == Some("argument".to_string()) {
718
793
  let name = source.unwrap().get("name").map(Json::to_s).unwrap_or_default();
719
794
  let cmd_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
@@ -731,7 +806,8 @@ fn emit_mutation_line_body(
731
806
  let (guard, effective_rhs) = entity_list_replace_guard(aggregate, target_attr, &target_field, &rhs, value_objects_by_name);
732
807
  format!("{guard}{}", exemplar.render("mutation_set_plain", &[("tmpl_field", target_field.to_string()), ("tmpl_rhs_placeholder2()", effective_rhs)]))
733
808
  } else {
734
- let wrap = (optional || crate::attr::optional(target_attr)) && !source_attr.map(crate::attr::optional).unwrap_or(false);
809
+ let source_is_option = source_attr.map(crate::attr::optional).unwrap_or(false) || state_source_is_option(source, aggregate, optional);
810
+ let wrap = (optional || crate::attr::optional(target_attr)) && !source_is_option;
735
811
  if wrap {
736
812
  exemplar.render("mutation_set_wrapped", &[("tmpl_field", target_field.to_string()), ("tmpl_rhs_placeholder2()", rhs)])
737
813
  } else {
@@ -851,3 +927,97 @@ mod transition_tests {
851
927
  assert_eq!(transition_check_arg(Some(&transition)), "None");
852
928
  }
853
929
  }
930
+
931
+ #[cfg(test)]
932
+ mod state_source_tests {
933
+ use super::*;
934
+
935
+ fn aggregate() -> Json {
936
+ Json::parse(r#"{"name":"Page","attributes":[{"name":"body","type":"Body"},{"name":"draft_body","type":"Body","optional":true}]}"#).unwrap()
937
+ }
938
+
939
+ fn state(name: &str) -> Json {
940
+ Json::parse(&format!(r#"{{"kind":"state","name":"{name}"}}"#)).unwrap()
941
+ }
942
+
943
+ #[test]
944
+ fn a_state_source_in_an_all_optional_record_is_already_an_option() {
945
+ assert!(state_source_is_option(Some(&state("body")), &aggregate(), true));
946
+ }
947
+
948
+ #[test]
949
+ fn a_state_source_reads_an_option_where_it_is_declared_optional() {
950
+ assert!(state_source_is_option(Some(&state("draft_body")), &aggregate(), false));
951
+ }
952
+
953
+ #[test]
954
+ fn a_required_state_source_in_a_plain_record_is_not_an_option() {
955
+ assert!(!state_source_is_option(Some(&state("body")), &aggregate(), false));
956
+ }
957
+
958
+ #[test]
959
+ fn an_argument_source_is_never_a_state_option() {
960
+ let argument = Json::parse(r#"{"kind":"argument","name":"draft_body"}"#).unwrap();
961
+ assert!(!state_source_is_option(Some(&argument), &aggregate(), true));
962
+ }
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
+ }
@@ -70,6 +70,23 @@ pub fn dispatch_fn_name(cmd: &str) -> String {
70
70
  /// `hecks project_reserved_names` — the same tables `naming.rb` reads.
71
71
  pub use crate::reserved_names::{CARGO_RESERVED_DOMAIN_NAMES, RUST_KEYWORDS};
72
72
 
73
+ /// The modules every generated chapter declares itself, beside its aggregates' modules: the
74
+ /// sidecar `metadata`, the `registry` table, and the `merged` router. An aggregate whose
75
+ /// downcased name is one of these would declare the module twice and overwrite the file.
76
+ pub const CHAPTER_OWNED_MODULES: &[&str] = &["metadata", "registry", "merged"];
77
+
78
+ /// The module (and file stem) one aggregate's generated Rust lives in: its downcased name, or
79
+ /// `<name>_aggregate` when that name is one the chapter declares itself. A downcased name never
80
+ /// holds an underscore, so the suffixed form cannot meet another aggregate's module.
81
+ pub fn aggregate_module(name: &str) -> String {
82
+ let module = name.to_lowercase();
83
+ if CHAPTER_OWNED_MODULES.contains(&module.as_str()) {
84
+ format!("{module}_aggregate")
85
+ } else {
86
+ module
87
+ }
88
+ }
89
+
73
90
  /// `/\A[a-z_][a-z0-9_]*\z/` — a plain lowercase Rust identifier.
74
91
  fn plain_lower_identifier(name: &str) -> bool {
75
92
  let mut chars = name.chars();
@@ -336,6 +353,19 @@ mod tests {
336
353
  assert_eq!(scalar_to_value("String", &deref_scalar("String", "v")).as_deref(), Some("Value::Str(v.clone())"));
337
354
  }
338
355
 
356
+ #[test]
357
+ fn an_aggregate_named_for_a_chapter_module_gets_a_suffixed_module() {
358
+ assert_eq!(aggregate_module("Metadata"), "metadata_aggregate");
359
+ assert_eq!(aggregate_module("Registry"), "registry_aggregate");
360
+ assert_eq!(aggregate_module("Merged"), "merged_aggregate");
361
+ }
362
+
363
+ #[test]
364
+ fn any_other_aggregate_keeps_its_downcased_module() {
365
+ assert_eq!(aggregate_module("LineItem"), "lineitem");
366
+ assert_eq!(aggregate_module("MetadataEntry"), "metadataentry");
367
+ }
368
+
339
369
  #[test]
340
370
  fn refuses_an_aggregate_whose_module_name_is_a_rust_keyword() {
341
371
  let refusal = reserved_name_refusal("shop", "shop", &["Pizza", "Match", "Type"]).expect("refused");
@@ -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());
@@ -26,15 +26,15 @@ pub fn emit_check_invariants(exemplar: &Exemplar, vo: &Json, value_objects_by_na
26
26
  continue;
27
27
  }
28
28
  let field = naming::rust_ident_field(crate::attr::name(attr));
29
- if crate::attr::list(attr) {
30
- body.push(format!(" for item in &self.{field} {{ item.check_invariants()?; }}"));
31
- } else {
32
- body.push(format!(" self.{field}.check_invariants()?;"));
33
- }
29
+ body.push(nested_invariant_line(&field, crate::attr::list(attr), crate::attr::optional(attr)));
34
30
  }
35
31
 
36
32
  for attr in attributes {
37
33
  if crate::attr::list(attr) {
34
+ let field = format!("self.{}", naming::rust_ident_field(crate::attr::name(attr)));
35
+ if let Some(line) = crate::constraints::emit_list_pattern_check(exemplar, &field, attr, &name) {
36
+ body.push(format!(" {line}"));
37
+ }
38
38
  continue;
39
39
  }
40
40
  let field = format!("self.{}", naming::rust_ident_field(crate::attr::name(attr)));
@@ -64,6 +64,18 @@ pub fn emit_check_invariants(exemplar: &Exemplar, vo: &Json, value_objects_by_na
64
64
  format!("impl {name} {{\n pub fn check_invariants(&self) -> Result<(), crate::kernel::Refusal> {{\n{}\n Ok(())\n }}\n}}\n", body.join("\n"))
65
65
  }
66
66
 
67
+ /// The line that runs a nested value object's own invariants. An optional slot is an `Option` in
68
+ /// the record, so its invariants run only when it is present, as Ruby validates only what it is
69
+ /// given; a list's members each run theirs.
70
+ fn nested_invariant_line(field: &str, list: bool, optional: bool) -> String {
71
+ match (list, optional) {
72
+ (true, true) => format!(" if let Some(items) = &self.{field} {{ for item in items {{ item.check_invariants()?; }} }}"),
73
+ (true, false) => format!(" for item in &self.{field} {{ item.check_invariants()?; }}"),
74
+ (false, true) => format!(" if let Some(value) = &self.{field} {{ value.check_invariants()?; }}"),
75
+ (false, false) => format!(" self.{field}.check_invariants()?;"),
76
+ }
77
+ }
78
+
67
79
  pub fn emit_closed_set_table(exemplar: &Exemplar, vo: &Json) -> String {
68
80
  let name = naming::rust_ident(vo.get("name").and_then(Json::as_str).unwrap_or(""));
69
81
  let attributes = vo.get("attributes").map(Json::each).unwrap_or(&[]);
@@ -286,6 +298,83 @@ pub fn emit_record(exemplar: &Exemplar, aggregate: &Json, value_objects_by_name:
286
298
  format!("{struct_part}\n\n{fielded_part}")
287
299
  }
288
300
 
301
+ /// Copy of `ir` whose every `projects` field also carries the `type` of the remote field it reads,
302
+ /// so the projecting aggregate's pseudo-attribute and setter follow it (ADR 0025 addendum). A
303
+ /// single-field value object resolves to its one attribute's type, a reference to `String`, and a
304
+ /// field that resolves to nothing stays `String`.
305
+ pub fn with_resolved_projection_types(ir: &Json) -> Json {
306
+ let mut resolved = ir.clone();
307
+ let aggregates: Vec<Json> = ir.get("aggregates").map(Json::each).unwrap_or(&[]).to_vec();
308
+ let Some(list) = resolved.get_mut("aggregates").and_then(Json::as_array_mut) else {
309
+ return resolved;
310
+ };
311
+ for (aggregate, original) in list.iter_mut().zip(aggregates.iter()) {
312
+ let Some(fields) = aggregate.get_mut("projected_fields").and_then(Json::as_array_mut) else {
313
+ continue;
314
+ };
315
+ for field in fields.iter_mut() {
316
+ let scalar = projected_scalar_type(&aggregates, original, field, 0);
317
+ field.set("type", Json::String(scalar));
318
+ }
319
+ }
320
+ resolved
321
+ }
322
+
323
+ const PROJECTION_CHAIN_LIMIT: usize = 8;
324
+
325
+ // The scalar type a projection of `field` copies, following a chain of projections.
326
+ fn projected_scalar_type(aggregates: &[Json], owner: &Json, field: &Json, depth: usize) -> String {
327
+ let reference = field.get("reference").and_then(Json::as_str).unwrap_or("");
328
+ let remote = field.get("remote_field").and_then(Json::as_str).unwrap_or("");
329
+ let target = owner
330
+ .get("attributes")
331
+ .map(Json::each)
332
+ .unwrap_or(&[])
333
+ .iter()
334
+ .find(|a| crate::attr::name(a) == reference)
335
+ .and_then(|a| naming::reference_target(crate::attr::type_name(a)))
336
+ .and_then(|name| aggregates.iter().find(|a| a.get("name").and_then(Json::as_str) == Some(name)));
337
+ target.map(|t| remote_scalar_type(aggregates, t, remote, depth)).unwrap_or_else(|| "String".to_string())
338
+ }
339
+
340
+ fn remote_scalar_type(aggregates: &[Json], target: &Json, remote: &str, depth: usize) -> String {
341
+ let declared = target.get("attributes").map(Json::each).unwrap_or(&[]).iter().find(|a| crate::attr::name(a) == remote);
342
+ if let Some(attr) = declared {
343
+ return unwrapped_scalar_type(target, crate::attr::type_name(attr));
344
+ }
345
+ let chained = target.get("projected_fields").map(Json::each).unwrap_or(&[]).iter().find(|f| f.get("name").and_then(Json::as_str) == Some(remote));
346
+ match chained {
347
+ Some(next) if depth < PROJECTION_CHAIN_LIMIT => projected_scalar_type(aggregates, target, next, depth + 1),
348
+ _ => "String".to_string(),
349
+ }
350
+ }
351
+
352
+ // A single-field value object reads as its one attribute's type; anything else as itself.
353
+ fn unwrapped_scalar_type(aggregate: &Json, type_name: &str) -> String {
354
+ let sole = aggregate
355
+ .get("value_objects")
356
+ .map(Json::each)
357
+ .unwrap_or(&[])
358
+ .iter()
359
+ .find(|vo| vo.get("name").and_then(Json::as_str) == Some(type_name))
360
+ .map(|vo| vo.get("attributes").map(Json::each).unwrap_or(&[]))
361
+ .filter(|attrs| attrs.len() == 1 && !crate::attr::list(&attrs[0]))
362
+ .map(|attrs| crate::attr::type_name(&attrs[0]).to_string());
363
+ let resolved = sole.unwrap_or_else(|| type_name.to_string());
364
+ let resolved = if resolved == "Boolean" { "TrueClass".to_string() } else { resolved };
365
+ naming::effective_scalar_type(&resolved).unwrap_or("String").to_string()
366
+ }
367
+
368
+ // The `Value` reader a generated setter narrows with, by scalar type.
369
+ fn projection_reader(scalar: &str) -> &'static str {
370
+ match scalar {
371
+ "Integer" => "into_i64",
372
+ "Float" => "into_f64",
373
+ "TrueClass" | "FalseClass" => "into_bool",
374
+ _ => "into_string",
375
+ }
376
+ }
377
+
289
378
  /// Port of the retired Ruby generator's `types.rb#projected_field_pseudo_attributes` (ADR 0025).
290
379
  pub fn projected_field_pseudo_attributes(aggregate: &Json) -> Vec<Json> {
291
380
  aggregate
@@ -295,9 +384,10 @@ pub fn projected_field_pseudo_attributes(aggregate: &Json) -> Vec<Json> {
295
384
  .iter()
296
385
  .map(|field| {
297
386
  let name = field.get("name").and_then(Json::as_str).unwrap_or("").to_string();
387
+ let scalar = field.get("type").and_then(Json::as_str).unwrap_or("String").to_string();
298
388
  Json::Object(vec![
299
389
  ("name".to_string(), Json::String(name)),
300
- ("type".to_string(), Json::String("String".to_string())),
390
+ ("type".to_string(), Json::String(scalar)),
301
391
  ("list".to_string(), Json::Bool(false)),
302
392
  ("optional".to_string(), Json::Bool(true)),
303
393
  ])
@@ -329,11 +419,12 @@ pub fn emit_set_projected_field(aggregate: &Json) -> String {
329
419
  .map(|field| {
330
420
  let field_name = field.get("name").and_then(Json::as_str).unwrap_or("");
331
421
  let ident = naming::rust_ident_field(field_name);
332
- format!(" {} => self.{} = value,", naming::ruby_inspect_string(field_name), ident)
422
+ let reader = projection_reader(field.get("type").and_then(Json::as_str).unwrap_or("String"));
423
+ format!(" {} => self.{} = value.and_then(crate::kernel::Value::{}),", naming::ruby_inspect_string(field_name), ident, reader)
333
424
  })
334
425
  .collect();
335
426
  format!(
336
- "impl crate::kernel::SetProjectedField for {name} {{\n fn set_projected_field(&mut self, name: &'static str, value: Option<String>) {{\n match name {{\n{}\n _ => {{}}\n }}\n }}\n}}\n",
427
+ "impl crate::kernel::SetProjectedField for {name} {{\n fn set_projected_field(&mut self, name: &'static str, value: Option<crate::kernel::Value>) {{\n match name {{\n{}\n _ => {{}}\n }}\n }}\n}}\n",
337
428
  arms.join("\n")
338
429
  )
339
430
  }
@@ -404,4 +495,71 @@ mod tests {
404
495
  assert!(!from_json.contains("TrueClass::from_json"));
405
496
  assert!(to_json.contains("crate::kernel::Json::Bool(self.on)"));
406
497
  }
498
+
499
+ /// An Event whose `starts_at` is a single-field Integer value object, and a Booking projecting it.
500
+ fn event_and_booking() -> Json {
501
+ Json::parse(
502
+ r#"{"aggregates":[
503
+ {"name":"Event","attributes":[{"name":"starts_at","type":"StartsAt","list":false}],
504
+ "value_objects":[{"name":"StartsAt","attributes":[{"name":"value","type":"Integer","list":false}]}]},
505
+ {"name":"Booking","attributes":[{"name":"event","type":"Reference<Event>","list":false}],
506
+ "projected_fields":[{"name":"starts_at","reference":"event","remote_field":"starts_at"}]}
507
+ ]}"#,
508
+ )
509
+ .unwrap()
510
+ }
511
+
512
+ fn resolved_booking() -> Json {
513
+ with_resolved_projection_types(&event_and_booking()).get("aggregates").map(Json::each).unwrap_or(&[])[1].clone()
514
+ }
515
+
516
+ #[test]
517
+ fn a_projection_through_a_single_field_value_object_takes_its_scalar_type() {
518
+ let field = resolved_booking().get("projected_fields").map(Json::each).unwrap_or(&[])[0].clone();
519
+
520
+ assert_eq!(field.get("type").and_then(Json::as_str), Some("Integer"));
521
+ }
522
+
523
+ #[test]
524
+ fn the_projected_pseudo_attribute_follows_the_remote_type() {
525
+ let attrs = projected_field_pseudo_attributes(&resolved_booking());
526
+
527
+ assert_eq!(crate::attr::type_name(&attrs[0]), "Integer");
528
+ }
529
+
530
+ #[test]
531
+ fn the_projected_setter_narrows_with_the_remote_types_reader() {
532
+ let setter = emit_set_projected_field(&resolved_booking());
533
+
534
+ assert!(setter.contains("\"starts_at\" => self.starts_at = value.and_then(crate::kernel::Value::into_i64),"));
535
+ }
536
+
537
+ /// A value object holding a required and an optional value object, and a required and an
538
+ /// optional list of them.
539
+ fn holder_value_object() -> Json {
540
+ Json::parse(
541
+ r#"{
542
+ "name":"Holder",
543
+ "attributes":[
544
+ {"name":"required_note","type":"Note","list":false,"optional":false},
545
+ {"name":"note","type":"Note","list":false,"optional":true},
546
+ {"name":"required_notes","type":"Note","list":true,"optional":false},
547
+ {"name":"notes","type":"Note","list":true,"optional":true}
548
+ ]
549
+ }"#,
550
+ )
551
+ .unwrap()
552
+ }
553
+
554
+ #[test]
555
+ fn a_nested_value_object_checks_its_invariants_only_when_it_is_present() {
556
+ let note = Json::parse(r#"{"name":"Note","attributes":[{"name":"text","type":"String","list":false,"optional":false}]}"#).unwrap();
557
+ let by_name: HashMap<String, &Json> = HashMap::from([("Note".to_string(), &note)]);
558
+ let generated = emit_check_invariants(&Exemplar::load(), &holder_value_object(), &by_name, &HashMap::new());
559
+
560
+ assert!(generated.contains("self.required_note.check_invariants()?;"));
561
+ assert!(generated.contains("if let Some(value) = &self.note { value.check_invariants()?; }"));
562
+ assert!(generated.contains("for item in &self.required_notes { item.check_invariants()?; }"));
563
+ assert!(generated.contains("if let Some(items) = &self.notes { for item in items { item.check_invariants()?; } }"));
564
+ }
407
565
  }
@@ -1 +1 @@
1
- 3.9.1
1
+ 3.11.0
data/rust/host/src/api.rs CHANGED
@@ -1089,8 +1089,8 @@ mod tests {
1089
1089
  }
1090
1090
 
1091
1091
  #[test]
1092
- fn a_record_that_does_not_exist_refuses_with_json_doors_own_wording() {
1093
- // `JSON_DOOR.find!`'s own message, `id` inspected the way Ruby
1092
+ fn a_record_that_does_not_exist_refuses_with_json_adapters_own_wording() {
1093
+ // `JSON_ADAPTER.find!`'s own message, `id` inspected the way Ruby
1094
1094
  // inspects it.
1095
1095
  let refusal = not_found(&format!("no {} found for id {:?}", "Client", "acme"));
1096
1096
  assert_eq!(body(&refusal), json!({"error": "NotFound", "message": "no Client found for id \"acme\""}));
@@ -1365,7 +1365,7 @@ mod tests {
1365
1365
  fn every_state_styled() -> Value {
1366
1366
  json!({"states": {
1367
1367
  "Event": {"open": {"tone": "good"}, "closed": {"tone": "muted"}},
1368
- "Registration": {"active": {}, "archived": {"tone": "muted"}},
1368
+ "Registration": {"active": {}, "archived": {"tone": "muted"}, "cancelled": {"tone": "muted"}},
1369
1369
  "PaymentConnection": {"connected": {}, "enabled": {}, "disconnected": {}, "paused": {}}
1370
1370
  }})
1371
1371
  }
@@ -1590,7 +1590,7 @@ mod tests {
1590
1590
  assert!(!refused["message"].as_str().expect("a message").is_empty(), "{refused}");
1591
1591
 
1592
1592
  // An unknown command, and an unknown record, both refuse the
1593
- // way JsonDoor refuses them.
1593
+ // way Json refuses them.
1594
1594
  let unknown_command =
1595
1595
  command_route(&ir, "customers", "CUST-9001", "nope", "", &client, &wasm, &lineage, &invoker).await;
1596
1596
  assert_eq!(unknown_command["statusCode"], 404);
@@ -0,0 +1,43 @@
1
+ //! Rust side of the stored-value differential: runs the mint audit's structural layer
2
+ //! (`reference_validate::validate`) over each `{aggregate, id, state}` case on stdin against one
3
+ //! domain IR, and prints the violations of each. `spec/rust_host_reference_validate_conformance_spec.rb`
4
+ //! holds those to the answers the Ruby runtime gives the same values.
5
+
6
+ #[allow(dead_code)]
7
+ #[path = "../expr_json.rs"]
8
+ mod expr_json;
9
+ #[allow(dead_code)]
10
+ #[path = "../reference_validate.rs"]
11
+ mod reference_validate;
12
+
13
+ use serde_json::{json, Value};
14
+ use std::io::Read;
15
+
16
+ // Usage: `validate_harness < {"ir": <domain ir.json>, "cases": [{"aggregate": "Name", "id": "x", "state": {..}}, ..]}`.
17
+ // Prints `{"results": [{"violations": [..]} | {"error": ".."}, ..]}`, one per case, in order.
18
+ fn main() -> anyhow::Result<()> {
19
+ let mut input = String::new();
20
+ std::io::stdin().read_to_string(&mut input)?;
21
+ let request: Value = serde_json::from_str(&input)?;
22
+ let ir = request.get("ir").ok_or_else(|| anyhow::anyhow!("no \"ir\" on stdin"))?;
23
+ let cases = request.get("cases").and_then(Value::as_array).ok_or_else(|| anyhow::anyhow!("no \"cases\" array on stdin"))?;
24
+
25
+ let results: Vec<Value> = cases.iter().map(|case| answer(ir, case)).collect();
26
+ println!("{}", json!({ "results": results }));
27
+ Ok(())
28
+ }
29
+
30
+ fn answer(ir: &Value, case: &Value) -> Value {
31
+ let name = case.get("aggregate").and_then(Value::as_str).unwrap_or("");
32
+ let aggregate = ir
33
+ .get("aggregates")
34
+ .and_then(Value::as_array)
35
+ .and_then(|list| list.iter().find(|a| a.get("name").and_then(Value::as_str) == Some(name)));
36
+ match aggregate {
37
+ None => json!({ "error": format!("no aggregate {name:?} in the IR") }),
38
+ Some(aggregate) => {
39
+ let id = case.get("id").and_then(Value::as_str).unwrap_or("");
40
+ json!({ "violations": reference_validate::validate(ir, aggregate, id, &case["state"]) })
41
+ }
42
+ }
43
+ }
@@ -8,7 +8,7 @@ use crate::fuzz_support::*;
8
8
  use serde_json::{json, Value};
9
9
 
10
10
  const BINARY: &[&str] = &["or", "and", "compare", "include", "add", "modulo"];
11
- const UNARY: &[&str] = &["not", "sign_test", "empty", "to_s", "size", "presence", "assignment", "first", "last", "split", "starts_with", "ends_with", "matches_regex"];
11
+ const UNARY: &[&str] = &["not", "sign_test", "empty", "to_s", "size", "presence", "assignment", "first", "last", "split", "strip", "starts_with", "ends_with", "matches_regex"];
12
12
 
13
13
  const PATTERNS: &[&str] = &[
14
14
  "", "^$", "a", "(a*)*b", "(a|aa)+$", "[", "]", "[]", "[^]", "[a-", "(", ")", "(?", "(?P<x>", "\\", "\\d+", "\\D\\W\\S\\H", "\\Z", "[\\d]", "[\\Z]", "\\p{L}+", "\\p{Nope}",
@@ -43,7 +43,7 @@ fn node(rng: &mut Rng, depth: usize) -> Value {
43
43
  }
44
44
  4..=7 => {
45
45
  let op = *rng.pick(UNARY);
46
- let mut value = json!({"op": op, "receiver": node(rng, depth + 1), "expr": node(rng, depth + 1), "negated": rng.chance(2), "separator": random_text(rng), "substring": random_text(rng), "pattern": *rng.pick(PATTERNS), "flags": *rng.pick(&["", "i", "m", "x", "imx", "z"])});
46
+ let mut value = json!({"op": op, "receiver": node(rng, depth + 1), "expr": node(rng, depth + 1), "negated": rng.chance(2), "separator": random_text(rng), "side": *rng.pick(&["both", "left", "right", "sideways"]), "substring": random_text(rng), "pattern": *rng.pick(PATTERNS), "flags": *rng.pick(&["", "i", "m", "x", "imx", "z"])});
47
47
  value["cmp"] = json!({"less_than": rng.chance(2), "equal": rng.chance(2), "negated": rng.chance(2)});
48
48
  value
49
49
  }
@@ -50,6 +50,7 @@ fn every_provider_reader_answers_any_ir_shape() {
50
50
  let _ = newsletter_provider(&ir).map(|p| p.instance_prefix());
51
51
  let _ = newsletter_issues_provider(&ir).map(|p| p.issue_prefix());
52
52
  let _ = payments_provider(&ir).map(|p| p.instance_prefix());
53
+ let _ = checkout_windows(&ir);
53
54
  if let Some(p) = registrations_provider(&ir) {
54
55
  let _ = (p.event_prefix(), p.registration_prefix(), p.request_target());
55
56
  }
@@ -1,4 +1,4 @@
1
- //! Seeded fuzz of the HTTP front door (`admit`, `value_to_response`): hostile bodies, methods, URIs
1
+ //! Seeded fuzz of the HTTP front entry point (`admit`, `value_to_response`): hostile bodies, methods, URIs
2
2
  //! and headers must be answered, never panic, and a refused request must leave no trace.
3
3
  //! Everything here is in-process: `admit` is the whole of what runs before dispatch, so none of
4
4
  //! it needs Postgres or wasm.