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
@@ -9,6 +9,8 @@ const TIMEOUT: Duration = Duration::from_secs(15);
9
9
  const MAX_ATTEMPTS: u32 = 4;
10
10
  const DEFAULT_RETRY_SECS: u64 = 1;
11
11
  const MAX_RETRY_SECS: u64 = 5;
12
+ /// The address the mock refuses, and (in `mock_deliver`) the reason it gives. Adapter-side test
13
+ /// vocabulary: the domain names neither, and the Ruby mock keeps its own copy (ADR 0099).
12
14
  const MOCK_BOUNCE_ADDRESS: &str = "bounce@example.com";
13
15
 
14
16
  /// One email to one recipient.
@@ -23,7 +23,7 @@ use tokio::sync::Mutex;
23
23
  use tokio_postgres::Client;
24
24
 
25
25
  /// The per-request dispatch decision, shared between the Lambda custom-runtime path and the
26
- /// axum fallback route below. `body` is already-parsed JSON in one of three shapes.
26
+ /// axum fallback route below. `body` is already-parsed JSON in one of its recognized shapes.
27
27
  pub async fn dispatch_body(
28
28
  body: Value,
29
29
  client: &Mutex<Client>,
@@ -208,7 +208,7 @@ fn admit(limits: &RateLimits, peer: SocketAddr, method: &Method, uri: &Uri, head
208
208
 
209
209
  // A real HTTP client (browser via the ALB, or a server-to-server call) carries none of a
210
210
  // Function URL's automatic wrapping, so anything not already one of `dispatch_body`'s
211
- // three recognized shapes gets that envelope synthesized from the real request instead.
211
+ // recognized shapes gets that envelope synthesized from the real request instead.
212
212
  // The internal shapes carry a caller's own claim of `role` with no session check, so
213
213
  // they're honored only from a peer on this host (the task's own sidecar); everyone
214
214
  // else — the load balancer, i.e. the public internet — gets an ordinary request
@@ -247,7 +247,10 @@ fn trusts_internal_dispatch(peer: SocketAddr) -> bool {
247
247
  /// Whether `value` already matches one of `dispatch_body`'s recognized shapes; anything
248
248
  /// else (including a bare `{}`) is real REST traffic needing `synthesize_function_url_envelope`.
249
249
  fn is_internal_dispatch_shape(value: &Value) -> bool {
250
- value.get("requestContext").is_some() || value.get("read").is_some() || value.get("verb").is_some()
250
+ value.get("requestContext").is_some()
251
+ || value.get("read").is_some()
252
+ || value.get("query").is_some()
253
+ || value.get("verb").is_some()
251
254
  }
252
255
 
253
256
  /// Rebuilds the envelope shape a real Lambda Function URL invocation already produces
@@ -324,6 +327,13 @@ mod tests {
324
327
  assert!(is_internal_dispatch_shape(&serde_json::json!({"read": true})));
325
328
  }
326
329
 
330
+ #[test]
331
+ fn a_declared_query_body_is_recognized_as_internal_dispatch() {
332
+ // `dispatch_body` answers `{"query": ...}`, and the generated editor's list and picture
333
+ // pickers ask it; without this a loopback caller was sent to the login page.
334
+ assert!(is_internal_dispatch_shape(&serde_json::json!({"query": "Pictures::MediaItem.Pictures", "args": {}})));
335
+ }
336
+
327
337
  #[test]
328
338
  fn a_verb_command_body_is_recognized_as_internal_dispatch() {
329
339
  assert!(is_internal_dispatch_shape(&serde_json::json!({"verb": "Register", "args": {}})));
@@ -367,10 +377,13 @@ mod tests {
367
377
  // caller's own word for its role and no session check.
368
378
  #[test]
369
379
  fn a_verb_or_read_body_from_outside_this_host_is_an_ordinary_request_not_a_command() {
370
- for body in [r#"{"verb":"Approve","role":"admin"}"#, r#"{"read":true}"#] {
380
+ for body in [r#"{"verb":"Approve","role":"admin"}"#, r#"{"read":true}"#, r#"{"query":"Pictures::MediaItem.Pictures","args":{}}"#] {
371
381
  let envelope = admitted("10.0.0.5:5000", "/webhooks/x", body);
372
382
 
373
- assert!(envelope.get("verb").is_none() && envelope.get("read").is_none(), "{body} must not reach dispatch as a command");
383
+ assert!(
384
+ envelope.get("verb").is_none() && envelope.get("read").is_none() && envelope.get("query").is_none(),
385
+ "{body} must not reach dispatch as a command"
386
+ );
374
387
  assert_eq!(envelope["requestContext"]["http"]["method"], "POST");
375
388
  assert_eq!(envelope["rawPath"], "/webhooks/x");
376
389
  assert_eq!(envelope["body"], body);
@@ -389,7 +402,7 @@ mod tests {
389
402
 
390
403
  #[test]
391
404
  fn a_verb_or_read_body_from_this_host_still_reaches_dispatch_unchanged() {
392
- for body in [r#"{"verb":"Register","to":"r1","with":{}}"#, r#"{"read":true}"#] {
405
+ for body in [r#"{"verb":"Register","to":"r1","with":{}}"#, r#"{"read":true}"#, r#"{"query":"Banking::Customer.Suspended","args":{}}"#] {
393
406
  let envelope = admitted("127.0.0.1:5000", "/anything", body);
394
407
 
395
408
  assert_eq!(envelope, serde_json::from_str::<Value>(body).unwrap());
@@ -29,7 +29,8 @@ pub fn build(domain_ir: &Value, config: &Value) -> Value {
29
29
  let mut collections = Map::new();
30
30
  for aggregate in &aggregates {
31
31
  let key = key_for.get(agg_name(aggregate)).cloned().unwrap_or_default();
32
- collections.insert(key, build_collection(aggregate, config, &key_for, &by_name));
32
+ let scoped = with_shared_value_objects(aggregate, &aggregates);
33
+ collections.insert(key, build_collection(&scoped, config, &key_for, &by_name));
33
34
  }
34
35
 
35
36
  json!({
@@ -48,12 +49,13 @@ pub fn schema(domain_ir: &Value, config: &Value) -> Value {
48
49
 
49
50
  let mut schema = Map::new();
50
51
  for aggregate in &aggregates {
52
+ let scoped = with_shared_value_objects(aggregate, &aggregates);
51
53
  let queries: Vec<Value> = array(aggregate, "queries")
52
54
  .iter()
53
55
  .map(|query| {
54
56
  let args: Vec<Value> = array(query, "attributes")
55
57
  .iter()
56
- .map(|attribute| field(attribute, aggregate, &key_for, &Value::Null))
58
+ .map(|attribute| field(attribute, &scoped, &key_for, &Value::Null))
57
59
  .collect();
58
60
  json!({ "name": query.get("name").cloned().unwrap_or(Value::Null), "args": args })
59
61
  })
@@ -81,6 +83,30 @@ pub fn key_for(aggregates: &[&Value], config: &Value) -> KeyFor {
81
83
  aggregates.iter().map(|a| (agg_name(a).to_string(), collection_key(a, config))).collect()
82
84
  }
83
85
 
86
+ /// The aggregate with every value object the rest of the domain
87
+ /// declares appended to its own, so an attribute typed by a value object
88
+ /// another aggregate owns resolves like an owner-declared one. The
89
+ /// aggregate's own declarations come first and `find_value_object` takes
90
+ /// the first match: the owner wins, then declaration order among the
91
+ /// others — the same order `web.rs` reads a value object's shape in.
92
+ fn with_shared_value_objects(aggregate: &Value, aggregates: &[&Value]) -> Value {
93
+ let mut scoped = aggregate.clone();
94
+ let shared: Vec<Value> = aggregates
95
+ .iter()
96
+ .filter(|other| agg_name(other) != agg_name(aggregate))
97
+ .flat_map(|other| array(other, "value_objects").iter().cloned())
98
+ .collect();
99
+ if shared.is_empty() {
100
+ return scoped;
101
+ }
102
+ let mut value_objects = array(aggregate, "value_objects").to_vec();
103
+ value_objects.extend(shared);
104
+ if let Some(object) = scoped.as_object_mut() {
105
+ object.insert("value_objects".to_string(), Value::Array(value_objects));
106
+ }
107
+ scoped
108
+ }
109
+
84
110
  pub fn aggregates(domain_ir: &Value) -> Vec<&Value> {
85
111
  domain_ir.get("aggregates").and_then(|v| v.as_array()).map(|a| a.iter().collect()).unwrap_or_default()
86
112
  }
@@ -557,8 +583,9 @@ pub fn field(attribute: &Value, aggregate: &Value, key_for: &KeyFor, formats: &V
557
583
  return merge(base, primitive_shape(ty, format));
558
584
  }
559
585
 
560
- // A value object belongs to its own aggregate's table only — one
561
- // typed by another aggregate falls through here, exactly as in Ruby.
586
+ // `build` and `schema` hand this the aggregate with the domain's other
587
+ // value objects appended (`with_shared_value_objects`), so one declared
588
+ // on a different aggregate resolves here too.
562
589
  if let Some(value_object) = find_value_object(aggregate, ty) {
563
590
  if truthy(value_object.get("closed_set")) {
564
591
  return merge(base, enum_shape(value_object));
@@ -1347,5 +1374,105 @@ mod tests {
1347
1374
  ])
1348
1375
  );
1349
1376
  }
1350
- }
1351
1377
 
1378
+ // Two aggregates: `Site` declares the value objects, `Deployment` types
1379
+ // attributes by them without declaring any of its own.
1380
+ fn shared_domain(extra_deployment_vos: Value) -> Value {
1381
+ let vo = |name: &str, attrs: Value| json!({"name": name, "attributes": attrs, "closed_set": false, "members": []});
1382
+ json!({
1383
+ "name": "Fleet",
1384
+ "aggregates": [
1385
+ {
1386
+ "name": "Site", "identified_by": ["key.value"],
1387
+ "attributes": [{"name": "key", "type": "SiteKey", "list": false, "optional": false}],
1388
+ "value_objects": [
1389
+ vo("SiteKey", json!([{"name": "value", "type": "String"}])),
1390
+ vo("Window", json!([{"name": "starts", "type": "String"}, {"name": "ends", "type": "String"}])),
1391
+ vo("Price", json!([{"name": "cents", "type": "Integer"}, {"name": "currency", "type": "String"}])),
1392
+ {"name": "Tier", "attributes": [{"name": "level", "type": "String"}], "closed_set": true,
1393
+ "members": [[["level", "gold"]], [["level", "silver"]]]}
1394
+ ],
1395
+ "entities": [], "commands": [], "queries": []
1396
+ },
1397
+ {
1398
+ "name": "Deployment", "identified_by": ["id"],
1399
+ "attributes": [
1400
+ {"name": "site_key", "type": "SiteKey", "list": false, "optional": false},
1401
+ {"name": "window", "type": "Window", "list": false, "optional": true},
1402
+ {"name": "price", "type": "Price", "list": false, "optional": true},
1403
+ {"name": "tier", "type": "Tier", "list": false, "optional": true}
1404
+ ],
1405
+ "value_objects": extra_deployment_vos,
1406
+ "entities": [], "commands": [], "queries": []
1407
+ }
1408
+ ]
1409
+ })
1410
+ }
1411
+
1412
+ fn deployment_field(schema: &Value, name: &str) -> Value {
1413
+ schema["collections"]["deployments"]["fields"]
1414
+ .as_array()
1415
+ .and_then(|fields| fields.iter().find(|f| f["key"] == name))
1416
+ .cloned()
1417
+ .expect("a Deployment field")
1418
+ }
1419
+
1420
+ #[test]
1421
+ fn a_one_attribute_value_object_another_aggregate_declares_keeps_its_wrapped_field() {
1422
+ let schema = build(&shared_domain(json!([])), &json!({}));
1423
+ assert_eq!(
1424
+ deployment_field(&schema, "site_key"),
1425
+ json!({"key": "site_key", "label": "Site Key", "optional": false, "shape": "text_value", "field": "value"})
1426
+ );
1427
+ // The column the console renders from it unwraps the same key.
1428
+ let columns = &schema["collections"]["deployments"]["columns"];
1429
+ assert_eq!(columns[0]["field"], "value");
1430
+ }
1431
+
1432
+ #[test]
1433
+ fn compound_money_and_enum_value_objects_resolve_the_same_when_another_aggregate_declares_them() {
1434
+ let schema = build(&shared_domain(json!([])), &json!({}));
1435
+ assert_eq!(deployment_field(&schema, "window")["shape"], "compound");
1436
+ assert_eq!(deployment_field(&schema, "window")["fields"].as_array().map(Vec::len), Some(2));
1437
+ assert_eq!(deployment_field(&schema, "price")["shape"], "money");
1438
+ let tier = deployment_field(&schema, "tier");
1439
+ assert_eq!(tier["shape"], "enum");
1440
+ assert_eq!(tier["field"], "level");
1441
+ }
1442
+
1443
+ #[test]
1444
+ fn an_owner_declared_value_object_is_unchanged_by_the_shared_lookup() {
1445
+ let schema = build(&domain(), &json!({}));
1446
+ assert_eq!(
1447
+ schema["collections"]["clients"]["fields"][1],
1448
+ json!({"key": "name", "label": "Name", "optional": false, "shape": "text_value", "field": "value"})
1449
+ );
1450
+ }
1451
+
1452
+ #[test]
1453
+ fn a_name_two_aggregates_declare_resolves_to_the_owner_then_the_first_declared() {
1454
+ // The owner's own `SiteKey` (attribute `text`) shadows Site's.
1455
+ let own = json!([{"name": "SiteKey", "attributes": [{"name": "text", "type": "String"}], "closed_set": false, "members": []}]);
1456
+ let schema = build(&shared_domain(own), &json!({}));
1457
+ assert_eq!(deployment_field(&schema, "site_key")["field"], "text");
1458
+
1459
+ // Two other aggregates declaring one name: the earlier one wins.
1460
+ let mut domain = shared_domain(json!([]));
1461
+ domain["aggregates"].as_array_mut().unwrap().push(json!({
1462
+ "name": "Probe", "identified_by": ["id"],
1463
+ "attributes": [{"name": "site_key", "type": "SiteKey", "list": false, "optional": false}],
1464
+ "value_objects": [{"name": "SiteKey", "attributes": [{"name": "text", "type": "String"}], "closed_set": false, "members": []}],
1465
+ "entities": [], "commands": [], "queries": []
1466
+ }));
1467
+ domain["aggregates"].as_array_mut().unwrap().push(json!({
1468
+ "name": "Other", "identified_by": ["id"],
1469
+ "attributes": [{"name": "site_key", "type": "SiteKey", "list": false, "optional": false}],
1470
+ "value_objects": [], "entities": [], "commands": [], "queries": []
1471
+ }));
1472
+ let schema = build(&domain, &json!({}));
1473
+ // Site precedes Probe in declaration order, so Other reads Site's.
1474
+ assert_eq!(schema["collections"]["others"]["fields"][0]["field"], "value");
1475
+ // Probe's own declaration still wins for Probe.
1476
+ assert_eq!(schema["collections"]["probes"]["fields"][0]["field"], "text");
1477
+ }
1478
+ }
@@ -106,7 +106,7 @@ async fn newsletter_subscribe_route(
106
106
  let Some((_, subscriber)) = subscribers.iter().find(|(id, _)| id == email) else {
107
107
  return respond(500, "text/plain", "subscriber vanished immediately after being written");
108
108
  };
109
- let status = subscriber.get("status").and_then(|v| v.as_str()).unwrap_or("pending");
109
+ let status = subscriber.get("status").and_then(|v| v.as_str()).unwrap_or(provider.initial_status());
110
110
  respond(200, "application/json", &json!({"email": email, "status": status}).to_string())
111
111
  }
112
112
 
@@ -134,7 +134,6 @@ async fn newsletter_subscribers_list_route(provider: &NewsletterProvider, client
134
134
  }
135
135
 
136
136
  const CONFIRM_PURPOSE: &str = "newsletter-confirm";
137
- const CONFIRM_TTL_SECS: u64 = 14 * 24 * 60 * 60;
138
137
 
139
138
  // SESSION_SECRET; unset means no confirm link can be signed and none is sent.
140
139
  fn confirm_secret() -> Option<String> {
@@ -142,7 +141,7 @@ fn confirm_secret() -> Option<String> {
142
141
  }
143
142
 
144
143
  fn confirm_token(secret: &str, email: &str) -> String {
145
- auth::purpose_token(secret, CONFIRM_PURPOSE, json!({ "email": email }), CONFIRM_TTL_SECS)
144
+ auth::purpose_token(secret, CONFIRM_PURPOSE, json!({ "email": email }), crate::commerce_ir::newsletter_windows_binding().0)
146
145
  }
147
146
 
148
147
  fn confirm_token_matches(secret: &str, token: &str, email: &str) -> bool {
@@ -219,7 +218,7 @@ pub(super) async fn send_confirmation_if_pending(email: &str, client: &Mutex<Cli
219
218
  };
220
219
  let pending = instances_for(&read, &provider.instance_prefix())
221
220
  .iter()
222
- .any(|(id, subscriber)| id == email && subscriber.get("status").and_then(|v| v.as_str()) == Some("pending"));
221
+ .any(|(id, subscriber)| id == email && provider.is_awaiting_confirmation(subscriber.get("status").and_then(|v| v.as_str())));
223
222
  if pending {
224
223
  send_confirmation(email).await;
225
224
  }
@@ -250,7 +249,7 @@ async fn newsletter_confirm_route(provider: &NewsletterProvider, query: &HashMap
250
249
  return respond(404, "application/json", &json!({"error": "no such subscriber"}).to_string());
251
250
  };
252
251
 
253
- if subscriber.get("status").and_then(|v| v.as_str()) == Some("pending") {
252
+ if provider.is_awaiting_confirmation(subscriber.get("status").and_then(|v| v.as_str())) {
254
253
  if let Err(e) = dispatch::handle_routed(client, wasm_path, &provider.confirm, json!(email), json!({}), None, config, invoker).await {
255
254
  return respond(500, "text/plain", &format!("{e:#}"));
256
255
  }
@@ -261,7 +260,7 @@ async fn newsletter_confirm_route(provider: &NewsletterProvider, query: &HashMap
261
260
  Err(e) => return respond(500, "text/plain", &format!("{e:#}")),
262
261
  };
263
262
  let subscribers = instances_for(&read, &provider.instance_prefix());
264
- let status = subscribers.iter().find(|(id, _)| id == email).and_then(|(_, s)| s.get("status")).and_then(|v| v.as_str()).unwrap_or("pending");
263
+ let status = subscribers.iter().find(|(id, _)| id == email).and_then(|(_, s)| s.get("status")).and_then(|v| v.as_str()).unwrap_or(provider.initial_status());
265
264
  respond(200, "application/json", &json!({"email": email, "status": status}).to_string())
266
265
  }
267
266
 
@@ -300,7 +299,7 @@ async fn newsletter_unsubscribe_route(provider: &NewsletterProvider, query: &Has
300
299
  return respond(404, "application/json", &json!({"error": "no such subscriber"}).to_string());
301
300
  };
302
301
 
303
- if subscriber.get("status").and_then(|v| v.as_str()) != Some("unsubscribed") {
302
+ if !provider.has_left(subscriber.get("status").and_then(|v| v.as_str())) {
304
303
  if let Err(e) = dispatch::handle_routed(client, wasm_path, &provider.unsubscribe, json!(email), json!({}), None, config, invoker).await {
305
304
  return respond(500, "text/plain", &format!("{e:#}"));
306
305
  }
@@ -311,6 +310,6 @@ async fn newsletter_unsubscribe_route(provider: &NewsletterProvider, query: &Has
311
310
  Err(e) => return respond(500, "text/plain", &format!("{e:#}")),
312
311
  };
313
312
  let subscribers = instances_for(&read, &provider.instance_prefix());
314
- let status = subscribers.iter().find(|(id, _)| id == email).and_then(|(_, s)| s.get("status")).and_then(|v| v.as_str()).unwrap_or("pending");
313
+ let status = subscribers.iter().find(|(id, _)| id == email).and_then(|(_, s)| s.get("status")).and_then(|v| v.as_str()).unwrap_or(provider.initial_status());
315
314
  respond(200, "application/json", &json!({"email": email, "status": status}).to_string())
316
315
  }
@@ -18,10 +18,10 @@ const UNSUBSCRIBE_TOKEN: &str = "{{UNSUBSCRIBE_URL}}";
18
18
  const DEFAULT_SITE_URL: &str = "http://localhost:4321";
19
19
 
20
20
  // The purpose keys an unsubscribe token apart from a confirm token, so neither
21
- // verifies as the other. The lifetime is long (two years) because the link sits
22
- // in emails that are opened months after they were sent.
21
+ // verifies as the other. The lifetime is the Newsletter bluebook's
22
+ // `unsubscribe_window` (long, because the link sits in emails opened months after
23
+ // they were sent), not a number held here.
23
24
  pub(super) const UNSUBSCRIBE_PURPOSE: &str = "newsletter-unsubscribe";
24
- const UNSUBSCRIBE_TTL_SECS: u64 = 730 * 24 * 60 * 60;
25
25
 
26
26
  #[derive(Debug, PartialEq)]
27
27
  pub(super) enum IssueAction {
@@ -116,7 +116,7 @@ pub(super) fn site_url() -> String {
116
116
  }
117
117
 
118
118
  pub(super) fn unsubscribe_token(secret: &str, email: &str) -> String {
119
- auth::purpose_token(secret, UNSUBSCRIBE_PURPOSE, json!({ "email": email }), UNSUBSCRIBE_TTL_SECS)
119
+ auth::purpose_token(secret, UNSUBSCRIBE_PURPOSE, json!({ "email": email }), crate::commerce_ir::newsletter_windows_binding().1)
120
120
  }
121
121
 
122
122
  pub(super) fn unsubscribe_token_matches(secret: &str, token: &str, email: &str) -> bool {
@@ -141,11 +141,11 @@ fn value_of<'a>(state: &'a Value, field: &str) -> &'a str {
141
141
  state.get(field).and_then(|v| v.get("value")).and_then(|v| v.as_str()).unwrap_or("")
142
142
  }
143
143
 
144
- // Every confirmed subscriber's address, alphabetical so a send is repeatable.
145
- fn confirmed_emails(subscribers: &[(String, Value)]) -> Vec<String> {
144
+ // Every address in a state that receives issues (the lifecycle's `receives_issues` mark), alphabetical so a send is repeatable.
145
+ fn confirmed_emails(provider: &NewsletterProvider, subscribers: &[(String, Value)]) -> Vec<String> {
146
146
  let mut emails: Vec<String> = subscribers
147
147
  .iter()
148
- .filter(|(_, s)| s.get("status").and_then(|v| v.as_str()) == Some("confirmed"))
148
+ .filter(|(_, s)| provider.receives_issues(s.get("status").and_then(|v| v.as_str())))
149
149
  .map(|(email, _)| email.clone())
150
150
  .collect();
151
151
  emails.sort();
@@ -177,7 +177,7 @@ async fn send_issue(
177
177
  };
178
178
  let subject = value_of(&issue, "subject").to_string();
179
179
  let body = value_of(&issue, "body").to_string();
180
- let recipients = confirmed_emails(&instances_for(&read, &subscribers.instance_prefix()));
180
+ let recipients = confirmed_emails(subscribers, &instances_for(&read, &subscribers.instance_prefix()));
181
181
 
182
182
  // Marked sent before mailing: a second click then refuses ("only a draft
183
183
  // issue can be sent") instead of mailing everyone twice.
@@ -323,15 +323,40 @@ mod tests {
323
323
  assert!(require_signing_secret("s3cret-value").is_ok());
324
324
  }
325
325
 
326
- #[test]
327
- fn only_confirmed_subscribers_are_recipients_in_alphabetical_order() {
328
- let subscribers = vec![
326
+ fn provider_with(marks: serde_json::Value) -> NewsletterProvider {
327
+ let mut fact = json!({
328
+ "provider": "Newsletter", "subscribe": "N::S.Subscribe", "add_name": "N::S.AddName",
329
+ "confirm": "N::S.Confirm", "unsubscribe": "N::S.Unsubscribe", "aggregate": "N::S"
330
+ });
331
+ for (key, states) in marks.as_object().into_iter().flatten() {
332
+ fact[key] = states.clone();
333
+ }
334
+ newsletter_provider(&json!({ "newsletter": fact })).expect("newsletter")
335
+ }
336
+
337
+ fn rows() -> Vec<(String, Value)> {
338
+ vec![
329
339
  ("zed@example.com".to_string(), json!({ "status": "confirmed" })),
330
340
  ("pending@example.com".to_string(), json!({ "status": "pending" })),
331
341
  ("amy@example.com".to_string(), json!({ "status": "confirmed" })),
342
+ ("vip@example.com".to_string(), json!({ "status": "vip" })),
332
343
  ("gone@example.com".to_string(), json!({ "status": "unsubscribed" })),
333
- ];
334
- assert_eq!(confirmed_emails(&subscribers), vec!["amy@example.com".to_string(), "zed@example.com".to_string()]);
344
+ ]
345
+ }
346
+
347
+ #[test]
348
+ fn legacy_default_sends_to_confirmed_subscribers_in_alphabetical_order() {
349
+ let provider = provider_with(json!({}));
350
+ assert_eq!(confirmed_emails(&provider, &rows()), vec!["amy@example.com".to_string(), "zed@example.com".to_string()]);
351
+ }
352
+
353
+ #[test]
354
+ fn the_declared_receives_issues_mark_selects_the_recipients() {
355
+ let provider = provider_with(json!({ "receives_issues": ["vip", "confirmed"] }));
356
+ assert_eq!(
357
+ confirmed_emails(&provider, &rows()),
358
+ vec!["amy@example.com".to_string(), "vip@example.com".to_string(), "zed@example.com".to_string()]
359
+ );
335
360
  }
336
361
 
337
362
  #[test]