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
@@ -34,17 +34,15 @@ fn payment_processor(read: &Value, payments: &PaymentsProvider, reference: &str)
34
34
  .and_then(|(_, payment)| payment.get("processor").and_then(|p| p.get("value")).and_then(|v| v.as_str()).map(String::from))
35
35
  }
36
36
 
37
- /// Payment statuses whose registration still holds a seat. A registration
38
- /// with no Payment, or one that's archived, holds none regardless of status.
39
- const SEAT_HOLDING_PAYMENT_STATUSES: [&str; 4] = ["pending", "succeeded", "refunding", "disputed"];
40
-
41
- /// The Registration lifecycle state that gives its seat back.
42
- const ARCHIVED_REGISTRATION_STATUS: &str = "archived";
43
-
44
- /// Whether a registration's own lifecycle has archived it. A missing or
45
- /// non-text `status` is active.
46
- fn registration_archived(registration: &Value) -> bool {
47
- registration.get("status").and_then(|s| s.as_str()) == Some(ARCHIVED_REGISTRATION_STATUS)
37
+ // Payment statuses whose registration still holds a seat come from the
38
+ // payment lifecycle's `holds_seat` mark (`PaymentsProvider::holds_seat`). A
39
+ // registration with no Payment, or one in a state that frees its seat, holds none.
40
+
41
+ /// Whether a registration's own lifecycle state is one the chapter declares gives its seat
42
+ /// back (`RegistrationsProvider::frees_seat`). A missing or non-text `status` is active.
43
+ fn registration_frees_seat(registration: &Value, frees_seat: &[String]) -> bool {
44
+ let status = registration.get("status").and_then(|s| s.as_str());
45
+ status.is_some_and(|status| frees_seat.iter().any(|freed| freed == status))
48
46
  }
49
47
 
50
48
  /// A reference to an aggregate reads back as its plain id, or wrapped as
@@ -55,17 +53,24 @@ fn plain_id(raw: Option<&Value>) -> Option<String> {
55
53
  value.as_str().filter(|id| !id.is_empty()).map(String::from)
56
54
  }
57
55
 
58
- /// Seats used per event slug: non-archived Registrations whose Payment (same
56
+ /// Seats used per event slug: Registrations not in a seat-freeing state whose Payment (same
59
57
  /// reference) is in a seat-holding status.
60
58
  fn seat_counts(read: &Value, domain: &str, payments: &PaymentsProvider) -> std::collections::HashMap<String, usize> {
59
+ let binding = crate::commerce_ir::registrations_binding(domain);
60
+ count_seats(read, &binding, payments)
61
+ }
62
+
63
+ /// The seat count of `seat_counts` against an explicit registrations binding.
64
+ fn count_seats(read: &Value, binding: &crate::commerce_ir::RegistrationsProvider, payments: &PaymentsProvider) -> std::collections::HashMap<String, usize> {
61
65
  let statuses: std::collections::HashMap<String, String> = instances_for(read, &payments.instance_prefix())
62
66
  .into_iter()
63
67
  .filter_map(|(id, payment)| payment.get("status").and_then(|s| s.as_str()).map(|s| (id, s.to_string())))
64
68
  .collect();
65
69
  let mut counts = std::collections::HashMap::new();
66
- for (id, registration) in instances_for(read, &crate::commerce_ir::registrations_binding(domain).registration_prefix()) {
67
- let holds = statuses.get(&id).is_some_and(|status| SEAT_HOLDING_PAYMENT_STATUSES.contains(&status.as_str()));
68
- if let (true, false, Some(event_slug)) = (holds, registration_archived(&registration), plain_id(registration.get("event_slug"))) {
70
+ for (id, registration) in instances_for(read, &binding.registration_prefix()) {
71
+ let holds = statuses.get(&id).is_some_and(|status| payments.holds_seat.iter().any(|held| held == status));
72
+ let freed = registration_frees_seat(&registration, &binding.frees_seat);
73
+ if let (true, false, Some(event_slug)) = (holds, freed, plain_id(registration.get("event_slug"))) {
69
74
  *counts.entry(event_slug).or_insert(0) += 1;
70
75
  }
71
76
  }
@@ -82,6 +87,27 @@ fn event_capacity(event: &Value) -> Option<i64> {
82
87
  event.get("capacity").and_then(|c| c.get("value")).and_then(|v| v.as_i64())
83
88
  }
84
89
 
90
+ /// A whole-number field read as a bare number or as a single-field value
91
+ /// object's `{"value": N}`; `None` for anything else, so an unreadable value
92
+ /// never blocks a registration.
93
+ fn whole_number(field: Option<&Value>) -> Option<i64> {
94
+ let field = field?;
95
+ field.as_i64().or_else(|| field.get("value").and_then(|v| v.as_i64()))
96
+ }
97
+
98
+ /// Whether an event's registration window has shut because its start is
99
+ /// nearer than `registration_cutoff_hours` (Unix seconds, whole hours).
100
+ /// Refused only when both `starts_at` and `registration_cutoff_hours` are
101
+ /// readable and `now_secs + hours * 3600 > starts_at`; exactly at the
102
+ /// cutoff is still open. Either field absent, null or unreadable means no
103
+ /// cutoff, the stance the seat check takes.
104
+ fn registration_cutoff_passed(event: &Value, now_secs: i64) -> bool {
105
+ let (Some(starts_at), Some(hours)) = (whole_number(event.get("starts_at")), whole_number(event.get("registration_cutoff_hours"))) else {
106
+ return false;
107
+ };
108
+ now_secs.saturating_add(hours.saturating_mul(3600)) > starts_at
109
+ }
110
+
85
111
  /// Seats still open given `taken`, never below zero.
86
112
  fn open_seats(capacity: i64, taken: usize) -> i64 {
87
113
  (capacity - taken as i64).max(0)
@@ -419,6 +445,22 @@ pub(crate) async fn registrations_route(
419
445
  config: &LineageConfig,
420
446
  invoker: &dyn LambdaInvoker,
421
447
  payments: &PaymentsProvider,
448
+ ) -> Value {
449
+ registrations_route_at(raw_body, platform, client, wasm_path, config, invoker, payments, unix_now()).await
450
+ }
451
+
452
+ // The route with the wall clock passed in, so a test can fix the time the
453
+ // registration cutoff is measured against.
454
+ #[allow(clippy::too_many_arguments)]
455
+ pub(crate) async fn registrations_route_at(
456
+ raw_body: &str,
457
+ platform: &payments::PlatformConfig,
458
+ client: &Mutex<Client>,
459
+ wasm_path: &Path,
460
+ config: &LineageConfig,
461
+ invoker: &dyn LambdaInvoker,
462
+ payments: &PaymentsProvider,
463
+ now_secs: i64,
422
464
  ) -> Value {
423
465
  let body: Value = match serde_json::from_str(raw_body) {
424
466
  Ok(v) => v,
@@ -448,6 +490,11 @@ pub(crate) async fn registrations_route(
448
490
  if event.get("status").and_then(|v| v.as_str()) != Some("open") {
449
491
  return respond(422, "application/json", &json!({"error": "registration is closed for this event"}).to_string());
450
492
  }
493
+ // An event that opted into a cutoff stops taking registrations that many
494
+ // hours before it starts; a missing or unreadable field is no cutoff.
495
+ if registration_cutoff_passed(event, now_secs) {
496
+ return respond(422, "application/json", &json!({"error": "registration has closed for this event"}).to_string());
497
+ }
451
498
  // A full event refuses before any Stripe call or write. An event whose
452
499
  // capacity cannot be read is not blocked: capacity is required when an
453
500
  // event is scheduled, so its absence means nothing to enforce.
@@ -477,7 +524,7 @@ pub(crate) async fn registrations_route(
477
524
  // follows, the unused session simply expires.
478
525
  let embedded_checkout = if let payments::CheckoutPlan::Stripe { api_key, publishable_key } = &plan {
479
526
  let auth = checkout::StripeAuth { api_key, base_url: &platform.api_base };
480
- match checkout::create_checkout_session(&auth, price_cents, event_name, &reference, checkout::session_expires_at(unix_now())).await {
527
+ match checkout::create_checkout_session(&auth, price_cents, event_name, &reference, checkout::session_expires_at(now_secs, crate::commerce_ir::checkout_windows_binding().session_hold)).await {
481
528
  // Stripe.js is opened with the publishable key; the answer carries
482
529
  // no `checkout_url`.
483
530
  Ok(session) => Some(json!({
@@ -611,9 +658,10 @@ pub(crate) async fn webhook_route(
611
658
  return mock_secret_refused();
612
659
  }
613
660
  let candidates = if configured.is_empty() { vec![MOCK_STRIPE_WEBHOOK_SECRET] } else { configured };
661
+ let tolerance = crate::commerce_ir::checkout_windows_binding().webhook_tolerance;
614
662
  let mut verification = Ok(());
615
663
  for secret in &candidates {
616
- verification = checkout::verify_signature(raw_body, signature_header, secret, now);
664
+ verification = checkout::verify_signature(raw_body, signature_header, secret, now, tolerance);
617
665
  if verification.is_ok() {
618
666
  break;
619
667
  }
@@ -671,7 +719,7 @@ pub(crate) async fn webhook_route(
671
719
  }
672
720
  "checkout.session.expired" => Some((payments.failed.as_str(), json!({
673
721
  "reference": reference_fact,
674
- "reason": {"value": "checkout_expired"},
722
+ "reason": {"value": payments.lapse_reason},
675
723
  "reported_processor": reported_processor,
676
724
  }))),
677
725
  _ => None,
@@ -749,7 +797,7 @@ mod tests {
749
797
 
750
798
  #[test]
751
799
  fn a_registration_holds_a_seat_while_its_payment_is_open_paid_or_being_settled() {
752
- let payments = crate::commerce_ir::fixture_payments();
800
+ let payments = four_state_payments();
753
801
  let read = read_with(
754
802
  10,
755
803
  &[("a", "yoga", "pending"), ("b", "yoga", "succeeded"), ("c", "yoga", "refunding"), ("d", "yoga", "disputed")],
@@ -788,6 +836,49 @@ mod tests {
788
836
  assert_eq!(seats_left(&read, "CheckoutFixture", &payments, "yoga"), Some(1));
789
837
  }
790
838
 
839
+ fn binding_freeing(states: &[&str]) -> crate::commerce_ir::RegistrationsProvider {
840
+ let mut binding = crate::commerce_ir::RegistrationsProvider::conventional("CheckoutFixture");
841
+ binding.frees_seat = states.iter().map(|s| s.to_string()).collect();
842
+ binding
843
+ }
844
+
845
+ #[test]
846
+ fn a_declared_state_other_than_archived_frees_its_seat() {
847
+ let payments = crate::commerce_ir::fixture_payments();
848
+ let mut read = read_with(3, &[("a", "yoga", "succeeded"), ("b", "yoga", "succeeded")]);
849
+ set_registration_status(&mut read, "a", "cancelled");
850
+ let counts = count_seats(&read, &binding_freeing(&["cancelled"]), &payments);
851
+ assert_eq!(counts.get("yoga"), Some(&1), "cancelled is declared, so it frees the seat");
852
+ }
853
+
854
+ #[test]
855
+ fn the_fixture_declared_cancelled_state_frees_its_seat_beside_archived() {
856
+ let payments = crate::commerce_ir::fixture_payments();
857
+ let binding = crate::commerce_ir::registrations_provider(&crate::commerce_ir::fixture_ir()).expect("registrations");
858
+ let mut read = read_with(3, &[("a", "yoga", "succeeded"), ("b", "yoga", "succeeded"), ("c", "yoga", "succeeded")]);
859
+ set_registration_status(&mut read, "a", "cancelled");
860
+ set_registration_status(&mut read, "b", "archived");
861
+ assert_eq!(count_seats(&read, &binding, &payments).get("yoga"), Some(&1));
862
+ }
863
+
864
+ #[test]
865
+ fn a_state_the_chapter_does_not_declare_keeps_its_seat_even_when_it_is_archived() {
866
+ let payments = crate::commerce_ir::fixture_payments();
867
+ let mut read = read_with(3, &[("a", "yoga", "succeeded")]);
868
+ set_registration_status(&mut read, "a", "archived");
869
+ let counts = count_seats(&read, &binding_freeing(&["cancelled"]), &payments);
870
+ assert_eq!(counts.get("yoga"), Some(&1), "only the declared states free a seat");
871
+ }
872
+
873
+ #[test]
874
+ fn with_no_declaration_the_conventional_binding_frees_the_archived_seat() {
875
+ let payments = crate::commerce_ir::fixture_payments();
876
+ let mut read = read_with(3, &[("a", "yoga", "succeeded")]);
877
+ set_registration_status(&mut read, "a", "archived");
878
+ let conventional = crate::commerce_ir::RegistrationsProvider::conventional("CheckoutFixture");
879
+ assert_eq!(count_seats(&read, &conventional, &payments).get("yoga"), None);
880
+ }
881
+
791
882
  #[test]
792
883
  fn a_registration_with_no_status_counts_as_active() {
793
884
  let payments = crate::commerce_ir::fixture_payments();
@@ -847,18 +938,22 @@ mod tests {
847
938
  read["instances"][format!("CheckoutFixture::Registration#{reference}")]["status"] = json!(status);
848
939
  }
849
940
 
941
+ /// The fixture's payments binding with the production table: the fixture lifecycle has only
942
+ /// pending, succeeded and failed, so refunding and disputed need the table set here.
943
+ fn four_state_payments() -> crate::commerce_ir::PaymentsProvider {
944
+ let mut payments = crate::commerce_ir::fixture_payments();
945
+ payments.holds_seat = HOLDING.iter().map(|s| s.to_string()).collect();
946
+ payments
947
+ }
948
+
850
949
  #[test]
851
- fn the_seat_holding_table_is_exactly_pending_succeeded_refunding_and_disputed() {
852
- let mut table = SEAT_HOLDING_PAYMENT_STATUSES;
853
- table.sort_unstable();
854
- let mut expected = HOLDING;
855
- expected.sort_unstable();
856
- assert_eq!(table, expected);
950
+ fn the_seat_holding_table_comes_from_the_fixture_ir_mark() {
951
+ assert_eq!(crate::commerce_ir::fixture_payments().holds_seat, vec!["pending", "succeeded"]);
857
952
  }
858
953
 
859
954
  #[test]
860
955
  fn every_holding_status_holds_one_seat_and_every_freeing_status_holds_none() {
861
- let payments = crate::commerce_ir::fixture_payments();
956
+ let payments = four_state_payments();
862
957
  for status in HOLDING {
863
958
  let read = read_with(10, &[("r", "yoga", status)]);
864
959
  assert_eq!(seats_taken(&read, "CheckoutFixture", &payments, "yoga"), 1, "{status} should hold a seat");
@@ -1235,6 +1330,114 @@ mod tests {
1235
1330
  assert!(response["body"].as_str().unwrap().contains("closed"));
1236
1331
  }
1237
1332
 
1333
+ // Schedules `slug` starting at `starts_at` with the optional cutoff set
1334
+ // (or absent), through the real Event.Schedule command.
1335
+ async fn schedule_timed_event(client: &Mutex<Client>, wasm_path: &Path, config: &LineageConfig, slug: &str, starts_at: i64, cutoff_hours: Option<i64>) {
1336
+ let mut args = json!({
1337
+ "slug": {"value": slug}, "name": {"value": "Sample Studio"},
1338
+ "price": {"cents": 4200}, "capacity": {"value": 20},
1339
+ "starts_at": {"value": starts_at},
1340
+ });
1341
+ if let Some(hours) = cutoff_hours {
1342
+ args["registration_cutoff_hours"] = json!({"value": hours});
1343
+ }
1344
+ let outcome = dispatch::handle(client, wasm_path, "CheckoutFixture::Event.Schedule", args, None, config, &lambda_client::NeverInvoker)
1345
+ .await
1346
+ .unwrap();
1347
+ assert!(outcome.accepted, "scheduling the timed fixture event should succeed: {:?}", outcome.result);
1348
+ }
1349
+
1350
+ #[tokio::test]
1351
+ async fn registrations_route_refuses_a_session_inside_its_registration_cutoff_and_writes_nothing() {
1352
+ let client = scratch_db("hecks_host_web_test_registrations_cutoff_passed").await;
1353
+ provision_lineage(&*client.lock().await, "CheckoutFixture", 1, &["Event", "Registration", "Payment"]).await;
1354
+ let config = checkout_config(1);
1355
+ let wasm_path = checkout_wasm_path();
1356
+
1357
+ // Starts in 5 hours with a 6 hour cutoff: registration closed an hour ago.
1358
+ let now = 1_800_000_000;
1359
+ schedule_timed_event(&client, &wasm_path, &config, "too-late", now + 5 * 3600, Some(6)).await;
1360
+
1361
+ let body = json!({"event_slug": "too-late", "name": "Ada", "email": "ada@example.com"}).to_string();
1362
+ let response = registrations_route_at(&body, &payments::test_platform(), &client, &wasm_path, &config, &lambda_client::NeverInvoker, &crate::commerce_ir::fixture_payments(), now).await;
1363
+ assert_eq!(response["statusCode"], 422, "{response:?}");
1364
+ assert_eq!(
1365
+ serde_json::from_str::<Value>(response["body"].as_str().unwrap()).unwrap(),
1366
+ json!({"error": "registration has closed for this event"})
1367
+ );
1368
+
1369
+ let read = dispatch::read(&client, &wasm_path).await.unwrap();
1370
+ let instances = read["instances"].as_object().unwrap();
1371
+ assert!(
1372
+ instances.keys().all(|k| !k.starts_with("Payments::Payment#") && !k.starts_with("CheckoutFixture::Registration#")),
1373
+ "a refused registration must write no Payment and no Registration: {instances:?}"
1374
+ );
1375
+ }
1376
+
1377
+ #[tokio::test]
1378
+ async fn registrations_route_accepts_a_session_with_no_cutoff_even_when_it_has_a_start() {
1379
+ let client = scratch_db("hecks_host_web_test_registrations_cutoff_absent").await;
1380
+ provision_lineage(&*client.lock().await, "CheckoutFixture", 1, &["Event", "Registration", "Payment"]).await;
1381
+ let config = checkout_config(1);
1382
+ let wasm_path = checkout_wasm_path();
1383
+
1384
+ // A start in the past and no cutoff field: nothing to enforce.
1385
+ let now = 1_800_000_000;
1386
+ schedule_timed_event(&client, &wasm_path, &config, "no-cutoff", now - 3600, None).await;
1387
+
1388
+ let body = json!({"event_slug": "no-cutoff", "name": "Ada", "email": "ada@example.com"}).to_string();
1389
+ let response = registrations_route_at(&body, &payments::test_platform(), &client, &wasm_path, &config, &lambda_client::NeverInvoker, &crate::commerce_ir::fixture_payments(), now).await;
1390
+ assert_eq!(response["statusCode"], 200, "{response:?}");
1391
+
1392
+ let read = dispatch::read(&client, &wasm_path).await.unwrap();
1393
+ let instances = read["instances"].as_object().unwrap();
1394
+ assert!(instances.keys().any(|k| k.starts_with("CheckoutFixture::Registration#")), "{instances:?}");
1395
+ assert!(instances.keys().any(|k| k.starts_with("Payments::Payment#")), "{instances:?}");
1396
+ }
1397
+
1398
+ #[test]
1399
+ fn a_registration_cutoff_needs_both_fields_and_ignores_unreadable_ones() {
1400
+ let now = 1_000_000;
1401
+ let past_start = json!({"starts_at": 0});
1402
+ assert!(!registration_cutoff_passed(&json!({}), now));
1403
+ assert!(!registration_cutoff_passed(&past_start, now), "a start with no cutoff hours is no cutoff");
1404
+ assert!(!registration_cutoff_passed(&json!({"registration_cutoff_hours": 48}), now), "hours with no start is no cutoff");
1405
+ assert!(!registration_cutoff_passed(&json!({"starts_at": null, "registration_cutoff_hours": 48}), now));
1406
+ assert!(!registration_cutoff_passed(&json!({"starts_at": 0, "registration_cutoff_hours": null}), now));
1407
+ assert!(!registration_cutoff_passed(&json!({"starts_at": "soon", "registration_cutoff_hours": 48}), now));
1408
+ assert!(!registration_cutoff_passed(&json!({"starts_at": 0, "registration_cutoff_hours": "two"}), now));
1409
+ assert!(!registration_cutoff_passed(&json!({"starts_at": {"value": "soon"}, "registration_cutoff_hours": {"value": 1.5}}), now));
1410
+ }
1411
+
1412
+ #[test]
1413
+ fn a_registration_cutoff_is_open_exactly_at_the_boundary_and_closed_a_second_later() {
1414
+ // Cutoff 2 hours before a start of 100_000: the window shuts after 92_800.
1415
+ let event = json!({"starts_at": 100_000, "registration_cutoff_hours": 2});
1416
+ assert!(!registration_cutoff_passed(&event, 92_799));
1417
+ assert!(!registration_cutoff_passed(&event, 92_800), "exactly at the cutoff is still open");
1418
+ assert!(registration_cutoff_passed(&event, 92_801));
1419
+ }
1420
+
1421
+ #[test]
1422
+ fn a_registration_cutoff_reads_a_value_object_or_a_bare_number() {
1423
+ let wrapped = json!({"starts_at": {"value": 100_000}, "registration_cutoff_hours": {"value": 2}});
1424
+ let bare = json!({"starts_at": 100_000, "registration_cutoff_hours": 2});
1425
+ let mixed = json!({"starts_at": {"value": 100_000}, "registration_cutoff_hours": 2});
1426
+ for event in [wrapped, bare, mixed] {
1427
+ assert!(!registration_cutoff_passed(&event, 92_800), "{event}");
1428
+ assert!(registration_cutoff_passed(&event, 92_801), "{event}");
1429
+ }
1430
+ }
1431
+
1432
+ #[test]
1433
+ fn a_registration_cutoff_refuses_a_start_already_in_the_past() {
1434
+ let event = json!({"starts_at": 1_000, "registration_cutoff_hours": 1});
1435
+ assert!(registration_cutoff_passed(&event, 5_000));
1436
+ let no_hours_of_notice = json!({"starts_at": 1_000, "registration_cutoff_hours": 0});
1437
+ assert!(!registration_cutoff_passed(&no_hours_of_notice, 1_000));
1438
+ assert!(registration_cutoff_passed(&no_hours_of_notice, 1_001));
1439
+ }
1440
+
1238
1441
  #[tokio::test]
1239
1442
  async fn registrations_route_propagates_a_real_domain_refusal_from_payment_initiate() {
1240
1443
  // A zero-price event: PositiveMoney's invariant refuses
data/rust/host/src/web.rs CHANGED
@@ -608,20 +608,20 @@ async fn registrations_list_route(
608
608
  // Built field-by-field from an allowlist so intake answers (health,
609
609
  // medications) and payment fields can never appear here.
610
610
  fn registration_list_rows(read: &Value, domain: &str) -> Vec<Value> {
611
- const TIMESTAMP_KEYS: [&str; 4] = ["created_at", "registered_at", "requested_at", "occurred_at"];
611
+ let binding = crate::commerce_ir::registrations_binding(domain);
612
612
  let plain = |value: Option<&Value>| -> Option<Value> {
613
613
  let value = value?;
614
614
  Some(value.get("value").cloned().unwrap_or_else(|| value.clone()))
615
615
  };
616
616
  let text = |value: Option<&Value>| plain(value).and_then(|v| v.as_str().map(|s| s.trim().to_string()));
617
617
 
618
- let mut rows: Vec<(Option<String>, Value)> = instances_for(read, &crate::commerce_ir::registrations_binding(domain).registration_prefix())
618
+ let mut rows: Vec<(Option<String>, Value)> = instances_for(read, &binding.registration_prefix())
619
619
  .into_iter()
620
620
  .map(|(id, registration)| {
621
621
  let attendee = registration.get("attendee").cloned().unwrap_or_else(|| json!({}));
622
622
  let joined = [text(attendee.get("first_name")), text(attendee.get("last_name"))].into_iter().flatten().collect::<Vec<_>>().join(" ");
623
623
  let name = if joined.is_empty() { text(attendee.get("name")).unwrap_or_default() } else { joined };
624
- let stamp = TIMESTAMP_KEYS.iter().find_map(|key| text(registration.get(*key)));
624
+ let stamp = binding.timestamp_keys.iter().find_map(|key| text(registration.get(key.as_str())));
625
625
  let mut row = json!({
626
626
  "registration_id": id,
627
627
  "email": text(attendee.get("email")),
@@ -411,13 +411,18 @@ fn mutation_source_json(source: &Option<ir::MutationSource>) -> JsonValue {
411
411
  ("kind".to_string(), JsonValue::str("state")),
412
412
  ("name".to_string(), JsonValue::str(name.clone())),
413
413
  ]),
414
+ Some(ir::MutationSource::Expression(text)) => JsonValue::Object(vec![
415
+ ("kind".to_string(), JsonValue::str("expression")),
416
+ ("text".to_string(), JsonValue::str(text.clone())),
417
+ ("ast".to_string(), crate::expr::ast_json::emit_resolver(&crate::expr::resolver::parse(text))),
418
+ ]),
414
419
  None => JsonValue::Null,
415
420
  }
416
421
  }
417
422
 
418
423
  /// `IR::Lifecycle#to_h`; `transitions:` arrives already expanded, one row per `from_state`.
419
424
  fn lifecycle_json(l: &ir::Lifecycle) -> JsonValue {
420
- JsonValue::Object(vec![
425
+ let mut fields = vec![
421
426
  ("field".to_string(), JsonValue::str(l.field.clone())),
422
427
  ("default".to_string(), JsonValue::str(l.default.clone())),
423
428
  (
@@ -435,7 +440,25 @@ fn lifecycle_json(l: &ir::Lifecycle) -> JsonValue {
435
440
  .collect(),
436
441
  ),
437
442
  ),
438
- ])
443
+ ];
444
+ // Omitted when empty, so an unmarked lifecycle's IR is unchanged.
445
+ if !l.marks.is_empty() {
446
+ fields.push((
447
+ "marks".to_string(),
448
+ JsonValue::Object(
449
+ l.marks
450
+ .iter()
451
+ .map(|(name, states)| {
452
+ (
453
+ name.clone(),
454
+ JsonValue::Array(states.iter().map(|s| JsonValue::str(s.clone())).collect()),
455
+ )
456
+ })
457
+ .collect(),
458
+ ),
459
+ ));
460
+ }
461
+ JsonValue::Object(fields)
439
462
  }
440
463
 
441
464
  /// `IR::Query#to_h` merged with `query_options_json`, which appends only the declared
@@ -783,15 +806,24 @@ fn dispatch_spec_json(d: &ir::DispatchSpec) -> JsonValue {
783
806
  ])
784
807
  }
785
808
 
809
+ /// An ask policy carries `ask` where a trigger policy carries `trigger_command`, as
810
+ /// `Behaviour::Policy#to_h` does; a trigger policy has no `ask` key at all.
811
+ fn trigger_or_ask_entry(p: &ir::Policy) -> (String, JsonValue) {
812
+ match &p.ask {
813
+ Some(name) => ("ask".to_string(), JsonValue::str(name.clone())),
814
+ None => (
815
+ "trigger_command".to_string(),
816
+ JsonValue::opt_str(&p.trigger_command),
817
+ ),
818
+ }
819
+ }
820
+
786
821
  /// `Policy#to_h`: attribute declaration order from reaction.bluebook, then `where_ast` last.
787
822
  fn policy_json(p: &ir::Policy) -> JsonValue {
788
823
  JsonValue::Object(vec![
789
824
  ("name".to_string(), JsonValue::str(p.name.clone())),
790
825
  ("on_event".to_string(), JsonValue::opt_str(&p.on_event)),
791
- (
792
- "trigger_command".to_string(),
793
- JsonValue::opt_str(&p.trigger_command),
794
- ),
826
+ trigger_or_ask_entry(p),
795
827
  (
796
828
  "target_domain".to_string(),
797
829
  JsonValue::opt_str(&p.target_domain),
@@ -66,7 +66,7 @@ fn emit_include(haystack: &Resolver, needle: &Resolver) -> JsonValue {
66
66
  .expect("non-empty by the guard above")
67
67
  }
68
68
 
69
- fn emit_resolver(node: &Resolver) -> JsonValue {
69
+ pub fn emit_resolver(node: &Resolver) -> JsonValue {
70
70
  match node {
71
71
  Resolver::IntegerLiteral(v) => obj(vec![op_tag("int"), ("value", JsonValue::Number(v.to_string()))]),
72
72
  Resolver::FloatLiteral(v) => obj(vec![op_tag("float"), ("value", JsonValue::Number(ruby_float(*v)))]),
@@ -78,6 +78,9 @@ fn emit_resolver(node: &Resolver) -> JsonValue {
78
78
  ("path", JsonValue::Array(path.split('.').map(|s| JsonValue::String(s.to_string())).collect())),
79
79
  ]),
80
80
  Resolver::Addition(left, right) => obj(vec![op_tag("add"), ("left", emit_resolver(left)), ("right", emit_resolver(right))]),
81
+ Resolver::Subtraction(left, right) => obj(vec![op_tag("sub"), ("left", emit_resolver(left)), ("right", emit_resolver(right))]),
82
+ Resolver::Multiplication(left, right) => obj(vec![op_tag("mul"), ("left", emit_resolver(left)), ("right", emit_resolver(right))]),
83
+ Resolver::Division(left, right) => obj(vec![op_tag("div"), ("left", emit_resolver(left)), ("right", emit_resolver(right))]),
81
84
  Resolver::SignTest { operator, receiver } => obj(vec![
82
85
  op_tag("sign_test"),
83
86
  ("cmp", emit_comparison(operator)),
@@ -106,6 +109,9 @@ fn emit_resolver(node: &Resolver) -> JsonValue {
106
109
  Resolver::Split { receiver, separator } => {
107
110
  obj(vec![op_tag("split"), ("receiver", emit_resolver(receiver)), ("separator", JsonValue::String(separator.clone()))])
108
111
  }
112
+ Resolver::Strip { receiver, side } => {
113
+ obj(vec![op_tag("strip"), ("receiver", emit_resolver(receiver)), ("side", JsonValue::String((*side).to_string()))])
114
+ }
109
115
  Resolver::StartsWith { receiver, substring } => {
110
116
  obj(vec![op_tag("starts_with"), ("receiver", emit_resolver(receiver)), ("substring", JsonValue::String(substring.clone()))])
111
117
  }