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
@@ -7,10 +7,6 @@ use sha2::Sha256;
7
7
 
8
8
  type HmacSha256 = Hmac<Sha256>;
9
9
 
10
- // docs.stripe.com/webhooks#verify-events — 5 minutes, guards against a
11
- // replayed old payload even against a leaked (not yet rotated) secret.
12
- const TOLERANCE_SECONDS: i64 = 300;
13
-
14
10
  pub struct SignatureError(pub String);
15
11
 
16
12
  impl std::fmt::Display for SignatureError {
@@ -22,7 +18,10 @@ impl std::fmt::Display for SignatureError {
22
18
  // Signs `<timestamp>.<raw body>`, the exact bytes Stripe sent -- re-serializing
23
19
  // the JSON would disagree on whitespace or key order and break every match.
24
20
  // `now` is a parameter, not read internally, so a test can hold time fixed.
25
- pub fn verify_signature(payload: &str, header: &str, secret: &str, now: i64) -> Result<(), SignatureError> {
21
+ // `tolerance_seconds` is the freshness window the Checkout bluebook declares
22
+ // (`WebhookReceipt.tolerance`, read from the IR): it guards against a replayed
23
+ // old payload even against a leaked (not yet rotated) secret.
24
+ pub fn verify_signature(payload: &str, header: &str, secret: &str, now: i64, tolerance_seconds: i64) -> Result<(), SignatureError> {
26
25
  let mut timestamp: Option<i64> = None;
27
26
  let mut signatures: Vec<&str> = Vec::new();
28
27
  for part in header.split(',') {
@@ -39,9 +38,9 @@ pub fn verify_signature(payload: &str, header: &str, secret: &str, now: i64) ->
39
38
  if signatures.is_empty() {
40
39
  return Err(SignatureError("Stripe-Signature header has no v1= signature".to_string()));
41
40
  }
42
- if (now - timestamp).abs() > TOLERANCE_SECONDS {
41
+ if (now - timestamp).abs() > tolerance_seconds {
43
42
  return Err(SignatureError(format!(
44
- "timestamp {timestamp} is outside the {TOLERANCE_SECONDS}s tolerance (now: {now})"
43
+ "timestamp {timestamp} is outside the {tolerance_seconds}s tolerance (now: {now})"
45
44
  )));
46
45
  }
47
46
 
@@ -98,11 +97,19 @@ pub struct EmbeddedSession {
98
97
  pub client_secret: String,
99
98
  }
100
99
 
101
- // 31, not 30: Stripe requires an expiry at least 30 minutes out.
102
- pub const SESSION_HOLD_SECONDS: i64 = 31 * 60;
100
+ // 31, not 30: Stripe requires an expiry at least 30 minutes out. This floor is a
101
+ // fact about the processor, so it lives here in the adapter; the domain declares
102
+ // only how long a session holds its seat (`CheckoutSession.hold`), and a hold
103
+ // shorter than the floor is raised to it.
104
+ pub const STRIPE_MIN_HOLD_SECONDS: i64 = 31 * 60;
105
+
106
+ /// The hold actually sent to Stripe for a domain hold of `hold_seconds`.
107
+ pub fn effective_hold_seconds(hold_seconds: i64) -> i64 {
108
+ hold_seconds.max(STRIPE_MIN_HOLD_SECONDS)
109
+ }
103
110
 
104
- pub fn session_expires_at(now: i64) -> i64 {
105
- now + SESSION_HOLD_SECONDS
111
+ pub fn session_expires_at(now: i64, hold_seconds: i64) -> i64 {
112
+ now + effective_hold_seconds(hold_seconds)
106
113
  }
107
114
 
108
115
  // Errors carry Stripe's message when it refuses, never a secret.
@@ -158,6 +165,8 @@ pub async fn create_checkout_session(
158
165
  mod tests {
159
166
  use super::*;
160
167
 
168
+ const TOLERANCE_SECONDS: i64 = 300;
169
+
161
170
  fn sign(secret: &str, timestamp: i64, payload: &str) -> String {
162
171
  let signed_payload = format!("{timestamp}.{payload}");
163
172
  let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
@@ -195,7 +204,7 @@ mod tests {
195
204
  let now = 1_700_000_000;
196
205
  let header = format!("t={now},v1={}", sign(secret, now, payload));
197
206
 
198
- assert!(verify_signature(payload, &header, secret, now).is_ok());
207
+ assert!(verify_signature(payload, &header, secret, now, TOLERANCE_SECONDS).is_ok());
199
208
  }
200
209
 
201
210
  #[test]
@@ -207,7 +216,7 @@ mod tests {
207
216
  // Same header, different body — exactly what an attacker
208
217
  // intercepting and rewriting the request in transit would send.
209
218
  let tampered = r#"{"type":"checkout.session.expired"}"#;
210
- assert!(verify_signature(tampered, &header, secret, now).is_err());
219
+ assert!(verify_signature(tampered, &header, secret, now, TOLERANCE_SECONDS).is_err());
211
220
  }
212
221
 
213
222
  #[test]
@@ -216,7 +225,7 @@ mod tests {
216
225
  let now = 1_700_000_000;
217
226
  let header = format!("t={now},v1={}", sign("whsec_real", now, payload));
218
227
 
219
- assert!(verify_signature(payload, &header, "whsec_wrong", now).is_err());
228
+ assert!(verify_signature(payload, &header, "whsec_wrong", now, TOLERANCE_SECONDS).is_err());
220
229
  }
221
230
 
222
231
  #[test]
@@ -231,7 +240,14 @@ mod tests {
231
240
  // to be a separate, independent gate rather than folded into
232
241
  // "does the signature verify at all."
233
242
  let much_later = signed_at + TOLERANCE_SECONDS + 1;
234
- assert!(verify_signature(payload, &header, secret, much_later).is_err());
243
+ assert!(verify_signature(payload, &header, secret, much_later, TOLERANCE_SECONDS).is_err());
244
+ }
245
+
246
+ #[test]
247
+ fn a_hold_shorter_than_stripes_floor_is_raised_to_it() {
248
+ assert_eq!(effective_hold_seconds(1800), STRIPE_MIN_HOLD_SECONDS);
249
+ assert_eq!(effective_hold_seconds(3600), 3600);
250
+ assert_eq!(session_expires_at(1_000, 1800), 1_000 + STRIPE_MIN_HOLD_SECONDS);
235
251
  }
236
252
 
237
253
  #[test]
@@ -246,20 +262,20 @@ mod tests {
246
262
  sign(new_secret, now, payload)
247
263
  );
248
264
 
249
- assert!(verify_signature(payload, &header, old_secret, now).is_ok());
250
- assert!(verify_signature(payload, &header, new_secret, now).is_ok());
265
+ assert!(verify_signature(payload, &header, old_secret, now, TOLERANCE_SECONDS).is_ok());
266
+ assert!(verify_signature(payload, &header, new_secret, now, TOLERANCE_SECONDS).is_ok());
251
267
  }
252
268
 
253
269
  #[test]
254
270
  fn a_header_with_no_v1_at_all_is_rejected() {
255
271
  let now = 1_700_000_000;
256
272
  let header = format!("t={now}");
257
- assert!(verify_signature("{}", &header, "whsec_test", now).is_err());
273
+ assert!(verify_signature("{}", &header, "whsec_test", now, TOLERANCE_SECONDS).is_err());
258
274
  }
259
275
 
260
276
  #[test]
261
277
  fn a_header_with_no_timestamp_at_all_is_rejected() {
262
278
  let header = "v1=deadbeef".to_string();
263
- assert!(verify_signature("{}", &header, "whsec_test", 1_700_000_000).is_err());
279
+ assert!(verify_signature("{}", &header, "whsec_test", 1_700_000_000, TOLERANCE_SECONDS).is_err());
264
280
  }
265
281
  }
@@ -20,9 +20,152 @@ pub struct NewsletterProvider {
20
20
  pub unsubscribe: String,
21
21
  /// Qualified subscribing aggregate, e.g. `Newsletter::Subscriber`.
22
22
  pub aggregate: String,
23
+ /// Subscriber states awaiting the emailed confirm link: the
24
+ /// `awaiting_confirmation` list of the `newsletter` fact (the lifecycle's
25
+ /// mark), or [`LEGACY_AWAITING_CONFIRMATION`] when the chapter declares none.
26
+ pub awaiting_confirmation: Vec<String>,
27
+ /// Subscriber states that receive each issue: the `receives_issues` list,
28
+ /// or [`LEGACY_RECEIVES_ISSUES`] when the chapter declares none.
29
+ pub receives_issues: Vec<String>,
30
+ /// Subscriber states of someone who has left: the `left` list, or
31
+ /// [`LEGACY_LEFT`] when the chapter declares none.
32
+ pub left: Vec<String>,
33
+ /// Seconds the emailed confirm link stays valid: the `confirm_window` of
34
+ /// the `newsletter` fact (ADR 0098), or [`LEGACY_CONFIRM_WINDOW_SECS`].
35
+ pub confirm_window: u64,
36
+ /// Seconds an emailed unsubscribe link stays valid: the `unsubscribe_window`,
37
+ /// or [`LEGACY_UNSUBSCRIBE_WINDOW_SECS`].
38
+ pub unsubscribe_window: u64,
39
+ }
40
+
41
+ /// LEGACY DEFAULT: how long the emailed confirm link lasts (14 days), used
42
+ /// while the `newsletter` fact carries no `confirm_window`, because the
43
+ /// Newsletter chapter predates ADR 0098 and does not declare
44
+ /// `provides "newsletter", ..., confirm_window: "Subscriber.confirm_window"`.
45
+ /// Delete this (and the fallback in `newsletter_provider`) once every shipped
46
+ /// Newsletter bluebook does.
47
+ pub const LEGACY_CONFIRM_WINDOW_SECS: u64 = 14 * 24 * 60 * 60;
48
+
49
+ /// LEGACY DEFAULT: how long an emailed unsubscribe link lasts (730 days), used
50
+ /// while the `newsletter` fact carries no `unsubscribe_window` (see
51
+ /// [`LEGACY_CONFIRM_WINDOW_SECS`]).
52
+ pub const LEGACY_UNSUBSCRIBE_WINDOW_SECS: u64 = 730 * 24 * 60 * 60;
53
+
54
+ /// The whole seconds the fact `fact` lists under `key`: an attribute default
55
+ /// resolved by the exporter (ADR 0098). `None` when the fact omits it.
56
+ fn declared_seconds(fact: &Value, key: &str) -> Option<u64> {
57
+ fact.get(key)?.as_u64().filter(|seconds| *seconds > 0)
58
+ }
59
+
60
+ /// The declared seconds for `key`, else `legacy` with a single warning per process.
61
+ fn seconds_or_legacy(fact: &Value, capability: &str, key: &str, legacy: u64, warned: &std::sync::Once) -> u64 {
62
+ declared_seconds(fact, key).unwrap_or_else(|| {
63
+ warned.call_once(|| eprintln!("{capability} capability declares no {key}; using built-in default"));
64
+ legacy
65
+ })
66
+ }
67
+
68
+ /// The checkout boundary's windows, read from `ir.json`'s own `checkout` key
69
+ /// (ADR 0098). Always present: a window the chapter does not declare takes its
70
+ /// labelled legacy default.
71
+ #[derive(Debug, Clone, Copy, PartialEq)]
72
+ pub struct CheckoutWindows {
73
+ /// Seconds either side of now a signed webhook's timestamp may sit.
74
+ pub webhook_tolerance: i64,
75
+ /// Seconds a checkout session holds its seat (before the processor's own
76
+ /// minimum is applied by the checkout adapter).
77
+ pub session_hold: i64,
78
+ }
79
+
80
+ /// LEGACY DEFAULT: the webhook freshness window (5 minutes), used while the
81
+ /// `checkout` fact carries no `webhook_tolerance`, because no shipped Checkout
82
+ /// bluebook declares `provides "checkout", webhook_tolerance: "WebhookReceipt.tolerance"`.
83
+ /// Delete this (and the fallback in `checkout_windows`) once every one does.
84
+ pub const LEGACY_WEBHOOK_TOLERANCE_SECS: i64 = 300;
85
+
86
+ /// LEGACY DEFAULT: the seat hold of a session (30 minutes), used while the
87
+ /// `checkout` fact carries no `session_hold` (see [`LEGACY_WEBHOOK_TOLERANCE_SECS`]).
88
+ pub const LEGACY_SESSION_HOLD_SECS: i64 = 30 * 60;
89
+
90
+ /// The checkout windows `domain_ir` declares, each falling back to its legacy default.
91
+ pub fn checkout_windows(domain_ir: &Value) -> CheckoutWindows {
92
+ static TOLERANCE_WARNED: std::sync::Once = std::sync::Once::new();
93
+ static HOLD_WARNED: std::sync::Once = std::sync::Once::new();
94
+ let fact = domain_ir.get("checkout").unwrap_or(&Value::Null);
95
+ let seconds = |key, legacy: i64, warned| seconds_or_legacy(fact, "checkout", key, legacy as u64, warned) as i64;
96
+ CheckoutWindows {
97
+ webhook_tolerance: seconds("webhook_tolerance", LEGACY_WEBHOOK_TOLERANCE_SECS, &TOLERANCE_WARNED),
98
+ session_hold: seconds("session_hold", LEGACY_SESSION_HOLD_SECS, &HOLD_WARNED),
99
+ }
100
+ }
101
+
102
+ /// The checkout windows of the IR this host loaded (legacy defaults without one).
103
+ pub fn checkout_windows_binding() -> CheckoutWindows {
104
+ checkout_windows(ir().unwrap_or(&Value::Null))
105
+ }
106
+
107
+ /// The confirm-link and unsubscribe-link windows of the IR this host loaded:
108
+ /// `(confirm, unsubscribe)` seconds, legacy defaults without a `newsletter` fact.
109
+ pub fn newsletter_windows_binding() -> (u64, u64) {
110
+ ir().and_then(newsletter_provider)
111
+ .map(|p| (p.confirm_window, p.unsubscribe_window))
112
+ .unwrap_or((LEGACY_CONFIRM_WINDOW_SECS, LEGACY_UNSUBSCRIBE_WINDOW_SECS))
113
+ }
114
+
115
+ /// LEGACY DEFAULT: the states a new subscriber waits in, used while the
116
+ /// `newsletter` fact carries no `awaiting_confirmation` list, because the
117
+ /// Newsletter chapter predates the marks and does not declare
118
+ /// `provides "newsletter", ..., awaiting_confirmation: "Subscriber.awaiting_confirmation"`.
119
+ /// Delete this (and the fallback in `newsletter_provider`) once every shipped
120
+ /// Newsletter bluebook does.
121
+ pub const LEGACY_AWAITING_CONFIRMATION: [&str; 1] = ["pending"];
122
+
123
+ /// LEGACY DEFAULT: the states that receive an issue, used while the
124
+ /// `newsletter` fact carries no `receives_issues` list (see
125
+ /// [`LEGACY_AWAITING_CONFIRMATION`]).
126
+ pub const LEGACY_RECEIVES_ISSUES: [&str; 1] = ["confirmed"];
127
+
128
+ /// LEGACY DEFAULT: the states of a subscriber who has left, used while the
129
+ /// `newsletter` fact carries no `left` list (see [`LEGACY_AWAITING_CONFIRMATION`]).
130
+ pub const LEGACY_LEFT: [&str; 1] = ["unsubscribed"];
131
+
132
+ /// The states the `newsletter` fact lists under `key`: a lifecycle mark
133
+ /// resolved by the exporter. `None` when the fact omits it.
134
+ fn declared_states(fact: &Value, key: &str) -> Option<Vec<String>> {
135
+ let states = fact.get(key)?.as_array()?;
136
+ Some(states.iter().filter_map(|s| s.as_str().map(String::from)).collect())
137
+ }
138
+
139
+ /// The declared states for `key`, else `legacy` with a single warning per process.
140
+ fn states_or_legacy(fact: &Value, key: &str, legacy: &[&str], warned: &std::sync::Once) -> Vec<String> {
141
+ declared_states(fact, key).unwrap_or_else(|| {
142
+ warned.call_once(|| eprintln!("newsletter capability declares no {key}; using built-in default"));
143
+ legacy.iter().map(|s| s.to_string()).collect()
144
+ })
23
145
  }
24
146
 
25
147
  impl NewsletterProvider {
148
+ /// Whether `status` is a state awaiting confirmation.
149
+ pub fn is_awaiting_confirmation(&self, status: Option<&str>) -> bool {
150
+ status.is_some_and(|s| self.awaiting_confirmation.iter().any(|m| m == s))
151
+ }
152
+
153
+ /// Whether `status` is a state that receives issues.
154
+ pub fn receives_issues(&self, status: Option<&str>) -> bool {
155
+ status.is_some_and(|s| self.receives_issues.iter().any(|m| m == s))
156
+ }
157
+
158
+ /// Whether `status` is a state of a subscriber who has left.
159
+ pub fn has_left(&self, status: Option<&str>) -> bool {
160
+ status.is_some_and(|s| self.left.iter().any(|m| m == s))
161
+ }
162
+
163
+ /// The state a subscriber is reported in when none is recorded yet: the
164
+ /// first awaiting-confirmation state.
165
+ pub fn initial_status(&self) -> &str {
166
+ self.awaiting_confirmation.first().map(String::as_str).unwrap_or("")
167
+ }
168
+
26
169
  /// The prefix every subscriber's key in a `dispatch::read` `instances`
27
170
  /// map starts with, e.g. `Newsletter::Subscriber#`.
28
171
  pub fn instance_prefix(&self) -> String {
@@ -49,8 +192,18 @@ pub fn command_declares(domain_ir: &Value, aggregate: &str, command: &str, field
49
192
  }
50
193
 
51
194
  pub fn newsletter_provider(domain_ir: &Value) -> Option<NewsletterProvider> {
195
+ static AWAITING_WARNED: std::sync::Once = std::sync::Once::new();
196
+ static RECEIVES_WARNED: std::sync::Once = std::sync::Once::new();
197
+ static LEFT_WARNED: std::sync::Once = std::sync::Once::new();
198
+ static CONFIRM_WINDOW_WARNED: std::sync::Once = std::sync::Once::new();
199
+ static UNSUBSCRIBE_WINDOW_WARNED: std::sync::Once = std::sync::Once::new();
52
200
  let fact = domain_ir.get("newsletter")?;
53
201
  Some(NewsletterProvider {
202
+ confirm_window: seconds_or_legacy(fact, "newsletter", "confirm_window", LEGACY_CONFIRM_WINDOW_SECS, &CONFIRM_WINDOW_WARNED),
203
+ unsubscribe_window: seconds_or_legacy(fact, "newsletter", "unsubscribe_window", LEGACY_UNSUBSCRIBE_WINDOW_SECS, &UNSUBSCRIBE_WINDOW_WARNED),
204
+ awaiting_confirmation: states_or_legacy(fact, "awaiting_confirmation", &LEGACY_AWAITING_CONFIRMATION, &AWAITING_WARNED),
205
+ receives_issues: states_or_legacy(fact, "receives_issues", &LEGACY_RECEIVES_ISSUES, &RECEIVES_WARNED),
206
+ left: states_or_legacy(fact, "left", &LEGACY_LEFT, &LEFT_WARNED),
54
207
  provider: fact.get("provider")?.as_str()?.to_string(),
55
208
  subscribe: fact.get("subscribe")?.as_str()?.to_string(),
56
209
  add_name: fact.get("add_name")?.as_str()?.to_string(),
@@ -108,6 +261,49 @@ pub struct PaymentsProvider {
108
261
  pub failed: String,
109
262
  /// Qualified paying aggregate, e.g. `Payments::Payment`.
110
263
  pub aggregate: String,
264
+ /// Payment states whose registration still holds a seat: the `holds_seat`
265
+ /// list of the `payments` fact (the payment lifecycle's mark), or
266
+ /// [`LEGACY_HOLDS_SEAT`] when the chapter declares none.
267
+ pub holds_seat: Vec<String>,
268
+ /// The failure reason a lapsed checkout hold records: the `lapse_reason` of
269
+ /// the `payments` fact (ADR 0099), or [`LEGACY_LAPSE_REASON`] when the
270
+ /// chapter declares none.
271
+ pub lapse_reason: String,
272
+ }
273
+
274
+ /// LEGACY DEFAULT: the failure reason a lapsed hold records, used while the
275
+ /// `payments` capability fact carries no `lapse_reason` because the Payments
276
+ /// chapter predates `provides "payments", lapse_reason: "Payment.lapse_reason"`.
277
+ /// Delete this (and the fallback in `payments_provider`) once every shipped
278
+ /// Payments bluebook declares it.
279
+ pub const LEGACY_LAPSE_REASON: &str = "checkout_expired";
280
+
281
+ /// LEGACY DEFAULT: the registration attributes guessed to hold the timestamp, in
282
+ /// order, used while the `registrations` fact carries no `registered_at` because
283
+ /// the chapter predates `provides "registrations", registered_at: "Registration.requested_at"`.
284
+ /// Delete this (and the fallback in `registrations_provider`) once every shipped
285
+ /// bluebook declares it.
286
+ pub const LEGACY_REGISTERED_AT: [&str; 4] = ["created_at", "registered_at", "requested_at", "occurred_at"];
287
+
288
+ /// LEGACY DEFAULT: the seat-holding payment states used while the `payments`
289
+ /// capability fact carries no `holds_seat` list, because the Payments chapter
290
+ /// predates `mark :holds_seat` and does not declare
291
+ /// `provides "payments", holds_seat: "Payment.holds_seat"`. Delete this (and the
292
+ /// fallback in `payments_provider`) once every shipped Payments bluebook does.
293
+ pub const LEGACY_HOLDS_SEAT: [&str; 4] = ["pending", "succeeded", "refunding", "disputed"];
294
+
295
+ /// LEGACY DEFAULT: the registration state that gives its seat back, used while the
296
+ /// `registrations` capability fact carries no `frees_seat` list, because the chapter
297
+ /// predates `mark :frees_seat` and does not declare
298
+ /// `provides "registrations", frees_seat: "Registration.frees_seat"`. Delete this (and the
299
+ /// fallback in `registrations_provider`) once every shipped bluebook declares it.
300
+ pub const LEGACY_FREES_SEAT: [&str; 1] = ["archived"];
301
+
302
+ /// The states the `payments` fact lists under `holds_seat`: the aggregate's
303
+ /// lifecycle mark, resolved by the exporter. `None` when the fact omits it.
304
+ fn declared_holds_seat(fact: &Value) -> Option<Vec<String>> {
305
+ let states = fact.get("holds_seat")?.as_array()?;
306
+ Some(states.iter().filter_map(|s| s.as_str().map(String::from)).collect())
111
307
  }
112
308
 
113
309
  impl PaymentsProvider {
@@ -120,12 +316,24 @@ impl PaymentsProvider {
120
316
 
121
317
  pub fn payments_provider(domain_ir: &Value) -> Option<PaymentsProvider> {
122
318
  let fact = domain_ir.get("payments")?;
319
+ let aggregate = fact.get("aggregate")?.as_str()?.to_string();
320
+ let holds_seat = declared_holds_seat(fact).unwrap_or_else(|| {
321
+ static WARNED: std::sync::Once = std::sync::Once::new();
322
+ WARNED.call_once(|| eprintln!("payments capability declares no holds_seat; using built-in default"));
323
+ LEGACY_HOLDS_SEAT.iter().map(|s| s.to_string()).collect()
324
+ });
123
325
  Some(PaymentsProvider {
124
326
  provider: fact.get("provider")?.as_str()?.to_string(),
125
327
  initiate: fact.get("initiate")?.as_str()?.to_string(),
126
328
  succeeded: fact.get("succeeded")?.as_str()?.to_string(),
127
329
  failed: fact.get("failed")?.as_str()?.to_string(),
128
- aggregate: fact.get("aggregate")?.as_str()?.to_string(),
330
+ aggregate,
331
+ holds_seat,
332
+ lapse_reason: fact.get("lapse_reason").and_then(|v| v.as_str()).filter(|v| !v.is_empty()).map(String::from).unwrap_or_else(|| {
333
+ static WARNED: std::sync::Once = std::sync::Once::new();
334
+ WARNED.call_once(|| eprintln!("payments capability declares no lapse_reason; using built-in default"));
335
+ LEGACY_LAPSE_REASON.to_string()
336
+ }),
129
337
  })
130
338
  }
131
339
 
@@ -143,6 +351,14 @@ pub struct RegistrationsProvider {
143
351
  pub event_aggregate: String,
144
352
  /// Qualified registration aggregate, e.g. `Studio::Registration`.
145
353
  pub registration_aggregate: String,
354
+ /// The registration attributes that may hold when it was asked for, in the order
355
+ /// to try them: the one `registered_at` of the `registrations` fact names
356
+ /// (ADR 0099), or [`LEGACY_REGISTERED_AT`] when the chapter declares none.
357
+ pub timestamp_keys: Vec<String>,
358
+ /// Registration states that give their seat back: the `frees_seat` list of the
359
+ /// `registrations` fact (the registration lifecycle's mark), or [`LEGACY_FREES_SEAT`]
360
+ /// when the chapter declares none.
361
+ pub frees_seat: Vec<String>,
146
362
  }
147
363
 
148
364
  impl RegistrationsProvider {
@@ -155,6 +371,8 @@ impl RegistrationsProvider {
155
371
  request: format!("{domain}::Registration.Request"),
156
372
  event_aggregate: format!("{domain}::Event"),
157
373
  registration_aggregate: format!("{domain}::Registration"),
374
+ timestamp_keys: LEGACY_REGISTERED_AT.iter().map(|k| k.to_string()).collect(),
375
+ frees_seat: LEGACY_FREES_SEAT.iter().map(|s| s.to_string()).collect(),
158
376
  }
159
377
  }
160
378
 
@@ -177,14 +395,35 @@ impl RegistrationsProvider {
177
395
  }
178
396
  }
179
397
 
398
+ /// The states the `registrations` fact lists under `frees_seat`: the aggregate's lifecycle
399
+ /// mark, resolved by the exporter. `None` when the fact omits it.
400
+ fn declared_frees_seat(fact: &Value) -> Option<Vec<String>> {
401
+ let states = fact.get("frees_seat")?.as_array()?;
402
+ Some(states.iter().filter_map(|s| s.as_str().map(String::from)).collect())
403
+ }
404
+
180
405
  pub fn registrations_provider(domain_ir: &Value) -> Option<RegistrationsProvider> {
181
406
  let fact = domain_ir.get("registrations")?;
407
+ let frees_seat = declared_frees_seat(fact).unwrap_or_else(|| {
408
+ static WARNED: std::sync::Once = std::sync::Once::new();
409
+ WARNED.call_once(|| eprintln!("registrations capability declares no frees_seat; using built-in default"));
410
+ LEGACY_FREES_SEAT.iter().map(|s| s.to_string()).collect()
411
+ });
182
412
  Some(RegistrationsProvider {
183
413
  provider: fact.get("provider")?.as_str()?.to_string(),
184
414
  schedule: fact.get("schedule")?.as_str()?.to_string(),
185
415
  request: fact.get("request")?.as_str()?.to_string(),
186
416
  event_aggregate: fact.get("event_aggregate")?.as_str()?.to_string(),
187
417
  registration_aggregate: fact.get("registration_aggregate")?.as_str()?.to_string(),
418
+ timestamp_keys: match fact.get("registered_at").and_then(|v| v.as_str()).filter(|v| !v.is_empty()) {
419
+ Some(declared) => vec![declared.to_string()],
420
+ None => {
421
+ static WARNED: std::sync::Once = std::sync::Once::new();
422
+ WARNED.call_once(|| eprintln!("registrations capability declares no registered_at; using built-in default"));
423
+ LEGACY_REGISTERED_AT.iter().map(|k| k.to_string()).collect()
424
+ }
425
+ },
426
+ frees_seat,
188
427
  })
189
428
  }
190
429
 
@@ -325,6 +564,84 @@ mod tests {
325
564
  assert!(newsletter_provider(&serde_json::json!({ "name": "Pizzas" })).is_none());
326
565
  }
327
566
 
567
+ fn newsletter_ir(marks: serde_json::Value) -> Value {
568
+ let mut fact = serde_json::json!({
569
+ "provider": "Newsletter",
570
+ "subscribe": "Newsletter::Subscriber.Subscribe",
571
+ "add_name": "Newsletter::Subscriber.AddName",
572
+ "confirm": "Newsletter::Subscriber.Confirm",
573
+ "unsubscribe": "Newsletter::Subscriber.Unsubscribe",
574
+ "aggregate": "Newsletter::Subscriber"
575
+ });
576
+ for (key, states) in marks.as_object().into_iter().flatten() {
577
+ fact[key] = states.clone();
578
+ }
579
+ serde_json::json!({ "name": "Studio", "newsletter": fact })
580
+ }
581
+
582
+ #[test]
583
+ fn newsletter_provider_reads_the_marks_from_the_fact() {
584
+ let ir = newsletter_ir(serde_json::json!({
585
+ "awaiting_confirmation": ["invited"],
586
+ "receives_issues": ["active", "vip"],
587
+ "left": ["gone"]
588
+ }));
589
+ let provider = newsletter_provider(&ir).expect("newsletter");
590
+ assert_eq!(provider.awaiting_confirmation, vec!["invited"]);
591
+ assert_eq!(provider.receives_issues, vec!["active", "vip"]);
592
+ assert_eq!(provider.left, vec!["gone"]);
593
+ assert!(provider.is_awaiting_confirmation(Some("invited")));
594
+ assert!(!provider.is_awaiting_confirmation(Some("pending")));
595
+ assert!(provider.receives_issues(Some("vip")));
596
+ assert!(!provider.receives_issues(Some("confirmed")));
597
+ assert!(provider.has_left(Some("gone")));
598
+ assert_eq!(provider.initial_status(), "invited");
599
+ }
600
+
601
+ #[test]
602
+ fn newsletter_provider_falls_back_to_the_legacy_defaults_without_marks() {
603
+ let provider = newsletter_provider(&newsletter_ir(serde_json::json!({}))).expect("newsletter");
604
+ assert_eq!(provider.awaiting_confirmation, LEGACY_AWAITING_CONFIRMATION);
605
+ assert_eq!(provider.receives_issues, LEGACY_RECEIVES_ISSUES);
606
+ assert_eq!(provider.left, LEGACY_LEFT);
607
+ assert!(provider.is_awaiting_confirmation(Some("pending")));
608
+ assert!(provider.receives_issues(Some("confirmed")));
609
+ assert!(provider.has_left(Some("unsubscribed")));
610
+ assert!(!provider.has_left(None));
611
+ assert_eq!(provider.initial_status(), "pending");
612
+ }
613
+
614
+ #[test]
615
+ fn newsletter_provider_reads_the_link_windows_from_the_fact() {
616
+ let ir = newsletter_ir(serde_json::json!({ "confirm_window": 3600, "unsubscribe_window": 7200 }));
617
+ let provider = newsletter_provider(&ir).expect("newsletter");
618
+ assert_eq!((provider.confirm_window, provider.unsubscribe_window), (3600, 7200));
619
+ }
620
+
621
+ #[test]
622
+ fn newsletter_provider_falls_back_to_the_legacy_windows_without_them() {
623
+ let provider = newsletter_provider(&newsletter_ir(serde_json::json!({}))).expect("newsletter");
624
+ assert_eq!(provider.confirm_window, LEGACY_CONFIRM_WINDOW_SECS);
625
+ assert_eq!(provider.unsubscribe_window, LEGACY_UNSUBSCRIBE_WINDOW_SECS);
626
+ assert_eq!((LEGACY_CONFIRM_WINDOW_SECS, LEGACY_UNSUBSCRIBE_WINDOW_SECS), (1_209_600, 63_072_000));
627
+ }
628
+
629
+ #[test]
630
+ fn checkout_windows_read_the_fact_and_fall_back_per_window() {
631
+ let declared = serde_json::json!({ "checkout": { "provider": "Checkout", "webhook_tolerance": 120, "session_hold": 2400 } });
632
+ assert_eq!(checkout_windows(&declared), CheckoutWindows { webhook_tolerance: 120, session_hold: 2400 });
633
+ let partial = serde_json::json!({ "checkout": { "provider": "Checkout", "session_hold": 2400 } });
634
+ assert_eq!(checkout_windows(&partial), CheckoutWindows { webhook_tolerance: LEGACY_WEBHOOK_TOLERANCE_SECS, session_hold: 2400 });
635
+ let none = checkout_windows(&serde_json::json!({ "name": "Pizzas" }));
636
+ assert_eq!(none, CheckoutWindows { webhook_tolerance: 300, session_hold: 1800 });
637
+ }
638
+
639
+ #[test]
640
+ fn a_zero_or_non_numeric_window_is_not_a_declaration() {
641
+ let ir = serde_json::json!({ "checkout": { "webhook_tolerance": 0, "session_hold": "soon" } });
642
+ assert_eq!(checkout_windows(&ir), CheckoutWindows { webhook_tolerance: 300, session_hold: 1800 });
643
+ }
644
+
328
645
  #[test]
329
646
  fn newsletter_issues_provider_reads_the_declared_capability() {
330
647
  let ir = serde_json::json!({
@@ -365,6 +682,39 @@ mod tests {
365
682
  assert!(payments_provider(&serde_json::json!({ "name": "Pizzas" })).is_none());
366
683
  }
367
684
 
685
+ fn payments_ir(holds_seat: Option<serde_json::Value>) -> Value {
686
+ let mut fact = serde_json::json!({
687
+ "provider": "Payments", "initiate": "Payments::Payment.Initiate",
688
+ "succeeded": "Payments::Payment.PaymentGateway.Succeeded",
689
+ "failed": "Payments::Payment.PaymentGateway.Failed", "aggregate": "Payments::Payment"
690
+ });
691
+ if let Some(states) = holds_seat {
692
+ fact["holds_seat"] = states;
693
+ }
694
+ serde_json::json!({"name": "Studio", "payments": fact})
695
+ }
696
+
697
+ #[test]
698
+ fn payments_provider_reads_holds_seat_from_the_fact() {
699
+ let ir = payments_ir(Some(serde_json::json!(["pending", "paid"])));
700
+ assert_eq!(declared_holds_seat(&ir["payments"]), Some(vec!["pending".to_string(), "paid".to_string()]));
701
+ assert_eq!(payments_provider(&ir).expect("payments").holds_seat, vec!["pending", "paid"]);
702
+ }
703
+
704
+ #[test]
705
+ fn the_checkout_fixture_ir_carries_holds_seat_with_no_fallback() {
706
+ let ir = fixture_ir();
707
+ assert_eq!(declared_holds_seat(&ir["payments"]), Some(vec!["pending".to_string(), "succeeded".to_string()]));
708
+ assert_eq!(fixture_payments().holds_seat, vec!["pending", "succeeded"]);
709
+ }
710
+
711
+ #[test]
712
+ fn payments_provider_falls_back_to_the_legacy_default_without_holds_seat() {
713
+ assert_eq!(declared_holds_seat(&payments_ir(None)["payments"]), None);
714
+ let held = payments_provider(&payments_ir(None)).expect("payments").holds_seat;
715
+ assert_eq!(held, LEGACY_HOLDS_SEAT);
716
+ }
717
+
368
718
  #[test]
369
719
  fn registrations_provider_reads_the_declared_capability() {
370
720
  let provider = registrations_provider(&fixture_ir()).expect("the checkout fixture provides registrations");
@@ -377,7 +727,63 @@ mod tests {
377
727
 
378
728
  #[test]
379
729
  fn the_conventional_registrations_names_match_what_the_fixture_declares() {
380
- assert_eq!(registrations_provider(&fixture_ir()), Some(RegistrationsProvider::conventional("CheckoutFixture")));
730
+ let declared = registrations_provider(&fixture_ir()).expect("registrations");
731
+ let conventional = RegistrationsProvider {
732
+ timestamp_keys: declared.timestamp_keys.clone(),
733
+ frees_seat: declared.frees_seat.clone(),
734
+ ..RegistrationsProvider::conventional("CheckoutFixture") };
735
+ assert_eq!(declared, conventional);
736
+ }
737
+
738
+ #[test]
739
+ fn the_registration_timestamp_is_the_declared_attribute_or_the_legacy_guess() {
740
+ assert_eq!(registrations_provider(&fixture_ir()).expect("registrations").timestamp_keys, vec!["requested_at"]);
741
+ let undeclared = serde_json::json!({"registrations": {
742
+ "provider": "B", "schedule": "B::E.S", "request": "B::R.Q",
743
+ "event_aggregate": "B::E", "registration_aggregate": "B::R"
744
+ }});
745
+ assert_eq!(registrations_provider(&undeclared).expect("registrations").timestamp_keys, LEGACY_REGISTERED_AT);
746
+ }
747
+
748
+ fn registrations_ir(frees_seat: Option<serde_json::Value>) -> Value {
749
+ let mut fact = serde_json::json!({
750
+ "provider": "B", "schedule": "B::E.S", "request": "B::R.Q",
751
+ "event_aggregate": "B::E", "registration_aggregate": "B::R"
752
+ });
753
+ if let Some(states) = frees_seat {
754
+ fact["frees_seat"] = states;
755
+ }
756
+ serde_json::json!({"registrations": fact})
757
+ }
758
+
759
+ #[test]
760
+ fn registrations_provider_reads_frees_seat_from_the_fact() {
761
+ let ir = registrations_ir(Some(serde_json::json!(["cancelled", "archived"])));
762
+ assert_eq!(declared_frees_seat(&ir["registrations"]), Some(vec!["cancelled".to_string(), "archived".to_string()]));
763
+ assert_eq!(registrations_provider(&ir).expect("registrations").frees_seat, vec!["cancelled", "archived"]);
764
+ }
765
+
766
+ #[test]
767
+ fn the_checkout_fixture_ir_carries_frees_seat_with_no_fallback() {
768
+ let ir = fixture_ir();
769
+ assert_eq!(declared_frees_seat(&ir["registrations"]), Some(vec!["archived".to_string(), "cancelled".to_string()]));
770
+ assert_eq!(registrations_provider(&ir).expect("registrations").frees_seat, vec!["archived", "cancelled"]);
771
+ }
772
+
773
+ #[test]
774
+ fn registrations_provider_falls_back_to_the_legacy_default_without_frees_seat() {
775
+ assert_eq!(declared_frees_seat(&registrations_ir(None)["registrations"]), None);
776
+ let freed = registrations_provider(&registrations_ir(None)).expect("registrations").frees_seat;
777
+ assert_eq!(freed, LEGACY_FREES_SEAT);
778
+ }
779
+
780
+ #[test]
781
+ fn the_lapse_reason_is_the_declared_word_or_the_legacy_default() {
782
+ assert_eq!(fixture_payments().lapse_reason, "checkout_expired");
783
+ let mut ir = payments_ir(None);
784
+ assert_eq!(payments_provider(&ir).expect("payments").lapse_reason, LEGACY_LAPSE_REASON);
785
+ ir["payments"]["lapse_reason"] = serde_json::json!("hold_lapsed");
786
+ assert_eq!(payments_provider(&ir).expect("payments").lapse_reason, "hold_lapsed");
381
787
  }
382
788
 
383
789
  #[test]