hecks 5.0.0 → 7.0.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 (1687) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/bluebook/pizzas.bluebook +1 -0
  3. data/exe/hecks +31 -12
  4. data/lib/hecks/adapters/driven/claude_code.rb +3 -3
  5. data/lib/hecks/adapters/driven/d1.rb +2 -1
  6. data/lib/hecks/adapters/driven/folder.rb +44 -28
  7. data/lib/hecks/adapters/driven/folder_overlay.rb +66 -0
  8. data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
  9. data/lib/hecks/adapters/driven/google_authentication.rb +99 -0
  10. data/lib/hecks/adapters/driven/governance_authorization.rb +32 -36
  11. data/lib/hecks/adapters/driven/heki/journal.rb +45 -0
  12. data/lib/hecks/adapters/driven/heki.rb +1 -1
  13. data/lib/hecks/adapters/driven/identity_registry.rb +1 -1
  14. data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -2
  15. data/lib/hecks/adapters/driven/in_process_key_vault.rb +1 -1
  16. data/lib/hecks/adapters/driven/key_keeper.rb +42 -0
  17. data/lib/hecks/adapters/driven/lambda/client.rb +8 -2
  18. data/lib/hecks/adapters/driven/lambda/remote_dispatcher.rb +185 -0
  19. data/lib/hecks/adapters/driven/lambda.rb +14 -2
  20. data/lib/hecks/adapters/driven/memory/dispatch_claims.rb +25 -0
  21. data/lib/hecks/adapters/driven/memory/outbox_rows.rb +2 -2
  22. data/lib/hecks/adapters/driven/memory.rb +15 -4
  23. data/lib/hecks/adapters/driven/postgres/codec.rb +1 -1
  24. data/lib/hecks/adapters/driven/postgres/journal.rb +46 -0
  25. data/lib/hecks/adapters/driven/postgres/name_guard.rb +25 -0
  26. data/lib/hecks/adapters/driven/postgres/outbox.rb +33 -7
  27. data/lib/hecks/adapters/driven/postgres/projection.rb +43 -0
  28. data/lib/hecks/adapters/driven/postgres/repository.rb +2 -2
  29. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +2 -1
  30. data/lib/hecks/adapters/driven/postgres/set_dialect.rb +44 -0
  31. data/lib/hecks/adapters/driven/postgres/shared_connection.rb +1 -1
  32. data/lib/hecks/adapters/driven/postgres.adapter +8 -0
  33. data/lib/hecks/adapters/driven/postgres.rb +11 -3
  34. data/lib/hecks/adapters/driven/postgres_dump/connection.rb +57 -0
  35. data/lib/hecks/adapters/driven/postgres_dump.rb +117 -0
  36. data/lib/hecks/adapters/driven/postgres_era/boot.rb +58 -0
  37. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/class_methods.rb +28 -8
  38. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/dialect.rb +11 -6
  39. data/lib/hecks/adapters/driven/postgres_era/field_cache_provisioner.rb +39 -0
  40. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/journal.rb +52 -13
  41. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/chain_sql.rb +37 -53
  42. data/lib/hecks/adapters/driven/postgres_era/lineage/direct_sql.rb +73 -0
  43. data/lib/hecks/adapters/driven/postgres_era/lineage/direct_verified.json +10 -0
  44. data/lib/hecks/adapters/driven/postgres_era/lineage/direct_verified.rb +65 -0
  45. data/lib/hecks/adapters/driven/postgres_era/lineage/domain_rename.rb +143 -0
  46. data/lib/hecks/adapters/driven/postgres_era/lineage/domain_rename_state.rb +108 -0
  47. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/era_integrity.rb +13 -8
  48. data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +125 -0
  49. data/lib/hecks/adapters/driven/postgres_era/lineage/layered_sql.rb +64 -0
  50. data/lib/hecks/adapters/driven/postgres_era/lineage/ledger_reset.rb +150 -0
  51. data/lib/hecks/adapters/driven/postgres_era/lineage/mint_provisioning.rb +85 -0
  52. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/mint_transaction.rb +13 -9
  53. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/provisioning.rb +11 -5
  54. data/lib/hecks/adapters/driven/postgres_era/lineage/split_sql.rb +76 -0
  55. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/sql_templates.rb +15 -2
  56. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/tail_merge.rb +1 -1
  57. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage.rb +11 -3
  58. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/context.rb +68 -0
  59. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage_manager/coverage_check.rb +16 -12
  60. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/disposable_ledger.rb +119 -0
  61. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolution.rb +219 -0
  62. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +114 -0
  63. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/held_era.rb +68 -0
  64. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/ledger_change.rb +63 -0
  65. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage_manager/merge_coordinator.rb +3 -3
  66. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/mint_decision.rb +163 -0
  67. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/mint_request.rb +24 -0
  68. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +143 -0
  69. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/refusals.rb +76 -0
  70. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage_manager.rb +46 -12
  71. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/query_phases.rb +8 -2
  72. data/lib/hecks/adapters/driven/postgres_era/shared_tables.rb +32 -0
  73. data/lib/hecks/adapters/driven/postgres_era.adapter +19 -1
  74. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era.rb +39 -23
  75. data/lib/hecks/adapters/driven/prism.rb +4 -4
  76. data/lib/hecks/adapters/driven/projection/read_model_walk.rb +126 -0
  77. data/lib/hecks/adapters/driven/projection/row_decoding.rb +57 -0
  78. data/lib/hecks/adapters/driven/set_transform/assignments.rb +101 -0
  79. data/lib/hecks/adapters/driven/set_transform/expressions.rb +138 -0
  80. data/lib/hecks/adapters/driven/set_transform.rb +81 -0
  81. data/lib/hecks/adapters/driven/sql_keyset.rb +61 -0
  82. data/lib/hecks/adapters/driven/sql_query_builder.rb +29 -9
  83. data/lib/hecks/adapters/driven/sqlite/codec.rb +1 -1
  84. data/lib/hecks/adapters/driven/sqlite/ddl.rb +7 -0
  85. data/lib/hecks/adapters/driven/sqlite/dialect.rb +2 -1
  86. data/lib/hecks/adapters/driven/sqlite/dispatch_claims.rb +28 -0
  87. data/lib/hecks/adapters/driven/sqlite/journal.rb +46 -0
  88. data/lib/hecks/adapters/driven/sqlite/name_guard.rb +25 -0
  89. data/lib/hecks/adapters/driven/sqlite/outbox.rb +5 -4
  90. data/lib/hecks/adapters/driven/sqlite/projection.rb +9 -135
  91. data/lib/hecks/adapters/driven/sqlite/repository.rb +2 -2
  92. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +36 -4
  93. data/lib/hecks/adapters/driven/sqlite/set_dialect.rb +39 -0
  94. data/lib/hecks/adapters/driven/sqlite.adapter +2 -0
  95. data/lib/hecks/adapters/driven/sqlite.rb +20 -3
  96. data/lib/hecks/adapters/driven/tenant_provisioner.rb +1 -1
  97. data/lib/hecks/adapters/driven/worktree_overlay.rb +117 -0
  98. data/lib/hecks/adapters/driven.rb +8 -3
  99. data/lib/hecks/adapters/driving/cli/shorthand.rb +5 -1
  100. data/lib/hecks/adapters/driving/cli.rb +2 -2
  101. data/lib/hecks/adapters/driving/cli_runner/answers.rb +96 -34
  102. data/lib/hecks/adapters/driving/cli_runner/bang_rule.rb +35 -0
  103. data/lib/hecks/adapters/driving/cli_runner/completion_words.rb +36 -0
  104. data/lib/hecks/adapters/driving/cli_runner/json_words.rb +55 -0
  105. data/lib/hecks/adapters/driving/cli_runner/name_index.rb +63 -0
  106. data/lib/hecks/adapters/driving/cli_runner/read_context.rb +32 -0
  107. data/lib/hecks/adapters/driving/cli_runner/renamed_names.rb +49 -0
  108. data/lib/hecks/adapters/driving/cli_runner/resolution.rb +66 -39
  109. data/lib/hecks/adapters/driving/cli_runner/resume_words.rb +63 -0
  110. data/lib/hecks/adapters/driving/cli_runner/run_reference.rb +66 -0
  111. data/lib/hecks/adapters/driving/cli_runner/run_replay.rb +118 -0
  112. data/lib/hecks/adapters/driving/cli_runner/search_words.rb +35 -0
  113. data/lib/hecks/adapters/driving/cli_runner/settling.rb +14 -9
  114. data/lib/hecks/adapters/driving/cli_runner/streaming.rb +12 -11
  115. data/lib/hecks/adapters/driving/cli_runner/suggestions.rb +16 -8
  116. data/lib/hecks/adapters/driving/cli_runner/tour_words.rb +77 -0
  117. data/lib/hecks/adapters/driving/cli_runner/verb_names.rb +81 -0
  118. data/lib/hecks/adapters/driving/cli_runner/verbosity.rb +75 -0
  119. data/lib/hecks/adapters/driving/cli_runner.rb +110 -9
  120. data/lib/hecks/adapters/driving/context_line.rb +85 -0
  121. data/lib/hecks/adapters/driving/exit_status.rb +31 -0
  122. data/lib/hecks/adapters/driving/github_webhook.rb +2 -1
  123. data/lib/hecks/adapters/driving/handle/verb_methods.rb +46 -0
  124. data/lib/hecks/adapters/driving/handle.rb +9 -17
  125. data/lib/hecks/adapters/driving/json.rb +11 -8
  126. data/lib/hecks/adapters/driving/launcher_options.rb +41 -34
  127. data/lib/hecks/adapters/driving/mcp/progress_sink.rb +45 -0
  128. data/lib/hecks/adapters/driving/mcp/tool_calls.rb +7 -5
  129. data/lib/hecks/adapters/driving/mcp.rb +25 -5
  130. data/lib/hecks/adapters/driving/mcp_scope/argument_policy.rb +40 -7
  131. data/lib/hecks/adapters/driving/mcp_scope/env_problems.rb +3 -1
  132. data/lib/hecks/adapters/driving/mcp_scope.rb +11 -9
  133. data/lib/hecks/adapters/driving/newcomer.rb +6 -5
  134. data/lib/hecks/adapters/driving/ruby/aggregate_module/hooks.rb +154 -0
  135. data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +73 -101
  136. data/lib/hecks/adapters/driving/ruby/bulk_verbs.rb +127 -0
  137. data/lib/hecks/adapters/driving/ruby/{chapter.rb → declaration.rb} +27 -27
  138. data/lib/hecks/adapters/driving/ruby/query_rows.rb +27 -0
  139. data/lib/hecks/adapters/driving/ruby.rb +25 -24
  140. data/lib/hecks/adapters/driving/usage_cache.rb +14 -3
  141. data/lib/hecks/adapters/driving/usage_log.rb +146 -0
  142. data/lib/hecks/adapters/driving/write_guard.rb +55 -0
  143. data/lib/hecks/adapters/driving.rb +0 -1
  144. data/lib/hecks/assembly.rb +101 -0
  145. data/lib/hecks/behaviors/dsl.rb +18 -4
  146. data/lib/hecks/behaviors/expectations/boot.rb +7 -7
  147. data/lib/hecks/behaviors/expectations/checks.rb +1 -1
  148. data/lib/hecks/behaviors/expectations/dispatching.rb +10 -9
  149. data/lib/hecks/behaviors/expectations/qualify.rb +9 -9
  150. data/lib/hecks/behaviors/expectations.rb +24 -10
  151. data/lib/hecks/behaviors/ir.rb +4 -2
  152. data/lib/hecks/behaviors/rspec.rb +1 -1
  153. data/lib/hecks/behaviors/runner.rb +31 -4
  154. data/lib/hecks/bench/environment.rb +6 -2
  155. data/lib/hecks/bench/postgres_probe.rb +3 -1
  156. data/lib/hecks/bench/report.rb +4 -1
  157. data/lib/hecks/bench/suite.rb +9 -3
  158. data/lib/hecks/bluebook/acknowledgements.rb +81 -0
  159. data/lib/hecks/bluebook/aggregate.rb +29 -17
  160. data/lib/hecks/bluebook/ask_resolution.rb +17 -17
  161. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +8 -5
  162. data/lib/hecks/bluebook/assembly/contracts/sagas.rb +2 -0
  163. data/lib/hecks/bluebook/assembly/contracts/structure.rb +24 -15
  164. data/lib/hecks/bluebook/assembly/marks.rb +10 -1
  165. data/lib/hecks/bluebook/assembly/scalars.rb +8 -2
  166. data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
  167. data/lib/hecks/bluebook/assembly.rb +10 -10
  168. data/lib/hecks/bluebook/behaviour/aggregate.rb +29 -4
  169. data/lib/hecks/bluebook/behaviour/attribute.rb +12 -0
  170. data/lib/hecks/bluebook/behaviour/command.rb +1 -1
  171. data/lib/hecks/bluebook/behaviour/{chapter.rb → declaration.rb} +17 -17
  172. data/lib/hecks/bluebook/behaviour/domain_port.rb +1 -1
  173. data/lib/hecks/bluebook/behaviour/entity.rb +1 -0
  174. data/lib/hecks/bluebook/behaviour/hecksagon.rb +8 -1
  175. data/lib/hecks/bluebook/behaviour/process_manager.rb +4 -1
  176. data/lib/hecks/bluebook/behaviour/query.rb +5 -0
  177. data/lib/hecks/bluebook/behaviour/read_model.rb +4 -1
  178. data/lib/hecks/bluebook/behaviour/traits.rb +13 -0
  179. data/lib/hecks/bluebook/capabilities/attribute_values.rb +17 -16
  180. data/lib/hecks/bluebook/capabilities/package_declared.rb +65 -0
  181. data/lib/hecks/bluebook/capabilities.rb +32 -132
  182. data/lib/hecks/bluebook/command.rb +33 -7
  183. data/lib/hecks/bluebook/context_map.rb +104 -0
  184. data/lib/hecks/bluebook/{chapter.rb → declaration.rb} +45 -17
  185. data/lib/hecks/bluebook/domain_port.rb +7 -3
  186. data/lib/hecks/bluebook/dsl/adapter_builder.rb +28 -4
  187. data/lib/hecks/bluebook/dsl/aggregate_builder/draining.rb +2 -2
  188. data/lib/hecks/bluebook/dsl/aggregate_builder/one_of_reference.rb +54 -0
  189. data/lib/hecks/bluebook/dsl/aggregate_builder/query_field_sealing.rb +11 -2
  190. data/lib/hecks/bluebook/dsl/aggregate_builder/query_sealing.rb +15 -5
  191. data/lib/hecks/bluebook/dsl/aggregate_builder/relationships.rb +21 -5
  192. data/lib/hecks/bluebook/dsl/aggregate_builder/renaming.rb +25 -0
  193. data/lib/hecks/bluebook/dsl/aggregate_builder/rules.rb +22 -22
  194. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +2 -2
  195. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +30 -22
  196. data/lib/hecks/bluebook/dsl/attribute_collector.rb +0 -1
  197. data/lib/hecks/bluebook/dsl/audience_word.rb +40 -0
  198. data/lib/hecks/bluebook/dsl/bluebook_builder/metadata.rb +56 -0
  199. data/lib/hecks/bluebook/dsl/bluebook_builder/modules.rb +80 -0
  200. data/lib/hecks/bluebook/dsl/bluebook_builder/pending_givens.rb +22 -22
  201. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/computed_sources.rb +27 -21
  202. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/correlation_keys.rb +54 -5
  203. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/event_shapes.rb +5 -4
  204. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/fan_out_rows.rb +3 -3
  205. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/hop_tails.rb +13 -5
  206. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/projected_fields.rb +4 -4
  207. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/provisions.rb +38 -84
  208. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/query_hops.rb +5 -5
  209. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/references.rb +6 -6
  210. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/with_projections.rb +6 -6
  211. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +4 -3
  212. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +75 -92
  213. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +10 -0
  214. data/lib/hecks/bluebook/dsl/command_builder/folded_sets.rb +48 -0
  215. data/lib/hecks/bluebook/dsl/command_builder.rb +39 -16
  216. data/lib/hecks/bluebook/dsl/context_map_builder.rb +73 -0
  217. data/lib/hecks/bluebook/dsl/entity_builder/rules.rb +17 -17
  218. data/lib/hecks/bluebook/dsl/entity_builder/scope.rb +3 -3
  219. data/lib/hecks/bluebook/dsl/entity_builder/sealing.rb +2 -2
  220. data/lib/hecks/bluebook/dsl/entity_builder.rb +3 -5
  221. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +6 -4
  222. data/lib/hecks/bluebook/dsl/hecksagon_builder/{chapter_scope.rb → bluebook_scope.rb} +14 -14
  223. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +33 -27
  224. data/lib/hecks/bluebook/dsl/identity_declaration.rb +6 -2
  225. data/lib/hecks/bluebook/dsl/kinds.rb +53 -0
  226. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +4 -1
  227. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +9 -2
  228. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +25 -15
  229. data/lib/hecks/bluebook/dsl/query_builder.rb +14 -8
  230. data/lib/hecks/bluebook/dsl/read_model_builder/sealing.rb +6 -11
  231. data/lib/hecks/bluebook/dsl/read_model_builder.rb +1 -2
  232. data/lib/hecks/bluebook/dsl/translation_builder.rb +16 -1
  233. data/lib/hecks/bluebook/dsl/word_gate.rb +3 -2
  234. data/lib/hecks/bluebook/dsl/world_builder.rb +2 -2
  235. data/lib/hecks/bluebook/dsl.rb +4 -2
  236. data/lib/hecks/bluebook/expression/ast_json/emitters.rb +4 -0
  237. data/lib/hecks/bluebook/expression/ast_json.rb +5 -3
  238. data/lib/hecks/bluebook/expression/ast_reader/blocks.rb +41 -0
  239. data/lib/hecks/bluebook/expression/ast_reader.rb +2 -10
  240. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -4
  241. data/lib/hecks/bluebook/expression/evaluator/parser.rb +4 -1
  242. data/lib/hecks/bluebook/expression/evaluator.rb +13 -9
  243. data/lib/hecks/bluebook/expression/projection.json +6 -0
  244. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +50 -2
  245. data/lib/hecks/bluebook/expression/resolver/lookups.rb +2 -1
  246. data/lib/hecks/bluebook/expression/resolver/operations.rb +1 -0
  247. data/lib/hecks/bluebook/expression/resolver/parsing.rb +21 -15
  248. data/lib/hecks/bluebook/expression/resolver/scanning.rb +4 -1
  249. data/lib/hecks/bluebook/expression.rb +1 -1
  250. data/lib/hecks/bluebook/hecksagon.rb +48 -23
  251. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +4 -1
  252. data/lib/hecks/bluebook/meta_validator/judge/cells.rb +1 -1
  253. data/lib/hecks/bluebook/meta_validator/judge/identity.rb +11 -5
  254. data/lib/hecks/bluebook/meta_validator/modes.rb +27 -27
  255. data/lib/hecks/bluebook/meta_validator/plan/category.rb +10 -2
  256. data/lib/hecks/bluebook/meta_validator/plan/verbs.rb +9 -0
  257. data/lib/hecks/bluebook/meta_validator/plan.rb +7 -6
  258. data/lib/hecks/bluebook/meta_validator/readings/fields.rb +3 -3
  259. data/lib/hecks/bluebook/meta_validator/reconstruction/heads.rb +14 -4
  260. data/lib/hecks/bluebook/meta_validator/reconstruction/reading.rb +8 -1
  261. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -19
  262. data/lib/hecks/bluebook/meta_validator/shapes.rb +9 -1
  263. data/lib/hecks/bluebook/meta_validator/syntax_boot/disk_cache.rb +19 -16
  264. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +21 -15
  265. data/lib/hecks/bluebook/meta_validator/verdict_cache/prebuilt.rb +4 -1
  266. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +11 -10
  267. data/lib/hecks/bluebook/meta_validator.rb +25 -22
  268. data/lib/hecks/bluebook/model_check/aggregate_checks.rb +95 -0
  269. data/lib/hecks/bluebook/model_check/ask_checks.rb +31 -5
  270. data/lib/hecks/bluebook/model_check/client_profile.rb +8 -42
  271. data/lib/hecks/bluebook/model_check/context_checks.rb +97 -0
  272. data/lib/hecks/bluebook/model_check/context_map_checks.rb +158 -0
  273. data/lib/hecks/bluebook/model_check/lifecycle_checks.rb +27 -5
  274. data/lib/hecks/bluebook/model_check/one_of_checks.rb +106 -0
  275. data/lib/hecks/bluebook/model_check/orphan_checks.rb +129 -0
  276. data/lib/hecks/bluebook/model_check/reaction_chains.rb +114 -0
  277. data/lib/hecks/bluebook/model_check/reaction_cycles.rb +53 -0
  278. data/lib/hecks/bluebook/model_check/reaction_depth_checks.rb +58 -0
  279. data/lib/hecks/bluebook/model_check/reaction_walk.rb +62 -0
  280. data/lib/hecks/bluebook/model_check/redundant_relationships.rb +38 -0
  281. data/lib/hecks/bluebook/model_check/reference_checks.rb +11 -1
  282. data/lib/hecks/bluebook/model_check/saga_checks.rb +8 -3
  283. data/lib/hecks/bluebook/model_check/verbs.rb +4 -4
  284. data/lib/hecks/bluebook/model_check.rb +33 -35
  285. data/lib/hecks/bluebook/named_pattern.rb +12 -1
  286. data/lib/hecks/bluebook/one_of_union.rb +35 -0
  287. data/lib/hecks/bluebook/pattern_subset.rb +12 -3
  288. data/lib/hecks/bluebook/process_manager.rb +20 -3
  289. data/lib/hecks/bluebook/project_loader.rb +3 -2
  290. data/lib/hecks/bluebook/project_register.rb +21 -5
  291. data/lib/hecks/bluebook/query.rb +4 -1
  292. data/lib/hecks/bluebook/read_model.rb +5 -5
  293. data/lib/hecks/bluebook/reference.rb +3 -3
  294. data/lib/hecks/bluebook/smoke_test.rb +15 -13
  295. data/lib/hecks/bluebook/synthesizer.rb +48 -13
  296. data/lib/hecks/bluebook/translation.rb +5 -3
  297. data/lib/hecks/bluebook/translation_expression/parser.rb +14 -5
  298. data/lib/hecks/bluebook/translation_plan.rb +9 -2
  299. data/lib/hecks/bluebook.rb +6 -3
  300. data/lib/hecks/bulk/bluebook/adapters/row_sweeper.adapter +7 -0
  301. data/lib/hecks/bulk/bluebook/bulk.behaviors +18 -0
  302. data/lib/hecks/bulk/bluebook/bulk.bluebook +126 -0
  303. data/lib/hecks/bulk/bluebook/bulk.hecksagon +11 -0
  304. data/lib/hecks/bulk/bluebook/bulk.ports.hecksagon +13 -0
  305. data/lib/hecks/bulk/plan.rb +60 -0
  306. data/lib/hecks/bulk/row_sweeper.rb +34 -0
  307. data/lib/hecks/bulk/set_arguments.rb +68 -0
  308. data/lib/hecks/bulk/set_eligibility.rb +50 -0
  309. data/lib/hecks/bulk/set_planner.rb +100 -0
  310. data/lib/hecks/bulk/set_sweep.rb +78 -0
  311. data/lib/hecks/bulk/sweep.rb +67 -0
  312. data/lib/hecks/ci_pipeline/adapters/ci_cache.adapter +7 -0
  313. data/lib/hecks/ci_pipeline/adapters/ci_cache.rb +39 -0
  314. data/lib/hecks/ci_pipeline/adapters/ci_runner.adapter +7 -0
  315. data/lib/hecks/ci_pipeline/adapters/ci_runner.rb +55 -0
  316. data/lib/hecks/ci_pipeline/ci_pipeline.behaviors +322 -0
  317. data/lib/hecks/ci_pipeline/ci_pipeline.bluebook +938 -0
  318. data/lib/hecks/ci_pipeline/ci_pipeline.hecksagon +12 -0
  319. data/lib/hecks/ci_pipeline/ci_pipeline.ports.hecksagon +16 -0
  320. data/lib/hecks/ci_speed/adapters/ci_reader.adapter +7 -0
  321. data/lib/hecks/ci_speed/adapters/ci_reader.rb +141 -0
  322. data/lib/hecks/ci_speed/adapters/local_runner/answer.rb +41 -0
  323. data/lib/hecks/ci_speed/adapters/local_runner/catalog.rb +89 -0
  324. data/lib/hecks/ci_speed/adapters/local_runner/checkouts.rb +89 -0
  325. data/lib/hecks/ci_speed/adapters/local_runner/launcher.rb +69 -0
  326. data/lib/hecks/ci_speed/adapters/local_runner/load_average.rb +38 -0
  327. data/lib/hecks/ci_speed/adapters/local_runner/refs.rb +39 -0
  328. data/lib/hecks/ci_speed/adapters/local_runner/refused.rb +11 -0
  329. data/lib/hecks/ci_speed/adapters/local_runner/verdict.rb +71 -0
  330. data/lib/hecks/ci_speed/adapters/local_runner.adapter +7 -0
  331. data/lib/hecks/ci_speed/adapters/local_runner.rb +168 -0
  332. data/lib/hecks/ci_speed/adapters/replayed_runner.rb +39 -0
  333. data/lib/hecks/ci_speed/ci_speed.behaviors +341 -0
  334. data/lib/hecks/ci_speed/ci_speed.bluebook +1073 -0
  335. data/lib/hecks/ci_speed/ci_speed.hecksagon +11 -0
  336. data/lib/hecks/ci_speed/ci_speed.ports.hecksagon +21 -0
  337. data/lib/hecks/ci_workflows/ci_workflows.behaviors +13 -0
  338. data/lib/hecks/ci_workflows/ci_workflows.bluebook +173 -0
  339. data/lib/hecks/ci_workflows/ci_workflows.hecksagon +15 -0
  340. data/lib/hecks/codemod.rb +3 -3
  341. data/lib/hecks/coder/coder.bluebook +385 -0
  342. data/lib/hecks/coder/coder.hecksagon +12 -0
  343. data/lib/hecks/command_names/check.rb +39 -0
  344. data/lib/hecks/command_names/extractor.rb +67 -0
  345. data/lib/hecks/command_names/index.rb +40 -0
  346. data/lib/hecks/command_names/resolution.rb +81 -0
  347. data/lib/hecks/command_names/scan.rb +54 -0
  348. data/lib/hecks/command_names/sources.rb +73 -0
  349. data/lib/hecks/command_names.rb +26 -0
  350. data/lib/hecks/composer/adapters/claude_composer.rb +3 -2
  351. data/lib/hecks/composer/agent.rb +1 -1
  352. data/lib/hecks/composer/assistant.rb +2 -2
  353. data/lib/hecks/composer/behavior_run.rb +7 -1
  354. data/lib/hecks/composer/bluebook/composer.bluebook +18 -25
  355. data/lib/hecks/composer/bluebook/composer.hecksagon +1 -1
  356. data/lib/hecks/composer/board.rb +6 -6
  357. data/lib/hecks/composer/board_edit/command_text.rb +4 -1
  358. data/lib/hecks/composer/board_edit/source.rb +18 -9
  359. data/lib/hecks/composer/board_edit.rb +2 -2
  360. data/lib/hecks/composer/check.rb +22 -22
  361. data/lib/hecks/composer/domains.rb +1 -1
  362. data/lib/hecks/composer/jobs.rb +1 -1
  363. data/lib/hecks/composer/live.rb +19 -13
  364. data/lib/hecks/composer/outline.rb +4 -1
  365. data/lib/hecks/composer/page.html +1 -1
  366. data/lib/hecks/composer/panels.rb +6 -3
  367. data/lib/hecks/composer/parse.rb +53 -36
  368. data/lib/hecks/composer/session.rb +22 -16
  369. data/lib/hecks/composer/toolbox.rb +17 -7
  370. data/lib/hecks/composer/web.rb +3 -2
  371. data/lib/hecks/composer/workbench.rb +12 -9
  372. data/lib/hecks/composer.rb +1 -1
  373. data/lib/hecks/construct.rb +7 -7
  374. data/lib/hecks/corpus/adapters.rb +2 -1
  375. data/lib/hecks/corpus/generated_key.rb +205 -0
  376. data/lib/hecks/corpus/listing.rb +34 -0
  377. data/lib/hecks/corpus/routes.rb +25 -5
  378. data/lib/hecks/corpus/rust.rb +86 -35
  379. data/lib/hecks/corpus/sources.rb +14 -12
  380. data/lib/hecks/corpus/worlds.rb +2 -1
  381. data/lib/hecks/corpus.rb +57 -54
  382. data/lib/hecks/deploy/bluebook/deploy.bluebook +304 -518
  383. data/lib/hecks/deprecation.rb +23 -0
  384. data/lib/hecks/doc/reference/harvester.rb +4 -1
  385. data/lib/hecks/doc/reference/readme.rb +11 -6
  386. data/lib/hecks/doc/reference/rendering.rb +12 -6
  387. data/lib/hecks/doc/reference.rb +8 -6
  388. data/lib/hecks/embryonaut_bluebook/lock.rb +5 -1
  389. data/lib/hecks/embryonaut_bluebook/manifest.rb +8 -2
  390. data/lib/hecks/embryonaut_bluebook/registry/changelog.rb +4 -1
  391. data/lib/hecks/embryonaut_bluebook/registry/versions.rb +13 -2
  392. data/lib/hecks/embryonaut_bluebook/registry.rb +5 -2
  393. data/lib/hecks/embryonaut_bluebook/shape.rb +1 -1
  394. data/lib/hecks/embryonaut_bluebook/vendor.rb +5 -2
  395. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +15 -1
  396. data/lib/hecks/embryonaut_bluebook.rb +26 -12
  397. data/lib/hecks/engine_agreement.rb +4 -1
  398. data/lib/hecks/fixer/fixer.bluebook +240 -0
  399. data/lib/hecks/fixer/fixer.hecksagon +17 -0
  400. data/lib/hecks/forms/app/commands.rb +35 -2
  401. data/lib/hecks/forms/app/records.rb +12 -12
  402. data/lib/hecks/forms/app/responses.rb +1 -1
  403. data/lib/hecks/forms/app.rb +22 -19
  404. data/lib/hecks/forms/command_form_renderer.rb +2 -2
  405. data/lib/hecks/forms/field_controls.rb +6 -1
  406. data/lib/hecks/forms/field_shape.rb +14 -6
  407. data/lib/hecks/forms/html.rb +8 -7
  408. data/lib/hecks/forms/html_pattern.rb +80 -0
  409. data/lib/hecks/forms/index_renderer.rb +11 -11
  410. data/lib/hecks/forms/params.rb +2 -1
  411. data/lib/hecks/forms/port_argument.rb +4 -1
  412. data/lib/hecks/forms/query_form_renderer.rb +1 -1
  413. data/lib/hecks/forms/record_renderer.rb +2 -2
  414. data/lib/hecks/forms/record_table.rb +1 -1
  415. data/lib/hecks/forms/reference_options.rb +2 -1
  416. data/lib/hecks/forms/text_shape.rb +4 -1
  417. data/lib/hecks/forms/value_object_shape.rb +1 -1
  418. data/lib/hecks/forms.rb +7 -7
  419. data/lib/hecks/fqn.rb +8 -2
  420. data/lib/hecks/framework/bluebook/change.bluebook +10 -15
  421. data/lib/hecks/framework/bluebook/change.hecksagon +1 -1
  422. data/lib/hecks/framework/bluebook/compliance.bluebook +27 -126
  423. data/lib/hecks/framework/bluebook/console_settings.bluebook +199 -114
  424. data/lib/hecks/framework/bluebook/framework.hecksagon +7 -0
  425. data/lib/hecks/framework/bluebook/governance.bluebook +36 -14
  426. data/lib/hecks/framework/bluebook/identity.bluebook +1 -1
  427. data/lib/hecks/framework/bluebook/privacy.bluebook +123 -19
  428. data/lib/hecks/framework/bluebook/privacy.key_keeper.adapter +7 -0
  429. data/lib/hecks/framework/bluebook/privacy.ports.hecksagon +19 -0
  430. data/lib/hecks/framework/compliance.oidc.json +6 -36
  431. data/lib/hecks/framework/console_settings.oidc.json +39 -24
  432. data/lib/hecks/framework/privacy.oidc.json +25 -0
  433. data/lib/hecks/framework.rb +24 -8
  434. data/lib/hecks/framework_bluebooks.rb +181 -0
  435. data/lib/hecks/freezer.rb +7 -1
  436. data/lib/hecks/fuzzing/combination_miner.rb +9 -6
  437. data/lib/hecks/fuzzing/concurrent_dispatch/world_copy.rb +4 -1
  438. data/lib/hecks/fuzzing/concurrent_dispatch.rb +2 -2
  439. data/lib/hecks/fuzzing/domain_generator.rb +8 -3
  440. data/lib/hecks/fuzzing/era_boundary.rb +1 -1
  441. data/lib/hecks/fuzzing/failure_triage.rb +13 -6
  442. data/lib/hecks/fuzzing/form_census/{chapter_forms.rb → bluebook_forms.rb} +12 -12
  443. data/lib/hecks/fuzzing/form_census.rb +14 -12
  444. data/lib/hecks/fuzzing/generated_domain_check.rb +2 -1
  445. data/lib/hecks/fuzzing/isolated_boot/postgres_scratch.rb +14 -15
  446. data/lib/hecks/fuzzing/isolated_boot/scratch_schema.rb +19 -9
  447. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +36 -13
  448. data/lib/hecks/fuzzing/isolated_boot.rb +5 -6
  449. data/lib/hecks/fuzzing/mutation/checks.rb +5 -4
  450. data/lib/hecks/fuzzing/mutation/finders.rb +25 -8
  451. data/lib/hecks/fuzzing/mutation/observation.rb +3 -1
  452. data/lib/hecks/fuzzing/mutation/source_map.rb +3 -1
  453. data/lib/hecks/fuzzing/nondeterministic.rb +10 -4
  454. data/lib/hecks/fuzzing/outside_world.rb +1 -1
  455. data/lib/hecks/fuzzing/projection_agreement/binding.rb +63 -0
  456. data/lib/hecks/fuzzing/projection_agreement/comparison.rb +104 -0
  457. data/lib/hecks/fuzzing/projection_agreement.rb +68 -0
  458. data/lib/hecks/fuzzing/properties/aggregation_recompute.rb +1 -1
  459. data/lib/hecks/fuzzing/properties/bulk_parity.rb +118 -0
  460. data/lib/hecks/fuzzing/properties/catalog.rb +7 -0
  461. data/lib/hecks/fuzzing/properties/cursor_walks.rb +56 -0
  462. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +39 -1
  463. data/lib/hecks/fuzzing/properties/group_by_recompute.rb +6 -2
  464. data/lib/hecks/fuzzing/properties/grouped_reduction_recompute.rb +88 -0
  465. data/lib/hecks/fuzzing/properties/guards.rb +4 -1
  466. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +12 -2
  467. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +2 -1
  468. data/lib/hecks/fuzzing/properties/mutation_recompute.rb +3 -2
  469. data/lib/hecks/fuzzing/properties/piece_invariants.rb +3 -2
  470. data/lib/hecks/fuzzing/properties/query_rows.rb +5 -3
  471. data/lib/hecks/fuzzing/properties/querying.rb +6 -3
  472. data/lib/hecks/fuzzing/properties/read_model_names.rb +8 -2
  473. data/lib/hecks/fuzzing/properties/read_models.rb +2 -1
  474. data/lib/hecks/fuzzing/properties/refusals.rb +35 -0
  475. data/lib/hecks/fuzzing/properties/role_checks.rb +3 -2
  476. data/lib/hecks/fuzzing/properties/set_transform_parity.rb +73 -0
  477. data/lib/hecks/fuzzing/properties/tenant_scope.rb +3 -2
  478. data/lib/hecks/fuzzing/properties.rb +6 -0
  479. data/lib/hecks/fuzzing/qa_settings.rb +4 -4
  480. data/lib/hecks/fuzzing/replay/fan_out.rb +4 -3
  481. data/lib/hecks/fuzzing/replay/filters.rb +2 -2
  482. data/lib/hecks/fuzzing/replay/guard_check.rb +2 -2
  483. data/lib/hecks/fuzzing/replay/mutation_trace.rb +23 -10
  484. data/lib/hecks/fuzzing/replay/observation.rb +11 -7
  485. data/lib/hecks/fuzzing/replay/read_only_steps.rb +40 -3
  486. data/lib/hecks/fuzzing/replay/refusal_trace.rb +44 -0
  487. data/lib/hecks/fuzzing/replay/role_check.rb +2 -2
  488. data/lib/hecks/fuzzing/replay/session.rb +38 -18
  489. data/lib/hecks/fuzzing/replay/validation_reports.rb +61 -0
  490. data/lib/hecks/fuzzing/replay.rb +2 -2
  491. data/lib/hecks/fuzzing/rust_gap_manifest.rb +3 -3
  492. data/lib/hecks/fuzzing/self_consistency/saga_redelivery.rb +18 -8
  493. data/lib/hecks/fuzzing/self_consistency/sagas.rb +2 -2
  494. data/lib/hecks/fuzzing/self_consistency/value_objects.rb +9 -2
  495. data/lib/hecks/fuzzing/self_consistency.rb +4 -8
  496. data/lib/hecks/fuzzing/sequence_generator/adversary/argument_mutations.rb +15 -5
  497. data/lib/hecks/fuzzing/sequence_generator/adversary/caller_draw.rb +14 -10
  498. data/lib/hecks/fuzzing/sequence_generator/adversary/precedence_mutations.rb +1 -1
  499. data/lib/hecks/fuzzing/sequence_generator/argument_drawing.rb +2 -1
  500. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +21 -13
  501. data/lib/hecks/fuzzing/sequence_generator/picker.rb +1 -1
  502. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +3 -3
  503. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +2 -2
  504. data/lib/hecks/fuzzing/sequence_generator.rb +1 -2
  505. data/lib/hecks/fuzzing/shrinker.rb +4 -1
  506. data/lib/hecks/fuzzing/value_generator.rb +14 -8
  507. data/lib/hecks/fuzzing.rb +1 -0
  508. data/lib/hecks/grammar/dialects.rb +56 -0
  509. data/lib/hecks/grammar/evolve/arguments.rb +1 -1
  510. data/lib/hecks/grammar/evolve/keywords.rb +1 -1
  511. data/lib/hecks/grammar/evolve/seeds.rb +34 -8
  512. data/lib/hecks/grammar/expression.bluebook +3 -0
  513. data/lib/hecks/grammar/expression_operators.json +1071 -1
  514. data/lib/hecks/grammar/operators.rb +45 -22
  515. data/lib/hecks/grammar/translation.bluebook +3 -1
  516. data/lib/hecks/grammar/translation_ledger.rb +45 -0
  517. data/lib/hecks/grammar/translation_rules.json +710 -0
  518. data/lib/hecks/grammar.rb +27 -23
  519. data/lib/hecks/hecks/adapter_words.json +7 -0
  520. data/lib/hecks/hecks/adapters/area_overlap.rb +156 -0
  521. data/lib/hecks/hecks/adapters/claude_agents.adapter +7 -0
  522. data/lib/hecks/hecks/adapters/claude_agents.rb +124 -0
  523. data/lib/hecks/hecks/adapters/claude_transcript.rb +71 -0
  524. data/lib/hecks/hecks/adapters/codebase/batch_runs.rb +60 -0
  525. data/lib/hecks/hecks/adapters/codebase/cache_clearing.rb +32 -0
  526. data/lib/hecks/hecks/adapters/codebase/cargo_tests.rb +50 -0
  527. data/lib/hecks/hecks/adapters/codebase/changed_specs.rb +52 -0
  528. data/lib/hecks/hecks/adapters/codebase/codemods.rb +3 -2
  529. data/lib/hecks/hecks/adapters/codebase/composing.rb +3 -2
  530. data/lib/hecks/hecks/adapters/codebase/conformance.rb +30 -1
  531. data/lib/hecks/hecks/adapters/codebase/corpus_queries.rb +53 -0
  532. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +1 -1
  533. data/lib/hecks/hecks/adapters/codebase/declarations.rb +144 -0
  534. data/lib/hecks/hecks/adapters/codebase/generated_keys.rb +31 -0
  535. data/lib/hecks/hecks/adapters/codebase/impacted_specs.rb +110 -0
  536. data/lib/hecks/hecks/adapters/codebase/inspection.rb +5 -4
  537. data/lib/hecks/hecks/adapters/codebase/language.rb +3 -2
  538. data/lib/hecks/hecks/adapters/codebase/launcher_text.rb +39 -0
  539. data/lib/hecks/hecks/adapters/codebase/minitest_runs.rb +52 -0
  540. data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +7 -1
  541. data/lib/hecks/hecks/adapters/codebase/previewing.rb +62 -0
  542. data/lib/hecks/hecks/adapters/codebase/promotion/gate.rb +1 -1
  543. data/lib/hecks/hecks/adapters/codebase/reading.rb +94 -0
  544. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +57 -10
  545. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +1 -1
  546. data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +5 -2
  547. data/lib/hecks/hecks/adapters/codebase/run_options.rb +35 -0
  548. data/lib/hecks/hecks/adapters/codebase/scripting.rb +39 -0
  549. data/lib/hecks/hecks/adapters/codebase/search.rb +91 -0
  550. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +114 -51
  551. data/lib/hecks/hecks/adapters/codebase/spec_files/report.rb +71 -0
  552. data/lib/hecks/hecks/adapters/codebase/spec_files/shared_run.rb +73 -0
  553. data/lib/hecks/hecks/adapters/codebase/spec_files.rb +95 -0
  554. data/lib/hecks/hecks/adapters/codebase/static_directory.rb +59 -0
  555. data/lib/hecks/hecks/adapters/codebase/style.rb +2 -2
  556. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +82 -14
  557. data/lib/hecks/hecks/adapters/codebase/test_scope.rb +105 -0
  558. data/lib/hecks/hecks/adapters/codebase/test_suite.rb +73 -21
  559. data/lib/hecks/hecks/adapters/console_capture.rb +28 -1
  560. data/lib/hecks/hecks/adapters/cost_explorer.rb +9 -1
  561. data/lib/hecks/hecks/adapters/deploy_plans.rb +12 -3
  562. data/lib/hecks/hecks/adapters/deploy_toolchain/answers.rb +14 -5
  563. data/lib/hecks/hecks/adapters/deploy_toolchain/scripts.rb +6 -3
  564. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +3 -3
  565. data/lib/hecks/hecks/adapters/desktop_alert/alert.rb +50 -0
  566. data/lib/hecks/hecks/adapters/desktop_alert.adapter +7 -0
  567. data/lib/hecks/hecks/adapters/desktop_alert.rb +99 -0
  568. data/lib/hecks/hecks/adapters/dispatching/brief_text.rb +51 -0
  569. data/lib/hecks/hecks/adapters/dispatching/launcher.rb +97 -0
  570. data/lib/hecks/hecks/adapters/dispatching/plan.rb +82 -0
  571. data/lib/hecks/hecks/adapters/dispatching/rows.rb +66 -0
  572. data/lib/hecks/hecks/adapters/dispatching/settings.rb +81 -0
  573. data/lib/hecks/hecks/adapters/dispatching/settler.rb +40 -0
  574. data/lib/hecks/hecks/adapters/dispatching.adapter +7 -0
  575. data/lib/hecks/hecks/adapters/dispatching.rb +93 -0
  576. data/lib/hecks/hecks/adapters/doctor_reading/report.rb +27 -0
  577. data/lib/hecks/hecks/adapters/doctor_reading.adapter +7 -0
  578. data/lib/hecks/hecks/adapters/doctor_reading.rb +161 -0
  579. data/lib/hecks/hecks/adapters/finding_github.rb +4 -1
  580. data/lib/hecks/hecks/adapters/first_admin.rb +16 -9
  581. data/lib/hecks/hecks/adapters/git.rb +5 -5
  582. data/lib/hecks/hecks/adapters/git_checkout.adapter +7 -0
  583. data/lib/hecks/hecks/adapters/git_checkout.rb +99 -0
  584. data/lib/hecks/hecks/adapters/git_commit.adapter +7 -0
  585. data/lib/hecks/hecks/adapters/git_commit.rb +61 -0
  586. data/lib/hecks/hecks/adapters/git_landing/finisher.rb +82 -0
  587. data/lib/hecks/hecks/adapters/git_landing/foreign_hold.rb +111 -0
  588. data/lib/hecks/hecks/adapters/git_landing/line.rb +171 -0
  589. data/lib/hecks/hecks/adapters/git_landing/lineup.rb +98 -0
  590. data/lib/hecks/hecks/adapters/git_landing/merger.rb +85 -0
  591. data/lib/hecks/hecks/adapters/git_landing/numbering.rb +54 -0
  592. data/lib/hecks/hecks/adapters/git_landing/rivals.rb +59 -0
  593. data/lib/hecks/hecks/adapters/git_landing/ticket.rb +156 -0
  594. data/lib/hecks/hecks/adapters/git_landing/tidy.rb +147 -0
  595. data/lib/hecks/hecks/adapters/git_landing/train.rb +104 -0
  596. data/lib/hecks/hecks/adapters/git_landing/verdict.rb +64 -0
  597. data/lib/hecks/hecks/adapters/git_landing/waiting.rb +120 -0
  598. data/lib/hecks/hecks/adapters/git_landing.adapter +7 -0
  599. data/lib/hecks/hecks/adapters/git_landing.rb +189 -0
  600. data/lib/hecks/hecks/adapters/git_launcher.adapter +7 -0
  601. data/lib/hecks/hecks/adapters/git_launcher.rb +82 -0
  602. data/lib/hecks/hecks/adapters/git_main_pull.rb +99 -0
  603. data/lib/hecks/hecks/adapters/git_refs.rb +8 -4
  604. data/lib/hecks/hecks/adapters/git_rehearsal/bench.rb +82 -0
  605. data/lib/hecks/hecks/adapters/git_rehearsal/due.rb +84 -0
  606. data/lib/hecks/hecks/adapters/git_rehearsal/impacted/skips.rb +47 -0
  607. data/lib/hecks/hecks/adapters/git_rehearsal/impacted/spec_run.rb +75 -0
  608. data/lib/hecks/hecks/adapters/git_rehearsal/impacted.rb +181 -0
  609. data/lib/hecks/hecks/adapters/git_rehearsal/line.rb +76 -0
  610. data/lib/hecks/hecks/adapters/git_rehearsal/reading.rb +24 -0
  611. data/lib/hecks/hecks/adapters/git_rehearsal/record.rb +146 -0
  612. data/lib/hecks/hecks/adapters/git_rehearsal/spec_map/corpus.rb +25 -0
  613. data/lib/hecks/hecks/adapters/git_rehearsal/spec_map/needles.rb +61 -0
  614. data/lib/hecks/hecks/adapters/git_rehearsal/spec_map.rb +105 -0
  615. data/lib/hecks/hecks/adapters/git_rehearsal/stages.rb +118 -0
  616. data/lib/hecks/hecks/adapters/git_rehearsal.adapter +7 -0
  617. data/lib/hecks/hecks/adapters/git_rehearsal.rb +141 -0
  618. data/lib/hecks/hecks/adapters/git_train/bench.rb +76 -0
  619. data/lib/hecks/hecks/adapters/git_train/branch_guard.rb +62 -0
  620. data/lib/hecks/hecks/adapters/git_train/carriage.rb +115 -0
  621. data/lib/hecks/hecks/adapters/git_train/launcher_shape.rb +75 -0
  622. data/lib/hecks/hecks/adapters/git_train/local_branch.rb +69 -0
  623. data/lib/hecks/hecks/adapters/git_train/mint_step.rb +47 -0
  624. data/lib/hecks/hecks/adapters/git_train/reading.rb +26 -0
  625. data/lib/hecks/hecks/adapters/git_train/settlement.rb +167 -0
  626. data/lib/hecks/hecks/adapters/git_train/survey.rb +60 -0
  627. data/lib/hecks/hecks/adapters/git_train.adapter +7 -0
  628. data/lib/hecks/hecks/adapters/git_train.rb +146 -0
  629. data/lib/hecks/hecks/adapters/git_worktree_add.adapter +7 -0
  630. data/lib/hecks/hecks/adapters/git_worktree_add.rb +68 -0
  631. data/lib/hecks/hecks/adapters/git_worktrees.rb +81 -0
  632. data/lib/hecks/hecks/adapters/github_pull_requests.adapter +7 -0
  633. data/lib/hecks/hecks/adapters/github_pull_requests.rb +109 -0
  634. data/lib/hecks/hecks/adapters/host_http.rb +9 -8
  635. data/lib/hecks/hecks/adapters/in_process_access.rb +3 -2
  636. data/lib/hecks/hecks/adapters/in_process_boot.rb +68 -39
  637. data/lib/hecks/hecks/adapters/in_process_operations.rb +40 -20
  638. data/lib/hecks/hecks/adapters/in_process_undo.rb +17 -0
  639. data/lib/hecks/hecks/adapters/journal_store/changes.rb +18 -9
  640. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +14 -7
  641. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +35 -20
  642. data/lib/hecks/hecks/adapters/journal_store/examination.rb +20 -4
  643. data/lib/hecks/hecks/adapters/journal_store/held_domain/renaming.rb +45 -0
  644. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +47 -9
  645. data/lib/hecks/hecks/adapters/journal_store/held_mint.rb +128 -0
  646. data/lib/hecks/hecks/adapters/journal_store/readings/attestation.rb +3 -3
  647. data/lib/hecks/hecks/adapters/journal_store/readings/boot_status.rb +58 -0
  648. data/lib/hecks/hecks/adapters/journal_store/readings/composition.rb +39 -0
  649. data/lib/hecks/hecks/adapters/journal_store/readings/event_trace/outcomes.rb +76 -0
  650. data/lib/hecks/hecks/adapters/journal_store/readings/event_trace/postgres_reader.rb +44 -0
  651. data/lib/hecks/hecks/adapters/journal_store/readings/event_trace/reader.rb +64 -0
  652. data/lib/hecks/hecks/adapters/journal_store/readings/event_trace/report.rb +102 -0
  653. data/lib/hecks/hecks/adapters/journal_store/readings/event_trace/sqlite_reader.rb +43 -0
  654. data/lib/hecks/hecks/adapters/journal_store/readings/event_trace/stores.rb +80 -0
  655. data/lib/hecks/hecks/adapters/journal_store/readings/event_trace/subscribers.rb +72 -0
  656. data/lib/hecks/hecks/adapters/journal_store/readings/event_trace/summary.rb +39 -0
  657. data/lib/hecks/hecks/adapters/journal_store/readings/event_trace.rb +39 -0
  658. data/lib/hecks/hecks/adapters/journal_store/readings/mint_plan/command_line.rb +57 -0
  659. data/lib/hecks/hecks/adapters/journal_store/readings/mint_plan/edge_guards.rb +107 -0
  660. data/lib/hecks/hecks/adapters/journal_store/readings/mint_plan/inspection.rb +94 -0
  661. data/lib/hecks/hecks/adapters/journal_store/readings/mint_plan/plan.rb +66 -0
  662. data/lib/hecks/hecks/adapters/journal_store/readings/mint_plan/report.rb +72 -0
  663. data/lib/hecks/hecks/adapters/journal_store/readings/mint_plan/store.rb +34 -0
  664. data/lib/hecks/hecks/adapters/journal_store/readings/mint_plan/watermark_guards.rb +86 -0
  665. data/lib/hecks/hecks/adapters/journal_store/readings/mint_plan/world.rb +66 -0
  666. data/lib/hecks/hecks/adapters/journal_store/readings/mint_plan.rb +36 -0
  667. data/lib/hecks/hecks/adapters/journal_store/readings/projection_audit.rb +64 -0
  668. data/lib/hecks/hecks/adapters/journal_store/readings/scaffolding.rb +10 -7
  669. data/lib/hecks/hecks/adapters/journal_store/readings/sqlite_status/database_reading.rb +113 -0
  670. data/lib/hecks/hecks/adapters/journal_store/readings/sqlite_status/read_only.rb +33 -0
  671. data/lib/hecks/hecks/adapters/journal_store/readings/sqlite_status.rb +32 -0
  672. data/lib/hecks/hecks/adapters/journal_store/readings.rb +15 -3
  673. data/lib/hecks/hecks/adapters/journal_store/reconcile_report.rb +60 -0
  674. data/lib/hecks/hecks/adapters/journal_store/reconciliation.rb +121 -0
  675. data/lib/hecks/hecks/adapters/journal_store/translation_write/shape_edge.rb +59 -0
  676. data/lib/hecks/hecks/adapters/journal_store/translation_write.rb +123 -0
  677. data/lib/hecks/hecks/adapters/journal_store.rb +18 -2
  678. data/lib/hecks/hecks/adapters/landed_work.adapter +7 -0
  679. data/lib/hecks/hecks/adapters/landed_work.rb +52 -0
  680. data/lib/hecks/hecks/adapters/ledger/chain.rb +63 -0
  681. data/lib/hecks/hecks/adapters/ledger/journal.rb +105 -0
  682. data/lib/hecks/hecks/adapters/ledger.adapter +7 -0
  683. data/lib/hecks/hecks/adapters/ledger.rb +89 -0
  684. data/lib/hecks/hecks/adapters/local_files.rb +21 -7
  685. data/lib/hecks/hecks/adapters/pg_admin.rb +7 -1
  686. data/lib/hecks/hecks/adapters/process_pool.rb +3 -2
  687. data/lib/hecks/hecks/adapters/push_size.rb +57 -0
  688. data/lib/hecks/hecks/adapters/qa_quiescing.adapter +7 -0
  689. data/lib/hecks/hecks/adapters/qa_quiescing.rb +57 -0
  690. data/lib/hecks/hecks/adapters/rails_app_files.rb +64 -0
  691. data/lib/hecks/hecks/adapters/rehearsal/audit_report.rb +4 -1
  692. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +39 -12
  693. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +65 -5
  694. data/lib/hecks/hecks/adapters/rehearsal/postgres_census.rb +4 -4
  695. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +30 -7
  696. data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +4 -1
  697. data/lib/hecks/hecks/adapters/rehearsal/rust_audit.rb +1 -1
  698. data/lib/hecks/hecks/adapters/rehearsal/subject.rb +44 -9
  699. data/lib/hecks/hecks/adapters/rust_toolchain.rb +15 -4
  700. data/lib/hecks/hecks/adapters/rust_workspace.rb +1 -1
  701. data/lib/hecks/hecks/adapters/settings_reading/disposable_ledger.rb +32 -0
  702. data/lib/hecks/hecks/adapters/settings_reading/push_nudge.rb +26 -0
  703. data/lib/hecks/hecks/adapters/settings_reading/stores.rb +76 -0
  704. data/lib/hecks/hecks/adapters/settings_reading/test_scope.rb +28 -0
  705. data/lib/hecks/hecks/adapters/settings_reading.adapter +7 -0
  706. data/lib/hecks/hecks/adapters/settings_reading.rb +168 -0
  707. data/lib/hecks/hecks/adapters/shell.rb +3 -0
  708. data/lib/hecks/hecks/adapters/site_toolchain.rb +8 -8
  709. data/lib/hecks/hecks/adapters/started_work.rb +42 -0
  710. data/lib/hecks/hecks/adapters/terminal.rb +12 -10
  711. data/lib/hecks/hecks/adapters/where_reading.adapter +7 -0
  712. data/lib/hecks/hecks/adapters/where_reading.rb +64 -0
  713. data/lib/hecks/hecks/adapters/work_areas.rb +57 -0
  714. data/lib/hecks/hecks/adapters/work_limits.rb +57 -0
  715. data/lib/hecks/hecks/adapters/world_clearing.rb +2 -1
  716. data/lib/hecks/hecks/build/build.bluebook +576 -0
  717. data/lib/hecks/hecks/check/check.bluebook +966 -0
  718. data/lib/hecks/hecks/codebase.behaviors +72 -4
  719. data/lib/hecks/hecks/composer.behaviors +10 -10
  720. data/lib/hecks/hecks/context_map.hecksagon +7 -6
  721. data/lib/hecks/hecks/custodian.behaviors +124 -3
  722. data/lib/hecks/hecks/delegate/delegate.bluebook +355 -0
  723. data/lib/hecks/hecks/deliver/deliver.bluebook +945 -0
  724. data/lib/hecks/hecks/deploy.behaviors +1 -1
  725. data/lib/hecks/hecks/deploy.world +3 -4
  726. data/lib/hecks/hecks/evolve/evolve.bluebook +788 -0
  727. data/lib/hecks/hecks/find/find.bluebook +81 -0
  728. data/lib/hecks/hecks/hecks.behaviors +2 -2
  729. data/lib/hecks/hecks/hecks.bluebook +0 -138
  730. data/lib/hecks/hecks/hecks.contextmap +27 -0
  731. data/lib/hecks/hecks/hecks.hecksagon +312 -23
  732. data/lib/hecks/hecks/hecks.world +54 -27
  733. data/lib/hecks/hecks/implicit_moves.json +24 -0
  734. data/lib/hecks/hecks/migrate/migrate.bluebook +1066 -0
  735. data/lib/hecks/hecks/operate/operate.bluebook +695 -0
  736. data/lib/hecks/hecks/quiesce/quiesce.bluebook +117 -0
  737. data/lib/hecks/hecks/rehearse/rehearse.bluebook +133 -0
  738. data/lib/hecks/hecks/release/release.bluebook +916 -0
  739. data/lib/hecks/hecks/serve/serve.bluebook +134 -0
  740. data/lib/hecks/hecks/shapes.json +3 -0
  741. data/lib/hecks/hecks/site.behaviors +1 -1
  742. data/lib/hecks/hecks/site.world +3 -3
  743. data/lib/hecks/hecks/start/start.bluebook +194 -0
  744. data/lib/hecks/hecks/test/test.bluebook +638 -0
  745. data/lib/hecks/hecks/tickets.world +14 -0
  746. data/lib/hecks/ir.rb +8 -1
  747. data/lib/hecks/kernel_capabilities/templates.rb +4 -3
  748. data/lib/hecks/kernel_capabilities.rb +6 -6
  749. data/lib/hecks/language/adapter.bluebook +61 -0
  750. data/lib/hecks/language/bluebook/aggregate.bluebook +58 -6
  751. data/lib/hecks/language/bluebook/attaches/paging.bluebook +1 -1
  752. data/lib/hecks/language/bluebook/bluebook.behaviors +6 -6
  753. data/lib/hecks/language/bluebook/bluebook.bluebook +62 -51
  754. data/lib/hecks/language/bluebook/bluebook.hecksagon +1 -1
  755. data/lib/hecks/language/bluebook/command.bluebook +69 -2
  756. data/lib/hecks/language/bluebook/entity.bluebook +2 -2
  757. data/lib/hecks/language/bluebook/policy.behaviors +2 -2
  758. data/lib/hecks/language/bluebook/policy.bluebook +3 -3
  759. data/lib/hecks/language/bluebook/process_manager.bluebook +17 -4
  760. data/lib/hecks/language/bluebook/projection.bluebook +11 -10
  761. data/lib/hecks/language/bluebook/query.bluebook +1 -0
  762. data/lib/hecks/language/bluebook/shape.bluebook +1 -1
  763. data/lib/hecks/language/bluebook/syntax.bluebook +3 -1
  764. data/lib/hecks/language/bluebook/vocabulary.bluebook +417 -16
  765. data/lib/hecks/language/context_map/context_map.bluebook +90 -0
  766. data/lib/hecks/language/hecksagon/domain_port.bluebook +3 -2
  767. data/lib/hecks/language/hecksagon/hecksagon.bluebook +8 -8
  768. data/lib/hecks/language/hecksagon/port_operation.bluebook +14 -0
  769. data/lib/hecks/language/kinds/run.kind +120 -0
  770. data/lib/hecks/language/oidc.json +20 -0
  771. data/lib/hecks/language/port.bluebook +1 -1
  772. data/lib/hecks/language/sentence/sentence.bluebook +210 -0
  773. data/lib/hecks/language/translation/translation_aggregate.bluebook +24 -0
  774. data/lib/hecks/language/world/world.bluebook +6 -6
  775. data/lib/hecks/literal.rb +8 -2
  776. data/lib/hecks/module_folders.rb +31 -0
  777. data/lib/hecks/naming.rb +44 -14
  778. data/lib/hecks/persistence_legacy_fixture/postgres_stores.rb +7 -6
  779. data/lib/hecks/persistence_legacy_fixture/regenerate.rb +6 -2
  780. data/lib/hecks/ports/access_control.rb +2 -2
  781. data/lib/hecks/ports/agent/answers.rb +2 -2
  782. data/lib/hecks/ports/agent.rb +2 -2
  783. data/lib/hecks/ports/authentication.port +6 -0
  784. data/lib/hecks/ports/authentication.rb +67 -0
  785. data/lib/hecks/ports/authorization.rb +3 -3
  786. data/lib/hecks/ports/dispatch.port +5 -0
  787. data/lib/hecks/ports/dispatch.rb +28 -0
  788. data/lib/hecks/ports/key_vault.rb +10 -7
  789. data/lib/hecks/ports/loading.rb +23 -7
  790. data/lib/hecks/ports/persistence/append_only.rb +44 -9
  791. data/lib/hecks/ports/persistence/binding_policy.rb +73 -12
  792. data/lib/hecks/ports/persistence/codec_boundary.rb +4 -3
  793. data/lib/hecks/ports/persistence/mint_door.rb +74 -0
  794. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +16 -15
  795. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +9 -6
  796. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +11 -11
  797. data/lib/hecks/ports/persistence/plugins/era/era_status.rb +48 -0
  798. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +1 -1
  799. data/lib/hecks/ports/persistence/plugins/era/projected_bluebook.rb +127 -0
  800. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +6 -6
  801. data/lib/hecks/ports/persistence/plugins/era/translation/additive_shape.rb +107 -0
  802. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -3
  803. data/lib/hecks/ports/persistence/plugins/era/translation/compose.rb +155 -0
  804. data/lib/hecks/ports/persistence/plugins/era/translation/compose_render.rb +79 -0
  805. data/lib/hecks/ports/persistence/plugins/era/translation/edge_files.rb +49 -0
  806. data/lib/hecks/ports/persistence/plugins/era/translation/edge_route.rb +61 -0
  807. data/lib/hecks/ports/persistence/plugins/era/translation/implicit_edge.rb +67 -0
  808. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +1 -1
  809. data/lib/hecks/ports/persistence/plugins/era/translation/reconcile.rb +130 -0
  810. data/lib/hecks/ports/persistence/plugins/era/translation/reconcile_files.rb +35 -0
  811. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +32 -19
  812. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +13 -5
  813. data/lib/hecks/ports/persistence/plugins/era/translation.rb +5 -0
  814. data/lib/hecks/ports/persistence/plugins/era.rb +1 -1
  815. data/lib/hecks/ports/persistence/remote_runtime.rb +3 -2
  816. data/lib/hecks/ports/persistence/repository_factory.rb +11 -3
  817. data/lib/hecks/ports/persistence/set_statement.rb +29 -0
  818. data/lib/hecks/ports/persistence/set_transforming.rb +30 -0
  819. data/lib/hecks/ports/persistence/state_codec.rb +3 -3
  820. data/lib/hecks/ports/persistence/stored_name_guard.rb +36 -0
  821. data/lib/hecks/ports/persistence.rb +2 -4
  822. data/lib/hecks/ports/projection.rb +4 -1
  823. data/lib/hecks/ports/query/cursor.rb +131 -0
  824. data/lib/hecks/ports/query/in_memory.rb +17 -10
  825. data/lib/hecks/ports/query.rb +4 -2
  826. data/lib/hecks/ports.rb +1 -0
  827. data/lib/hecks/projection_files/expression_tables.rb +3 -3
  828. data/lib/hecks/projection_files/sentence_table.rb +17 -0
  829. data/lib/hecks/projection_files.rb +16 -13
  830. data/lib/hecks/projections/bootstrap_table.rb +3 -3
  831. data/lib/hecks/projections/ci_pipeline/table/checks.rb +137 -0
  832. data/lib/hecks/projections/ci_pipeline/table/lookup.rb +59 -0
  833. data/lib/hecks/projections/ci_pipeline/table/refusals.rb +58 -0
  834. data/lib/hecks/projections/ci_pipeline/table/rows.rb +119 -0
  835. data/lib/hecks/projections/ci_pipeline/table/tree.rb +30 -0
  836. data/lib/hecks/projections/ci_pipeline/table/typing.rb +92 -0
  837. data/lib/hecks/projections/ci_pipeline/table.rb +112 -0
  838. data/lib/hecks/projections/ci_pipeline/workflows.rb +162 -0
  839. data/lib/hecks/projections/ci_pipeline/yaml_text.rb +104 -0
  840. data/lib/hecks/projections/command_schema/shape.rb +95 -0
  841. data/lib/hecks/projections/command_schema.rb +74 -0
  842. data/lib/hecks/projections/context_map_diagram.rb +135 -0
  843. data/lib/hecks/projections/context_relationships.rb +125 -0
  844. data/lib/hecks/projections/deploy/box/access.rb +7 -4
  845. data/lib/hecks/projections/deploy/box/caddy.rb +2 -1
  846. data/lib/hecks/projections/deploy/box/compose.rb +4 -8
  847. data/lib/hecks/projections/deploy/box/edge.rb +2 -1
  848. data/lib/hecks/projections/deploy/box/hosting.rb +5 -3
  849. data/lib/hecks/projections/deploy/box/hosting_parts.rb +27 -12
  850. data/lib/hecks/projections/deploy/box/logging.rb +2 -3
  851. data/lib/hecks/projections/deploy/box/roll_recipe.rb +2 -4
  852. data/lib/hecks/projections/deploy/box/roll_scripts.rb +2 -17
  853. data/lib/hecks/projections/deploy/box/settings/containers.rb +29 -15
  854. data/lib/hecks/projections/deploy/box/settings/patterns.rb +5 -6
  855. data/lib/hecks/projections/deploy/box/settings/types.rb +5 -10
  856. data/lib/hecks/projections/deploy/box/settings.rb +9 -8
  857. data/lib/hecks/projections/deploy/box/stack_yaml.rb +10 -14
  858. data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +0 -1
  859. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +16 -3
  860. data/lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl +7 -6
  861. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +2 -6
  862. data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +2 -6
  863. data/lib/hecks/projections/deploy/box.rb +17 -10
  864. data/lib/hecks/projections/deploy/fargate/assembly.rb +7 -2
  865. data/lib/hecks/projections/deploy/fargate/cdn/options.rb +4 -1
  866. data/lib/hecks/projections/deploy/fargate/check.rb +3 -1
  867. data/lib/hecks/projections/deploy/fargate/containers/rendering.rb +8 -5
  868. data/lib/hecks/projections/deploy/fargate/containers/routing.rb +2 -1
  869. data/lib/hecks/projections/deploy/fargate/monitoring/reading.rb +4 -1
  870. data/lib/hecks/projections/deploy/fargate/sections.rb +7 -4
  871. data/lib/hecks/projections/deploy/fargate/settings/ids.rb +4 -1
  872. data/lib/hecks/projections/deploy/fargate/settings/tuning.rb +13 -4
  873. data/lib/hecks/projections/deploy/fargate/stack/blocks.rb +6 -1
  874. data/lib/hecks/projections/deploy/fargate/stack/readers.rb +2 -1
  875. data/lib/hecks/projections/deploy/fargate/stack.rb +11 -6
  876. data/lib/hecks/projections/deploy/fargate/yaml.rb +10 -3
  877. data/lib/hecks/projections/deploy/fargate.rb +8 -3
  878. data/lib/hecks/projections/deploy/lambda/stack/blocks.rb +4 -2
  879. data/lib/hecks/projections/deploy/lambda/stack/deploy_chain.rb +2 -1
  880. data/lib/hecks/projections/deploy/lambda/stack/reading.rb +12 -9
  881. data/lib/hecks/projections/deploy/lambda/stack/reading_web.rb +7 -2
  882. data/lib/hecks/projections/deploy/lambda/stack/web.rb +2 -1
  883. data/lib/hecks/projections/deploy/lambda.rb +8 -3
  884. data/lib/hecks/projections/deploy/preview/containers.rb +14 -1
  885. data/lib/hecks/projections/deploy/preview/preview.sh.erb +2 -2
  886. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +8 -6
  887. data/lib/hecks/projections/deploy/preview/settings.rb +23 -5
  888. data/lib/hecks/projections/deploy/preview/task_yaml.rb +1 -1
  889. data/lib/hecks/projections/deploy/preview/yaml_text.rb +2 -1
  890. data/lib/hecks/projections/deploy/scripts/settings.rb +5 -2
  891. data/lib/hecks/projections/deploy/shared.rb +12 -1
  892. data/lib/hecks/projections/deploy/shared_database_instance.rb +1 -1
  893. data/lib/hecks/projections/deploy/smoke.rb +3 -1
  894. data/lib/hecks/projections/deploy/template_diff/normalizer.rb +4 -1
  895. data/lib/hecks/projections/deploy/templates/fargate/Makefile.tmpl +1 -1
  896. data/lib/hecks/projections/deploy/templates/lambda/Makefile.tmpl +3 -3
  897. data/lib/hecks/projections/deploy/templates/lambda/mint_era_shared.tmpl +2 -2
  898. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  899. data/lib/hecks/projections/deploy/vercel.rb +1 -1
  900. data/lib/hecks/projections/diagrams/dispatch.rb +5 -3
  901. data/lib/hecks/projections/diagrams/statecharts.rb +8 -3
  902. data/lib/hecks/projections/diagrams/structure.rb +9 -6
  903. data/lib/hecks/projections/diagrams.rb +9 -7
  904. data/lib/hecks/projections/glossary/entries.rb +4 -4
  905. data/lib/hecks/projections/glossary/html/page.rb +4 -1
  906. data/lib/hecks/projections/glossary/html/parser.rb +13 -4
  907. data/lib/hecks/projections/glossary/html/sections.rb +4 -1
  908. data/lib/hecks/projections/glossary/html.rb +10 -3
  909. data/lib/hecks/projections/glossary/markdown.rb +4 -3
  910. data/lib/hecks/projections/glossary/mermaid.rb +8 -5
  911. data/lib/hecks/projections/glossary/sections.rb +1 -1
  912. data/lib/hecks/projections/glossary/sensitivity.rb +11 -4
  913. data/lib/hecks/projections/glossary/sentences/actions.rb +5 -2
  914. data/lib/hecks/projections/glossary/sentences/queries.rb +2 -1
  915. data/lib/hecks/projections/glossary/sentences/value_objects.rb +15 -6
  916. data/lib/hecks/projections/glossary/sentences.rb +6 -3
  917. data/lib/hecks/projections/glossary/slugs.rb +8 -1
  918. data/lib/hecks/projections/glossary.rb +3 -3
  919. data/lib/hecks/projections/model.rb +10 -6
  920. data/lib/hecks/projections/oidc.rb +19 -7
  921. data/lib/hecks/projections/parser_table.rb +10 -10
  922. data/lib/hecks/projections/phrase_table.rb +105 -0
  923. data/lib/hecks/projections/plain_english.rb +14 -4
  924. data/lib/hecks/projections/rails_app.rb +77 -0
  925. data/lib/hecks/projections/reference.rb +2 -2
  926. data/lib/hecks/projections/rust_vocabulary/arguments.rb +1 -1
  927. data/lib/hecks/projections/rust_vocabulary/tables.rb +20 -0
  928. data/lib/hecks/projections/rust_vocabulary.rb +14 -17
  929. data/lib/hecks/projections/site/admin.rb +7 -7
  930. data/lib/hecks/projections/site/admin_cms.rb +1 -1
  931. data/lib/hecks/projections/site/cms_editor/blocks.rb +5 -2
  932. data/lib/hecks/projections/site/cms_editor/checks.rb +9 -6
  933. data/lib/hecks/projections/site/cms_editor/color.rb +4 -1
  934. data/lib/hecks/projections/site/cms_editor/composition.rb +20 -18
  935. data/lib/hecks/projections/site/cms_editor/destructive.rb +4 -1
  936. data/lib/hecks/projections/site/cms_editor/drafts.rb +6 -2
  937. data/lib/hecks/projections/site/cms_editor/key_kinds.rb +4 -1
  938. data/lib/hecks/projections/site/cms_editor/limits.rb +1 -1
  939. data/lib/hecks/projections/site/cms_editor/media.rb +1 -1
  940. data/lib/hecks/projections/site/cms_editor/moment.rb +3 -1
  941. data/lib/hecks/projections/site/cms_editor/pickers.rb +1 -1
  942. data/lib/hecks/projections/site/cms_editor/preview.rb +10 -4
  943. data/lib/hecks/projections/site/cms_editor/relations.rb +3 -2
  944. data/lib/hecks/projections/site/cms_editor/scale_checks.rb +32 -25
  945. data/lib/hecks/projections/site/cms_editor/scheduling.rb +1 -1
  946. data/lib/hecks/projections/site/cms_editor/schema.rb +31 -29
  947. data/lib/hecks/projections/site/cms_editor/setting.rb +34 -27
  948. data/lib/hecks/projections/site/cms_editor/targets.rb +9 -6
  949. data/lib/hecks/projections/site/cms_editor/templates/src/app.ts.tmpl +3 -3
  950. data/lib/hecks/projections/site/cms_editor/templates/src/commands.ts.tmpl +2 -2
  951. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +1 -1
  952. data/lib/hecks/projections/site/cms_editor/templates/src/media/handler.ts.tmpl +4 -4
  953. data/lib/hecks/projections/site/cms_editor/templates/src/names.ts.tmpl +14 -14
  954. data/lib/hecks/projections/site/cms_editor/templates/src/schema.ts.tmpl +12 -12
  955. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pages.ts.tmpl +12 -12
  956. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pickers.ts.tmpl +1 -1
  957. data/lib/hecks/projections/site/cms_editor/templates/src/ui/schedule.ts.tmpl +2 -2
  958. data/lib/hecks/projections/site/cms_editor/templates/src/ui/shell.ts.tmpl +11 -11
  959. data/lib/hecks/projections/site/cms_editor/templates/src/ui/world.ts.tmpl +3 -3
  960. data/lib/hecks/projections/site/cms_editor/theme_checks.rb +8 -2
  961. data/lib/hecks/projections/site/cms_editor.rb +19 -16
  962. data/lib/hecks/projections/site/edge/checks.rb +1 -1
  963. data/lib/hecks/projections/site/edge/function_checks.rb +1 -1
  964. data/lib/hecks/projections/site/edge/function_code.rb +4 -1
  965. data/lib/hecks/projections/site/edge/pattern.rb +4 -1
  966. data/lib/hecks/projections/site/edge/route_checks.rb +2 -2
  967. data/lib/hecks/projections/site/edge/rules.rb +1 -1
  968. data/lib/hecks/projections/site/edge.rb +14 -8
  969. data/lib/hecks/projections/site/live_edge.rb +2 -2
  970. data/lib/hecks/projections/site/payload/fields/definitions.rb +1 -1
  971. data/lib/hecks/projections/site/payload/lifecycle/text.rb +1 -1
  972. data/lib/hecks/projections/site/payload/model.rb +16 -12
  973. data/lib/hecks/projections/site/payload/specs.rb +4 -2
  974. data/lib/hecks/projections/site/payload_driver.rb +9 -8
  975. data/lib/hecks/projections/site/probe.rb +12 -3
  976. data/lib/hecks/projections/site/regions.rb +9 -1
  977. data/lib/hecks/projections/site/role_probe.rb +7 -7
  978. data/lib/hecks/projections/site/root_rows.rb +4 -4
  979. data/lib/hecks/projections/site/routes_ts/listings.rb +13 -6
  980. data/lib/hecks/projections/site/routes_ts.rb +6 -3
  981. data/lib/hecks/projections/site/site_admin.rb +1 -1
  982. data/lib/hecks/projections/site/site_cdn.rb +3 -2
  983. data/lib/hecks/projections/site/site_host.rb +1 -1
  984. data/lib/hecks/projections/site/site_root.rb +4 -4
  985. data/lib/hecks/projections/site/table/checks.rb +4 -1
  986. data/lib/hecks/projections/site/table/navigation_checks.rb +2 -2
  987. data/lib/hecks/projections/site/table/row_builder.rb +7 -7
  988. data/lib/hecks/projections/site/table/row_defaults.rb +1 -1
  989. data/lib/hecks/projections/site/table.rb +32 -26
  990. data/lib/hecks/projections/statements/actions.rb +4 -1
  991. data/lib/hecks/projections/statements/relationships.rb +15 -2
  992. data/lib/hecks/projections/statements.rb +2 -2
  993. data/lib/hecks/projections/undo.rb +46 -0
  994. data/lib/hecks/projections/vocabulary.rb +6 -6
  995. data/lib/hecks/projections/world_skeleton.rb +68 -0
  996. data/lib/hecks/projections.rb +7 -0
  997. data/lib/hecks/projector/cli_audience.rb +12 -9
  998. data/lib/hecks/projector/cli_projector/claims.rb +19 -8
  999. data/lib/hecks/projector/cli_projector/command_help.rb +6 -1
  1000. data/lib/hecks/projector/cli_projector/command_specs.rb +20 -6
  1001. data/lib/hecks/projector/cli_projector/completion.rb +87 -0
  1002. data/lib/hecks/projector/cli_projector/everyday.rb +40 -0
  1003. data/lib/hecks/projector/cli_projector/json_listing.rb +77 -0
  1004. data/lib/hecks/projector/cli_projector/listing.rb +20 -8
  1005. data/lib/hecks/projector/cli_projector/modules.rb +39 -0
  1006. data/lib/hecks/projector/cli_projector/option_specs.rb +1 -1
  1007. data/lib/hecks/projector/cli_projector/query_specs.rb +2 -1
  1008. data/lib/hecks/projector/cli_projector/renames.rb +58 -0
  1009. data/lib/hecks/projector/cli_projector/search.rb +74 -0
  1010. data/lib/hecks/projector/cli_projector/usage.rb +33 -12
  1011. data/lib/hecks/projector/cli_projector.rb +26 -3
  1012. data/lib/hecks/projector/docs_projector/behaviour.rb +5 -4
  1013. data/lib/hecks/projector/docs_projector/closing.rb +3 -3
  1014. data/lib/hecks/projector/docs_projector.rb +12 -7
  1015. data/lib/hecks/projector/exporter/adapters.rb +15 -2
  1016. data/lib/hecks/projector/exporter/bindings.rb +39 -134
  1017. data/lib/hecks/projector/exporter/plugins.rb +55 -0
  1018. data/lib/hecks/projector/exporter.rb +2 -0
  1019. data/lib/hecks/projector/narrate_projector/commands.rb +4 -3
  1020. data/lib/hecks/projector/narrate_projector/reactions.rb +2 -2
  1021. data/lib/hecks/projector/narrate_projector.rb +9 -8
  1022. data/lib/hecks/projector/target.rb +8 -8
  1023. data/lib/hecks/projector.rb +9 -9
  1024. data/lib/hecks/quality_control/adapters/agent.rb +2 -1
  1025. data/lib/hecks/quality_control/adapters/ci_head.adapter +8 -0
  1026. data/lib/hecks/quality_control/adapters/github_checks.adapter +1 -1
  1027. data/lib/hecks/quality_control/adapters/github_issues.adapter +1 -1
  1028. data/lib/hecks/quality_control/adapters/github_issues.rb +6 -2
  1029. data/lib/hecks/quality_control/adapters/image_roll.adapter +9 -0
  1030. data/lib/hecks/quality_control/adapters/image_roll.rb +158 -0
  1031. data/lib/hecks/quality_control/adapters/main_maintenance.adapter +8 -0
  1032. data/lib/hecks/quality_control/adapters/main_maintenance.rb +275 -0
  1033. data/lib/hecks/quality_control/adapters/pump_tools.adapter +7 -0
  1034. data/lib/hecks/quality_control/adapters/pump_tools.rb +18 -0
  1035. data/lib/hecks/quality_control/adapters/push_due_reader.adapter +8 -0
  1036. data/lib/hecks/quality_control/adapters/push_due_reader.rb +99 -0
  1037. data/lib/hecks/quality_control/adapters/qa_auto_mint.rb +183 -0
  1038. data/lib/hecks/quality_control/adapters/qa_era_mint.rb +94 -0
  1039. data/lib/hecks/quality_control/adapters/qa_quiesce_marker.rb +126 -0
  1040. data/lib/hecks/quality_control/adapters/run_keys.rb +36 -0
  1041. data/lib/hecks/quality_control/adapters/run_reader.adapter +7 -0
  1042. data/lib/hecks/quality_control/adapters/run_reader.rb +188 -0
  1043. data/lib/hecks/quality_control/adapters/target_tools.rb +9 -3
  1044. data/lib/hecks/quality_control/cli/child.rb +2 -0
  1045. data/lib/hecks/quality_control/cli/qa_discover_external_domains/scanning.rb +1 -1
  1046. data/lib/hecks/quality_control/cli/qa_domain_novelty/verdict.rb +1 -1
  1047. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +3 -3
  1048. data/lib/hecks/quality_control/cli/qa_event_pump/attached.rb +67 -0
  1049. data/lib/hecks/quality_control/cli/qa_event_pump/consumer.rb +103 -0
  1050. data/lib/hecks/quality_control/cli/qa_event_pump/cursors.rb +60 -0
  1051. data/lib/hecks/quality_control/cli/qa_event_pump/reactions.rb +46 -0
  1052. data/lib/hecks/quality_control/cli/qa_event_pump/reader.rb +30 -0
  1053. data/lib/hecks/quality_control/cli/qa_event_pump.rb +76 -0
  1054. data/lib/hecks/quality_control/cli/qa_fix_main_bug/publishing.rb +79 -0
  1055. data/lib/hecks/quality_control/cli/qa_fix_main_bug.rb +167 -0
  1056. data/lib/hecks/quality_control/cli/qa_generated_domains/evaluation.rb +13 -3
  1057. data/lib/hecks/quality_control/cli/qa_generated_domains/promotion.rb +7 -4
  1058. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +4 -1
  1059. data/lib/hecks/quality_control/cli/qa_log_bug/proof.rb +16 -3
  1060. data/lib/hecks/quality_control/cli/qa_main_gate/gate_run.rb +52 -0
  1061. data/lib/hecks/quality_control/cli/qa_main_gate/logging.rb +70 -0
  1062. data/lib/hecks/quality_control/cli/qa_main_gate/shell.rb +47 -0
  1063. data/lib/hecks/quality_control/cli/qa_main_gate/sweeping.rb +59 -0
  1064. data/lib/hecks/quality_control/cli/qa_main_gate/worktree.rb +62 -0
  1065. data/lib/hecks/quality_control/cli/qa_main_gate.rb +135 -0
  1066. data/lib/hecks/quality_control/cli/qa_mine_combinations/booting.rb +4 -2
  1067. data/lib/hecks/quality_control/cli/qa_mutate.rb +161 -0
  1068. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +1 -1
  1069. data/lib/hecks/quality_control/cli/qa_pr_check/checking.rb +2 -3
  1070. data/lib/hecks/quality_control/cli/qa_push_nudge.rb +48 -0
  1071. data/lib/hecks/quality_control/cli/qa_seed_angles/seed.rb +1 -1
  1072. data/lib/hecks/quality_control/cli/qa_sweep/all_mode/report.rb +4 -1
  1073. data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +2 -1
  1074. data/lib/hecks/quality_control/cli/qa_sweep/seat.rb +1 -1
  1075. data/lib/hecks/quality_control/cli/qa_sweep.rb +4 -1
  1076. data/lib/hecks/quality_control/cli/qa_tick/event_pump_step.rb +34 -0
  1077. data/lib/hecks/quality_control/cli/qa_tick/output.rb +43 -26
  1078. data/lib/hecks/quality_control/cli/qa_tick/preflight.rb +12 -3
  1079. data/lib/hecks/quality_control/cli/qa_tick/push_nudge_step.rb +28 -0
  1080. data/lib/hecks/quality_control/cli/qa_tick/quiesce_guard.rb +51 -0
  1081. data/lib/hecks/quality_control/cli/qa_tick.rb +52 -8
  1082. data/lib/hecks/quality_control/quality_control.behaviors +65 -0
  1083. data/lib/hecks/quality_control/quality_control.bluebook +1224 -103
  1084. data/lib/hecks/quality_control/quality_control.hecksagon +14 -3
  1085. data/lib/hecks/quality_control/quality_control.ports.hecksagon +141 -2
  1086. data/lib/hecks/query_ir/rules.rb +9 -9
  1087. data/lib/hecks/query_ir/touchpoints.rb +10 -9
  1088. data/lib/hecks/query_ir.rb +3 -3
  1089. data/lib/hecks/query_specification/common/comparison.rb +1 -1
  1090. data/lib/hecks/query_specification/common/dsl.rb +3 -4
  1091. data/lib/hecks/query_specification/hop_path.rb +7 -4
  1092. data/lib/hecks/rails_app/bluebook/rails_app.bluebook +60 -0
  1093. data/lib/hecks/rails_app/bluebook/rails_app.hecksagon +4 -0
  1094. data/lib/hecks/refused.rb +125 -0
  1095. data/lib/hecks/release/approval.rb +15 -5
  1096. data/lib/hecks/release/conditions.rb +46 -0
  1097. data/lib/hecks/release/gem_pin.rb +7 -3
  1098. data/lib/hecks/release/lane.rb +1 -1
  1099. data/lib/hecks/release/runner/clean_tree.rb +4 -1
  1100. data/lib/hecks/release/runner/console.rb +2 -1
  1101. data/lib/hecks/release/runner/gem_publisher.rb +4 -4
  1102. data/lib/hecks/release/version_commit.rb +48 -0
  1103. data/lib/hecks/router/aggregate_reader.rb +3 -4
  1104. data/lib/hecks/router/namespace_installer.rb +31 -17
  1105. data/lib/hecks/router.rb +2 -10
  1106. data/lib/hecks/runtime/adapter_lookup.rb +30 -7
  1107. data/lib/hecks/runtime/adapter_namespace.rb +21 -0
  1108. data/lib/hecks/runtime/asking.rb +30 -0
  1109. data/lib/hecks/runtime/caller.rb +22 -0
  1110. data/lib/hecks/runtime/capability_graph.rb +2 -1
  1111. data/lib/hecks/runtime/collecting.rb +86 -0
  1112. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +13 -8
  1113. data/lib/hecks/runtime/command_interpreter/delegation.rb +46 -17
  1114. data/lib/hecks/runtime/command_interpreter/hydration.rb +22 -25
  1115. data/lib/hecks/runtime/command_interpreter/list_mutations.rb +2 -1
  1116. data/lib/hecks/runtime/command_interpreter/persisting.rb +23 -14
  1117. data/lib/hecks/runtime/command_interpreter.rb +19 -12
  1118. data/lib/hecks/runtime/command_rules/admissibility.rb +17 -12
  1119. data/lib/hecks/runtime/command_rules/authorization.rb +8 -6
  1120. data/lib/hecks/runtime/command_rules/emission.rb +0 -1
  1121. data/lib/hecks/runtime/command_rules/guard_state.rb +10 -10
  1122. data/lib/hecks/runtime/command_rules/invariants.rb +3 -1
  1123. data/lib/hecks/runtime/command_rules/missing_reference.rb +25 -0
  1124. data/lib/hecks/runtime/command_rules/one_of_references.rb +44 -0
  1125. data/lib/hecks/runtime/command_rules/references.rb +14 -14
  1126. data/lib/hecks/runtime/command_rules.rb +2 -0
  1127. data/lib/hecks/runtime/declarations.rb +66 -0
  1128. data/lib/hecks/runtime/dependency_planning/analyzer.rb +2 -1
  1129. data/lib/hecks/runtime/dependency_planning/expression_reads.rb +3 -3
  1130. data/lib/hecks/runtime/dependency_planning/mutation_analysis.rb +2 -1
  1131. data/lib/hecks/runtime/dispatch_claim.rb +72 -0
  1132. data/lib/hecks/runtime/dispatch_context.rb +41 -0
  1133. data/lib/hecks/runtime/dispatcher/planning.rb +24 -0
  1134. data/lib/hecks/runtime/dispatcher/queries.rb +17 -0
  1135. data/lib/hecks/runtime/dispatcher/routing.rb +40 -12
  1136. data/lib/hecks/runtime/dispatcher/validating.rb +73 -0
  1137. data/lib/hecks/runtime/dispatcher/value_routing.rb +54 -0
  1138. data/lib/hecks/runtime/dispatcher.rb +46 -24
  1139. data/lib/hecks/runtime/dry_run_plan.rb +60 -0
  1140. data/lib/hecks/runtime/entity_element/location.rb +15 -8
  1141. data/lib/hecks/runtime/entity_element/mutations.rb +4 -2
  1142. data/lib/hecks/runtime/entity_interpreter/enforcement.rb +1 -1
  1143. data/lib/hecks/runtime/entity_interpreter/locating.rb +6 -1
  1144. data/lib/hecks/runtime/entity_interpreter/saving.rb +5 -5
  1145. data/lib/hecks/runtime/entity_interpreter.rb +11 -5
  1146. data/lib/hecks/runtime/errors.rb +56 -17
  1147. data/lib/hecks/runtime/identity.rb +9 -0
  1148. data/lib/hecks/runtime/instance.rb +3 -3
  1149. data/lib/hecks/runtime/interpreting.rb +63 -6
  1150. data/lib/hecks/runtime/invocation_builder.rb +7 -5
  1151. data/lib/hecks/runtime/invocation_facts.rb +13 -8
  1152. data/lib/hecks/runtime/loader/boot_steps.rb +3 -2
  1153. data/lib/hecks/runtime/loader/described.rb +4 -0
  1154. data/lib/hecks/runtime/loader.rb +52 -62
  1155. data/lib/hecks/runtime/outbox/consumer_runner.rb +9 -1
  1156. data/lib/hecks/runtime/outbox/redrive.rb +105 -0
  1157. data/lib/hecks/runtime/outbox/relay.rb +36 -43
  1158. data/lib/hecks/runtime/outbox/runnable.rb +62 -0
  1159. data/lib/hecks/runtime/outbox.rb +21 -3
  1160. data/lib/hecks/runtime/policy_interpreter/arguments.rb +7 -1
  1161. data/lib/hecks/runtime/policy_interpreter/fan_out.rb +26 -37
  1162. data/lib/hecks/runtime/policy_interpreter.rb +25 -9
  1163. data/lib/hecks/runtime/port_operation_interpreter.rb +47 -19
  1164. data/lib/hecks/runtime/query_interpreter/arguments.rb +6 -5
  1165. data/lib/hecks/runtime/query_interpreter/outside_answer.rb +1 -1
  1166. data/lib/hecks/runtime/query_interpreter/paging.rb +42 -0
  1167. data/lib/hecks/runtime/query_interpreter/plugin_answer.rb +54 -0
  1168. data/lib/hecks/runtime/query_interpreter/store_specification.rb +32 -0
  1169. data/lib/hecks/runtime/query_interpreter.rb +16 -9
  1170. data/lib/hecks/runtime/reaction_invocation/target_resolution.rb +10 -4
  1171. data/lib/hecks/runtime/reaction_invocation.rb +5 -3
  1172. data/lib/hecks/runtime/read_model_interpreter/head_ordering.rb +1 -1
  1173. data/lib/hecks/runtime/read_model_interpreter/reductions.rb +23 -15
  1174. data/lib/hecks/runtime/read_model_interpreter/shaping.rb +18 -7
  1175. data/lib/hecks/runtime/read_model_interpreter.rb +5 -2
  1176. data/lib/hecks/runtime/reference_hop.rb +3 -2
  1177. data/lib/hecks/runtime/refusal_wording.rb +27 -7
  1178. data/lib/hecks/runtime/registry/adapter_capabilities.rb +43 -0
  1179. data/lib/hecks/runtime/registry/ask_verification.rb +15 -4
  1180. data/lib/hecks/runtime/registry/{chapter_verification.rb → bluebook_verification.rb} +23 -23
  1181. data/lib/hecks/runtime/registry/context_maps.rb +26 -0
  1182. data/lib/hecks/runtime/registry/declarations.rb +38 -6
  1183. data/lib/hecks/runtime/registry/durability_warnings.rb +18 -2
  1184. data/lib/hecks/runtime/registry/hecksagon_rules.rb +42 -25
  1185. data/lib/hecks/runtime/registry/plugin_query_verification.rb +45 -0
  1186. data/lib/hecks/runtime/registry/providers.rb +34 -99
  1187. data/lib/hecks/runtime/registry/query_verification.rb +30 -21
  1188. data/lib/hecks/runtime/registry/runtime_state.rb +14 -3
  1189. data/lib/hecks/runtime/registry/saga_persistence.rb +7 -16
  1190. data/lib/hecks/runtime/registry/verification.rb +9 -5
  1191. data/lib/hecks/runtime/registry/world_defaults.rb +31 -6
  1192. data/lib/hecks/runtime/registry/world_names.rb +99 -0
  1193. data/lib/hecks/runtime/registry.rb +84 -53
  1194. data/lib/hecks/runtime/row_sweep.rb +104 -0
  1195. data/lib/hecks/runtime/saga_interpreter/compensation.rb +15 -4
  1196. data/lib/hecks/runtime/saga_interpreter/correlation.rb +29 -3
  1197. data/lib/hecks/runtime/saga_interpreter/delivery.rb +13 -11
  1198. data/lib/hecks/runtime/saga_interpreter/lifecycle.rb +1 -1
  1199. data/lib/hecks/runtime/saga_interpreter/redrive.rb +141 -0
  1200. data/lib/hecks/runtime/saga_interpreter/transitions.rb +35 -6
  1201. data/lib/hecks/runtime/saga_interpreter.rb +82 -6
  1202. data/lib/hecks/runtime/saga_pending_dispatch.rb +4 -2
  1203. data/lib/hecks/runtime/scalar_field.rb +73 -0
  1204. data/lib/hecks/runtime/tenant_scope.rb +2 -1
  1205. data/lib/hecks/runtime/validation.rb +24 -0
  1206. data/lib/hecks/runtime/value/admission.rb +11 -11
  1207. data/lib/hecks/runtime/value/coercion.rb +43 -15
  1208. data/lib/hecks/runtime/value/field_checks.rb +13 -13
  1209. data/lib/hecks/runtime/value/invariant_violation.rb +4 -1
  1210. data/lib/hecks/runtime/value/references.rb +7 -4
  1211. data/lib/hecks/runtime/value/validation.rb +7 -4
  1212. data/lib/hecks/runtime/wasm_plugin.rb +98 -0
  1213. data/lib/hecks/runtime.rb +14 -14
  1214. data/lib/hecks/rust_build/command_schemas.rb +27 -0
  1215. data/lib/hecks/rust_build/conformance/surface.rb +50 -7
  1216. data/lib/hecks/rust_build/conformance.rb +41 -8
  1217. data/lib/hecks/rust_build/conformance_fuzz.rb +2 -2
  1218. data/lib/hecks/rust_build/domain_name.rb +4 -1
  1219. data/lib/hecks/rust_build/host.rb +15 -9
  1220. data/lib/hecks/rust_build/native_build.rb +2 -1
  1221. data/lib/hecks/rust_build/parser.rb +25 -0
  1222. data/lib/hecks/rust_build/prebuilt_binary.rb +25 -5
  1223. data/lib/hecks/rust_build/project_rust/codegen_run.rb +51 -17
  1224. data/lib/hecks/rust_build/project_rust/declared.rb +80 -0
  1225. data/lib/hecks/rust_build/project_rust/domain_loader.rb +15 -2
  1226. data/lib/hecks/rust_build/project_rust/features_header.rb +22 -0
  1227. data/lib/hecks/rust_build/project_rust/manifest.rb +96 -0
  1228. data/lib/hecks/rust_build/project_rust/meta.rb +36 -0
  1229. data/lib/hecks/rust_build/project_rust/root_mod/notes.rb +1 -1
  1230. data/lib/hecks/rust_build/project_rust/target_ir.rb +12 -3
  1231. data/lib/hecks/rust_build/project_rust.rb +31 -73
  1232. data/lib/hecks/rust_build/target_dir.rb +40 -0
  1233. data/lib/hecks/rust_build/wasm.rb +46 -17
  1234. data/lib/hecks/rust_build/wasm_browser.rb +8 -2
  1235. data/lib/hecks/rust_build/write_if_changed.rb +19 -4
  1236. data/lib/hecks/rust_build.rb +31 -3
  1237. data/lib/hecks/rust_format.rb +32 -2
  1238. data/lib/hecks/secrets.rb +111 -0
  1239. data/lib/hecks/sentence_form.rb +212 -0
  1240. data/lib/hecks/served_options.rb +2 -1
  1241. data/lib/hecks/shape_ledger/edge_hygiene.rb +170 -0
  1242. data/lib/hecks/shape_ledger/scope.rb +51 -0
  1243. data/lib/hecks/shape_ledger/status.rb +89 -0
  1244. data/lib/hecks/shape_ledger.rb +194 -0
  1245. data/lib/hecks/site/bluebook/site.bluebook +40 -62
  1246. data/lib/hecks/site/bluebook/site.ports.hecksagon +1 -1
  1247. data/lib/hecks/sme/bluebook/sme.bluebook +7 -1
  1248. data/lib/hecks/storehouse/audit.rb +38 -5
  1249. data/lib/hecks/storehouse/bluebook_words.rb +35 -0
  1250. data/lib/hecks/storehouse/callers.rb +8 -0
  1251. data/lib/hecks/storehouse/dispatching.rb +20 -6
  1252. data/lib/hecks/storehouse/identity.rb +41 -0
  1253. data/lib/hecks/storehouse/introspection.rb +4 -4
  1254. data/lib/hecks/storehouse/querying.rb +9 -9
  1255. data/lib/hecks/storehouse.rb +38 -10
  1256. data/lib/hecks/switches.rb +21 -0
  1257. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +5 -9
  1258. data/lib/hecks/text.rb +57 -0
  1259. data/lib/hecks/tickets/bluebook/adapters/work_areas.adapter +7 -0
  1260. data/lib/hecks/tickets/bluebook/adapters/work_limits.adapter +7 -0
  1261. data/lib/hecks/tickets/bluebook/tickets.behaviors +70 -0
  1262. data/lib/hecks/tickets/bluebook/tickets.bluebook +350 -76
  1263. data/lib/hecks/tickets/bluebook/tickets.hecksagon +8 -1
  1264. data/lib/hecks/tickets/bluebook/tickets.ports.hecksagon +18 -1
  1265. data/lib/hecks/tools/argument_gate_matrix/dispatching.rb +1 -1
  1266. data/lib/hecks/tools/argument_gate_matrix/rows.rb +7 -7
  1267. data/lib/hecks/tools/argument_gate_matrix/violations.rb +1 -1
  1268. data/lib/hecks/tools/argument_gate_matrix.rb +3 -3
  1269. data/lib/hecks/{cli → tools}/behaviors.rb +1 -1
  1270. data/lib/hecks/{cli → tools}/canonicalise.rb +1 -1
  1271. data/lib/hecks/{cli → tools}/check_engine_agreement.rb +1 -1
  1272. data/lib/hecks/{cli → tools}/check_era.rb +7 -7
  1273. data/lib/hecks/tools/ci_gate_decision.rb +5 -2
  1274. data/lib/hecks/tools/ci_gates.rb +9 -3
  1275. data/lib/hecks/tools/ci_workflows.rb +98 -0
  1276. data/lib/hecks/tools/comment_style/caps_words.rb +11 -8
  1277. data/lib/hecks/tools/comment_style/dictionary.rb +7 -2
  1278. data/lib/hecks/tools/comment_style/fixing.rb +10 -5
  1279. data/lib/hecks/tools/comment_style/long_blocks.rb +1 -1
  1280. data/lib/hecks/tools/comment_style/masking.rb +5 -5
  1281. data/lib/hecks/tools/comment_style/run.rb +4 -1
  1282. data/lib/hecks/tools/comment_style/source_file.rb +10 -3
  1283. data/lib/hecks/tools/comment_style/structure_checks.rb +13 -4
  1284. data/lib/hecks/tools/comment_style/text_checks.rb +1 -1
  1285. data/lib/hecks/tools/comment_style.rb +25 -2
  1286. data/lib/hecks/{cli → tools}/console.rb +12 -7
  1287. data/lib/hecks/tools/deploy_recipe/generation.rb +1 -1
  1288. data/lib/hecks/tools/deploy_recipe/targets.rb +1 -1
  1289. data/lib/hecks/tools/deploy_recipe/worlds.rb +15 -9
  1290. data/lib/hecks/tools/deploy_recipe.rb +17 -10
  1291. data/lib/hecks/tools/deploy_recipe_lint/fixtures.rb +1 -1
  1292. data/lib/hecks/tools/deploy_recipe_lint/makefile.rb +7 -1
  1293. data/lib/hecks/{cli → tools}/deploy_template_diff.rb +1 -1
  1294. data/lib/hecks/{cli → tools}/doc_coverage.rb +2 -2
  1295. data/lib/hecks/{cli → tools}/document.rb +15 -10
  1296. data/lib/hecks/{cli → tools}/domain_stub.rb +1 -1
  1297. data/lib/hecks/{cli → tools}/domain_writer.rb +5 -2
  1298. data/lib/hecks/tools/downstream_model_check.rb +122 -0
  1299. data/lib/hecks/tools/drop_implicit_append_fields.rb +4 -1
  1300. data/lib/hecks/tools/era_projection_backfill.rb +11 -6
  1301. data/lib/hecks/tools/era_reattest.rb +11 -6
  1302. data/lib/hecks/tools/evolve_run.rb +2 -2
  1303. data/lib/hecks/tools/expected_era.rb +139 -0
  1304. data/lib/hecks/{cli → tools}/expression_projection.rb +3 -3
  1305. data/lib/hecks/{cli → tools}/follow.rb +4 -4
  1306. data/lib/hecks/tools/fuzz_sweep/findings.rb +18 -2
  1307. data/lib/hecks/tools/fuzz_sweep/options.rb +45 -6
  1308. data/lib/hecks/tools/fuzz_sweep/pool.rb +12 -1
  1309. data/lib/hecks/tools/fuzz_sweep/shards.rb +155 -0
  1310. data/lib/hecks/tools/fuzz_sweep.rb +28 -10
  1311. data/lib/hecks/tools/gate.rb +65 -34
  1312. data/lib/hecks/tools/gate_memo/local_files.rb +33 -0
  1313. data/lib/hecks/tools/gate_memo/rehearsal.rb +54 -0
  1314. data/lib/hecks/tools/gate_memo/ruby_files.rb +152 -0
  1315. data/lib/hecks/tools/gate_memo/session.rb +114 -0
  1316. data/lib/hecks/tools/gate_memo/standalone.rb +67 -0
  1317. data/lib/hecks/tools/gate_memo.rb +196 -0
  1318. data/lib/hecks/tools/gate_report.rb +56 -0
  1319. data/lib/hecks/{cli → tools}/history.rb +5 -2
  1320. data/lib/hecks/tools/hoist_local_givens/rewriting.rb +3 -2
  1321. data/lib/hecks/tools/hoist_local_givens.rb +2 -4
  1322. data/lib/hecks/{cli → tools}/interview_agent.rb +1 -1
  1323. data/lib/hecks/{cli → tools}/interview_aggregate.rb +1 -1
  1324. data/lib/hecks/{cli → tools}/interview_draft.rb +9 -2
  1325. data/lib/hecks/{cli → tools}/interview_record.rb +8 -4
  1326. data/lib/hecks/{cli → tools}/interview_run.rb +9 -7
  1327. data/lib/hecks/{cli → tools}/interview_session/agent_state.rb +1 -1
  1328. data/lib/hecks/{cli → tools}/interview_session/catalog.rb +1 -1
  1329. data/lib/hecks/{cli → tools}/interview_session/manual_findings.rb +1 -1
  1330. data/lib/hecks/{cli → tools}/interview_session/proposals.rb +6 -3
  1331. data/lib/hecks/{cli → tools}/interview_session.rb +5 -4
  1332. data/lib/hecks/tools/interview_shape/lifecycle.rb +92 -0
  1333. data/lib/hecks/{cli → tools}/interview_shape.rb +20 -74
  1334. data/lib/hecks/{cli → tools}/ir.rb +1 -1
  1335. data/lib/hecks/tools/lanes/files.rb +1 -1
  1336. data/lib/hecks/tools/lanes/prebuilt_workflow.rb +1 -1
  1337. data/lib/hecks/tools/lanes/rows.rb +9 -3
  1338. data/lib/hecks/tools/lanes/ruleset.rb +1 -1
  1339. data/lib/hecks/tools/lanes/watch_workflow.rb +7 -9
  1340. data/lib/hecks/tools/lanes/workflow.rb +2 -2
  1341. data/lib/hecks/tools/lanes.rb +2 -2
  1342. data/lib/hecks/{cli → tools}/mcp.rb +1 -1
  1343. data/lib/hecks/tools/model_check/examination.rb +142 -0
  1344. data/lib/hecks/{cli → tools}/model_check/sources.rb +5 -3
  1345. data/lib/hecks/{cli → tools}/model_check.rb +2 -2
  1346. data/lib/hecks/tools/move_disables/reasons.rb +35 -0
  1347. data/lib/hecks/tools/move_disables.rb +211 -0
  1348. data/lib/hecks/tools/move_former_name.rb +105 -0
  1349. data/lib/hecks/tools/oidc_manifests.rb +16 -19
  1350. data/lib/hecks/{cli → tools}/pattern_cases.rb +1 -1
  1351. data/lib/hecks/tools/persistence_notice.rb +45 -0
  1352. data/lib/hecks/{cli → tools}/present.rb +2 -2
  1353. data/lib/hecks/{cli → tools}/project_cli/launcher_source.rb +39 -27
  1354. data/lib/hecks/{cli → tools}/project_cli/templates.rb +42 -14
  1355. data/lib/hecks/{cli → tools}/project_cli.rb +22 -20
  1356. data/lib/hecks/tools/project_diagrams.rb +59 -0
  1357. data/lib/hecks/{cli → tools}/project_glossary.rb +16 -16
  1358. data/lib/hecks/{cli → tools}/reference.rb +3 -3
  1359. data/lib/hecks/{cli → tools}/refresh_projections.rb +17 -12
  1360. data/lib/hecks/{cli → tools}/refresh_rspec_runtime_baseline.rb +1 -1
  1361. data/lib/hecks/tools/regeneration_run/pool.rb +148 -0
  1362. data/lib/hecks/tools/regeneration_run/scratch.rb +76 -0
  1363. data/lib/hecks/tools/regeneration_run.rb +134 -107
  1364. data/lib/hecks/{cli → tools}/release.rb +1 -1
  1365. data/lib/hecks/{cli → tools}/release_gem.rb +1 -1
  1366. data/lib/hecks/tools/release_only.rb +5 -2
  1367. data/lib/hecks/{cli → tools}/rspec_io_parallel_files.rb +1 -1
  1368. data/lib/hecks/{cli → tools}/rspec_shard_files.rb +1 -1
  1369. data/lib/hecks/{cli → tools}/run.rb +36 -7
  1370. data/lib/hecks/{cli → tools}/run_expectations.rb +1 -1
  1371. data/lib/hecks/tools/rust_comment_style/caps_words.rb +5 -5
  1372. data/lib/hecks/tools/rust_comment_style/fixing.rb +1 -1
  1373. data/lib/hecks/tools/rust_comment_style/masking.rb +7 -4
  1374. data/lib/hecks/tools/rust_comment_style/source_file.rb +7 -2
  1375. data/lib/hecks/tools/rust_comment_style/text_checks.rb +1 -1
  1376. data/lib/hecks/tools/rust_comment_style/tokenizer.rb +4 -1
  1377. data/lib/hecks/{cli → tools}/seed_semantics_corpus.rb +18 -8
  1378. data/lib/hecks/{cli → tools}/serve_query_ir_mcp.rb +1 -1
  1379. data/lib/hecks/{cli → tools}/shape.rb +6 -6
  1380. data/lib/hecks/tools/site_routes/editor_files.rb +16 -13
  1381. data/lib/hecks/tools/site_routes/root_files.rb +14 -13
  1382. data/lib/hecks/tools/site_routes/templates.rb +5 -4
  1383. data/lib/hecks/tools/site_routes.rb +18 -15
  1384. data/lib/hecks/{cli → tools}/smoke_http/checks.rb +1 -1
  1385. data/lib/hecks/{cli → tools}/smoke_http/settings.rb +1 -1
  1386. data/lib/hecks/{cli → tools}/smoke_http/transport.rb +4 -3
  1387. data/lib/hecks/{cli → tools}/smoke_http.rb +1 -1
  1388. data/lib/hecks/{cli → tools}/smoke_test.rb +1 -1
  1389. data/lib/hecks/{cli → tools}/spec_example.rb +1 -1
  1390. data/lib/hecks/tools/stage_progress.rb +54 -0
  1391. data/lib/hecks/{cli → tools}/statements.rb +8 -8
  1392. data/lib/hecks/tools/stores.rb +123 -0
  1393. data/lib/hecks/{cli → tools}/stress_concurrency_specs/reporting.rb +5 -2
  1394. data/lib/hecks/{cli → tools}/stress_concurrency_specs/usage.rb +4 -4
  1395. data/lib/hecks/{cli → tools}/stress_concurrency_specs.rb +3 -3
  1396. data/lib/hecks/tools/tail_merge.rb +3 -3
  1397. data/lib/hecks/tools/tenant_provisioning.rb +29 -6
  1398. data/lib/hecks/tools/tools_doc.rb +22 -20
  1399. data/lib/hecks/tools/translation_audit/domain_loading.rb +3 -3
  1400. data/lib/hecks/tools/translation_audit/edges.rb +21 -16
  1401. data/lib/hecks/tools/translation_audit/verdicts.rb +1 -1
  1402. data/lib/hecks/tools/translation_audit.rb +16 -5
  1403. data/lib/hecks/tools/translation_scaffold/reporting.rb +4 -1
  1404. data/lib/hecks/tools/translation_scaffold.rb +5 -15
  1405. data/lib/hecks/tools.rb +4 -1
  1406. data/lib/hecks/vendoring/git_source.rb +7 -4
  1407. data/lib/hecks/version.rb +1 -1
  1408. data/lib/hecks/vocabulary.rb +147 -10
  1409. data/lib/hecks/workflow_pagination/judge.rb +48 -0
  1410. data/lib/hecks/workflow_pagination/statement.rb +50 -0
  1411. data/lib/hecks/workflow_pagination.rb +58 -0
  1412. data/lib/hecks.rb +39 -18
  1413. data/lib/rubocop/cop/hecks/bare_event_and_query_names.rb +58 -0
  1414. data/lib/rubocop/cop/hecks/correlates_by_aggregate.rb +39 -0
  1415. data/lib/rubocop/cop/hecks/creating_command_sets.rb +75 -0
  1416. data/lib/rubocop/cop/hecks/inline_regexp.rb +29 -0
  1417. data/lib/rubocop/cop/hecks/invariant_expression_language.rb +47 -0
  1418. data/lib/rubocop/cop/hecks/lifecycle_needs_attribute.rb +58 -0
  1419. data/lib/rubocop/cop/hecks/no_id_string_attribute.rb +49 -0
  1420. data/lib/rubocop/cop/hecks/no_transport_in_runtime.rb +78 -0
  1421. data/lib/rubocop/cop/hecks/one_terminal_mark_per_lifecycle.rb +47 -0
  1422. data/lib/rubocop/cop/hecks/rails_model_rules.rb +46 -0
  1423. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +1 -1
  1424. data/lib/rubocop/cop/hecks/train_wreck.rb +69 -0
  1425. data/lib/rubocop/cop/hecks/unnamed_pipeline.rb +51 -0
  1426. data/lib/rubocop/cop/hecks/value_object_shorthand.rb +56 -0
  1427. data/prebuilt/13c04bdc5970295b310f0a980d397ba722232c8da99cc1a0cdb383249921e871.marshal +0 -0
  1428. data/prebuilt/e3857b77f47d9ecc1787b2e1f5136364f3cb3444ef4aea5479f3b98167affea2.marshal +0 -0
  1429. data/qa/settings.yml +32 -0
  1430. data/rust/.gitignore +0 -6
  1431. data/rust/Cargo.lock +8 -1
  1432. data/rust/Cargo.toml +8 -8
  1433. data/rust/build/Cargo.toml +1 -1
  1434. data/rust/build/src/binding_pass.rs +68 -19
  1435. data/rust/build/src/cargo_sync.rs +1 -1
  1436. data/rust/build/src/lineage_pass.rs +4 -71
  1437. data/rust/build/src/pipeline.rs +104 -73
  1438. data/rust/build/src/resolve.rs +86 -29
  1439. data/rust/build/src/seams_pass.rs +117 -99
  1440. data/rust/build/src/world_pass.rs +21 -12
  1441. data/rust/codegen/Cargo.toml +1 -1
  1442. data/rust/codegen/src/domain_generator.rs +27 -2
  1443. data/rust/codegen/src/expr_emitter.rs +6 -0
  1444. data/rust/codegen/src/json_codec.rs +230 -33
  1445. data/rust/codegen/src/main.rs +35 -34
  1446. data/rust/codegen/src/manifest.rs +1 -1
  1447. data/rust/codegen/src/naming.rs +5 -5
  1448. data/rust/codegen/src/one_of_references.rs +125 -0
  1449. data/rust/codegen/src/presenters.rs +3 -3
  1450. data/rust/codegen/src/queries.rs +76 -9
  1451. data/rust/codegen/src/reactions.rs +19 -14
  1452. data/rust/codegen/src/read_models.rs +19 -14
  1453. data/rust/codegen/src/registry.rs +110 -20
  1454. data/rust/codegen/src/sidecars.rs +1 -1
  1455. data/rust/expr/Cargo.lock +54 -0
  1456. data/rust/expr/Cargo.toml +52 -0
  1457. data/rust/{src/kernel → expr/src}/attribute_shapes/composite.rs +45 -14
  1458. data/rust/{src/kernel → expr/src}/attribute_shapes/list.rs +1 -1
  1459. data/rust/{src/kernel → expr/src}/attribute_shapes/mod.rs +2 -2
  1460. data/rust/{src/kernel → expr/src}/attribute_shapes/optional.rs +1 -1
  1461. data/rust/{src/kernel → expr/src}/attribute_shapes/scalar.rs +3 -3
  1462. data/rust/{src/kernel → expr/src}/expr.rs +30 -8
  1463. data/rust/{src/kernel → expr/src}/expression_operators/arithmetic.rs +41 -18
  1464. data/rust/{src/kernel → expr/src}/expression_operators/comparison.rs +54 -8
  1465. data/rust/{src/kernel → expr/src}/expression_operators/enumeration.rs +73 -6
  1466. data/rust/{src/kernel → expr/src}/expression_operators/logical.rs +2 -2
  1467. data/rust/expr/src/expression_operators/membership.rs +33 -0
  1468. data/rust/{src/kernel → expr/src}/expression_operators/mod.rs +2 -2
  1469. data/rust/expr/src/expression_operators/pattern_match.rs +179 -0
  1470. data/rust/{src/kernel → expr/src}/expression_operators/positional.rs +8 -6
  1471. data/rust/{src/kernel → expr/src}/expression_operators/presence.rs +7 -6
  1472. data/rust/{src/kernel → expr/src}/expression_operators/sign_test.rs +14 -4
  1473. data/rust/{src/kernel → expr/src}/expression_operators/sized.rs +16 -9
  1474. data/rust/{src/kernel → expr/src}/expression_operators/text.rs +30 -17
  1475. data/rust/{src/kernel → expr/src}/expression_operators/to_string.rs +8 -6
  1476. data/rust/expr/src/lib.rs +17 -0
  1477. data/rust/expr/src/refusal.rs +62 -0
  1478. data/rust/expr/src/rendering.rs +135 -0
  1479. data/rust/{src/kernel → expr/src}/ruby_float.rs +7 -6
  1480. data/rust/host/Cargo.lock +137 -123
  1481. data/rust/host/Cargo.toml +8 -4
  1482. data/rust/host/HECKS_RELEASE +1 -1
  1483. data/rust/host/core/Cargo.toml +64 -0
  1484. data/rust/host/core/clippy.toml +4 -0
  1485. data/rust/host/core/src/arg_limits.rs +48 -0
  1486. data/rust/host/core/src/clock.rs +146 -0
  1487. data/rust/host/core/src/dispatch.rs +920 -0
  1488. data/rust/host/core/src/expr_json.rs +1137 -0
  1489. data/rust/host/{src → core/src}/host.rs +13 -14
  1490. data/rust/host/{src → core/src}/ir.rs +227 -18
  1491. data/rust/host/core/src/journal.rs +126 -0
  1492. data/rust/host/{src → core/src}/lambda_client.rs +27 -96
  1493. data/rust/host/core/src/lib.rs +37 -0
  1494. data/rust/host/{src → core/src}/needs.rs +66 -20
  1495. data/rust/host/core/src/plugins.rs +269 -0
  1496. data/rust/host/core/src/presentation.rs +471 -0
  1497. data/rust/host/{src → core/src}/presentation_write.rs +75 -43
  1498. data/rust/host/{src → core/src}/query_step.rs +54 -16
  1499. data/rust/host/{src → core/src}/reference_validate.rs +6 -6
  1500. data/rust/host/{src → core/src}/storage_shape.rs +2 -2
  1501. data/rust/host/core/src/store/memory.rs +635 -0
  1502. data/rust/host/core/src/store.rs +418 -0
  1503. data/rust/host/{src → core/src}/ui_schema.rs +8 -8
  1504. data/rust/host/core/src/validation.rs +129 -0
  1505. data/rust/host/core/src/wasm_runner.rs +410 -0
  1506. data/rust/host/fuzz/.gitignore +4 -0
  1507. data/rust/host/fuzz/Cargo.lock +4479 -0
  1508. data/rust/host/fuzz/Cargo.toml +42 -0
  1509. data/rust/host/fuzz/README.md +55 -0
  1510. data/rust/host/fuzz/fuzz_targets/postgres_equivalence.rs +24 -0
  1511. data/rust/host/fuzz/src/backend.rs +273 -0
  1512. data/rust/host/fuzz/src/equivalence.rs +195 -0
  1513. data/rust/host/fuzz/src/footprint.rs +182 -0
  1514. data/rust/host/fuzz/src/lib.rs +17 -0
  1515. data/rust/host/fuzz/src/ops.rs +345 -0
  1516. data/rust/host/fuzz/tests/smoke.rs +50 -0
  1517. data/rust/host/src/api.rs +316 -105
  1518. data/rust/host/src/auth/google_signin.rs +183 -0
  1519. data/rust/host/src/auth.rs +527 -370
  1520. data/rust/host/src/aws_lambda_invoker.rs +61 -0
  1521. data/rust/host/src/bin/expr_harness.rs +9 -4
  1522. data/rust/host/src/bin/ledger.rs +84 -0
  1523. data/rust/host/src/bin/lineage_harness.rs +17 -21
  1524. data/rust/host/src/bin/mint_harness.rs +12 -36
  1525. data/rust/host/src/bin/validate_harness.rs +1 -6
  1526. data/rust/host/src/boot.rs +466 -174
  1527. data/rust/host/src/boundary_fuzz/ir.rs +10 -5
  1528. data/rust/host/src/boundary_fuzz/server.rs +6 -5
  1529. data/rust/host/src/checkout.rs +4 -1
  1530. data/rust/host/src/commerce_ir.rs +126 -105
  1531. data/rust/host/src/{dispatch.rs → dispatch_tests.rs} +367 -847
  1532. data/rust/host/src/extension.rs +3 -4
  1533. data/rust/host/src/hecksagon_boundary_tests.rs +149 -0
  1534. data/rust/host/src/ledger.rs +683 -0
  1535. data/rust/host/src/lib.rs +21 -28
  1536. data/rust/host/src/payments/tests.rs +136 -38
  1537. data/rust/host/src/payments.rs +36 -32
  1538. data/rust/host/src/platform_signin/extension.rs +1 -1
  1539. data/rust/host/src/platform_signin/mapping_tests.rs +12 -9
  1540. data/rust/host/src/platform_signin/session.rs +7 -14
  1541. data/rust/host/src/platform_signin/verify.rs +0 -2
  1542. data/rust/host/src/presentation_tests.rs +382 -0
  1543. data/rust/host/src/rate_limit.rs +18 -2
  1544. data/rust/host/src/server.rs +222 -43
  1545. data/rust/host/src/{journal.rs → store/journal_sql.rs} +21 -127
  1546. data/rust/host/src/{mint.rs → store/mint.rs} +52 -18
  1547. data/rust/host/src/store/postgres.rs +493 -0
  1548. data/rust/host/src/store.rs +97 -0
  1549. data/rust/host/src/store_conformance.rs +1265 -0
  1550. data/rust/host/src/web/commerce.rs +10 -18
  1551. data/rust/host/src/web/newsletter/cooldown.rs +107 -22
  1552. data/rust/host/src/web/newsletter/naming.rs +104 -0
  1553. data/rust/host/src/web/newsletter/tests.rs +53 -0
  1554. data/rust/host/src/web/newsletter.rs +73 -50
  1555. data/rust/host/src/web/newsletter_send.rs +12 -13
  1556. data/rust/host/src/web/registration_receipt.rs +2 -2
  1557. data/rust/host/src/web/registrations.rs +754 -375
  1558. data/rust/host/src/web/resend_webhook.rs +3 -3
  1559. data/rust/host/src/web/signin_tests.rs +131 -0
  1560. data/rust/host/src/web.rs +1479 -149
  1561. data/rust/lsp/Cargo.toml +1 -1
  1562. data/rust/lsp/README.md +27 -27
  1563. data/rust/lsp/src/diagnostics.rs +75 -15
  1564. data/rust/lsp/src/main.rs +101 -5
  1565. data/rust/parser/Cargo.toml +1 -1
  1566. data/rust/parser/fuzz/Cargo.toml +2 -2
  1567. data/rust/parser/fuzz/README.md +3 -3
  1568. data/rust/parser/fuzz/fuzz_targets/{chapter.rs → bluebook.rs} +4 -4
  1569. data/rust/parser/fuzz/fuzz_targets/resolve.rs +2 -2
  1570. data/rust/parser/src/build/correlation_keys.rs +125 -0
  1571. data/rust/parser/src/build/mod.rs +1 -0
  1572. data/rust/parser/src/build/named_pattern.rs +22 -0
  1573. data/rust/parser/src/build/references.rs +52 -0
  1574. data/rust/parser/src/diag.rs +22 -1
  1575. data/rust/parser/src/emit.rs +122 -6
  1576. data/rust/parser/src/expr/ast_json.rs +10 -0
  1577. data/rust/parser/src/expr/resolver.rs +49 -9
  1578. data/rust/parser/src/ir.rs +66 -7
  1579. data/rust/parser/src/keywords.rs +704 -419
  1580. data/rust/parser/src/lex.rs +115 -87
  1581. data/rust/parser/src/main.rs +182 -32
  1582. data/rust/parser/src/parse/adapter.rs +28 -0
  1583. data/rust/parser/src/parse/aggregate.rs +64 -35
  1584. data/rust/parser/src/parse/audience.rs +65 -0
  1585. data/rust/parser/src/parse/command.rs +114 -15
  1586. data/rust/parser/src/parse/{chapter.rs → declaration.rs} +182 -54
  1587. data/rust/parser/src/parse/domain_port.rs +66 -9
  1588. data/rust/parser/src/parse/entity.rs +35 -32
  1589. data/rust/parser/src/parse/file.rs +1 -1
  1590. data/rust/parser/src/parse/hecksagon.rs +15 -5
  1591. data/rust/parser/src/parse/mod.rs +85 -2
  1592. data/rust/parser/src/parse/process_manager.rs +18 -11
  1593. data/rust/parser/src/parse/query.rs +13 -3
  1594. data/rust/parser/src/parse/read_model.rs +2 -1
  1595. data/rust/parser/src/parse/world.rs +12 -8
  1596. data/rust/parser/src/phrases.rs +1258 -0
  1597. data/rust/parser/src/render_ruby.rs +549 -0
  1598. data/rust/parser/src/sentence/changes.rs +437 -0
  1599. data/rust/parser/src/sentence/command_step.rs +206 -0
  1600. data/rust/parser/src/sentence/complete.rs +276 -0
  1601. data/rust/parser/src/sentence/entity.rs +41 -0
  1602. data/rust/parser/src/sentence/examples.rs +1012 -0
  1603. data/rust/parser/src/sentence/format.rs +885 -0
  1604. data/rust/parser/src/sentence/lower.rs +1366 -0
  1605. data/rust/parser/src/sentence/mod.rs +444 -0
  1606. data/rust/parser/src/sentence/names.rs +69 -0
  1607. data/rust/parser/src/sentence/options.rs +154 -0
  1608. data/rust/parser/src/sentence/pattern.rs +433 -0
  1609. data/rust/parser/src/sentence/process.rs +200 -0
  1610. data/rust/parser/src/sentence/queries.rs +431 -0
  1611. data/rust/parser/src/sentence/report.rs +283 -0
  1612. data/rust/parser/src/sentence/token.rs +106 -0
  1613. data/rust/parser/src/sentence/values.rs +498 -0
  1614. data/rust/src/exemplar/json.rs +17 -0
  1615. data/rust/src/exemplar/queries.rs +1 -0
  1616. data/rust/src/exemplar/reactions.rs +1 -1
  1617. data/rust/src/exemplar/registry.rs +30 -0
  1618. data/rust/src/kernel/cli.rs +239 -15
  1619. data/rust/src/kernel/collecting.rs +174 -0
  1620. data/rust/src/kernel/dispatch.rs +59 -29
  1621. data/rust/src/kernel/json.rs +62 -0
  1622. data/rust/src/kernel/mod.rs +7 -67
  1623. data/rust/src/kernel/named_query.rs +225 -11
  1624. data/rust/src/kernel/orchestrate.rs +241 -28
  1625. data/rust/src/kernel/orchestrate_guard_tests.rs +15 -12
  1626. data/rust/src/kernel/query_cursor.rs +251 -0
  1627. data/rust/src/kernel/query_ordering.rs +6 -6
  1628. data/rust/src/kernel/read_model.rs +162 -17
  1629. data/rust/src/kernel/repository.rs +107 -17
  1630. data/rust/src/kernel/vocab/mod.rs +4 -0
  1631. data/rust/src/kernel/vocab/reducer.rs +109 -0
  1632. data/rust/src/kernel/vocab/refusal_class.rs +124 -0
  1633. data/rust/src/kernel/vocab/refusal_template.rs +2 -2
  1634. data/rust/src/lib.rs +1 -1
  1635. data/rust/web/Cargo.lock +47 -0
  1636. data/rust/web/Cargo.toml +1 -1
  1637. metadata +689 -161
  1638. data/lib/hecks/chapters.rb +0 -147
  1639. data/lib/hecks/cli/command_table.rb +0 -72
  1640. data/lib/hecks/cli/launcher_words.rb +0 -168
  1641. data/lib/hecks/cli/model_check/examination.rb +0 -93
  1642. data/lib/hecks/cli/project_diagrams.rb +0 -59
  1643. data/lib/hecks/cli/stores.rb +0 -55
  1644. data/lib/hecks/cli.rb +0 -133
  1645. data/lib/hecks/gate/stages.yml +0 -234
  1646. data/lib/hecks/hecks/codebase.bluebook +0 -3241
  1647. data/lib/hecks/hecks/custodian.bluebook +0 -904
  1648. data/lib/hecks/hecks/governance.world +0 -7
  1649. data/lib/hecks/hecks/rehearsal.bluebook +0 -471
  1650. data/lib/hecks/hecks/tooling.bluebook +0 -1238
  1651. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +0 -137
  1652. data/lib/hecks/ports/persistence/plugins/era/postgres_era/boot.rb +0 -43
  1653. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +0 -91
  1654. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +0 -67
  1655. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/context.rb +0 -38
  1656. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +0 -100
  1657. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +0 -102
  1658. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +0 -51
  1659. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +0 -59
  1660. data/lib/hecks/ports/persistence/postgres_dump.rb +0 -100
  1661. data/lib/hecks/projections/deploy/box/templates/tunnel-egress.yaml.tmpl +0 -11
  1662. data/lib/hecks/projector/exporter/capability_values.rb +0 -55
  1663. data/lib/hecks/quality_control/adapters/clearance_recorder.rb +0 -36
  1664. data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +0 -56
  1665. data/lib/hecks/runtime/remote_dispatcher.rb +0 -176
  1666. data/prebuilt/ad68f9e06c8068c2a4176b57abf3083d591d5d8980c2b3e897b97331f965b643.marshal +0 -0
  1667. data/prebuilt/ee22ec8642c4006717ba2b80f46e5efb81de459eef9dfec043e838c0ad4fd720.marshal +0 -0
  1668. data/prebuilt/verdicts-b69f5dcbba79c20daa0493476fa7c317004a30aa2bc3d3dc6a722ea70ae2b43b.json +0 -1
  1669. data/rust/host/src/clock.rs +0 -8
  1670. data/rust/host/src/expr_json.rs +0 -1983
  1671. data/rust/host/src/presentation.rs +0 -842
  1672. data/rust/host/src/wasm_runner.rs +0 -206
  1673. data/rust/src/kernel/expression_operators/membership.rs +0 -20
  1674. data/rust/src/kernel/expression_operators/pattern_match.rs +0 -108
  1675. /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/events.rb +0 -0
  1676. /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/head_writes.rb +0 -0
  1677. /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/base_sql.rb +0 -0
  1678. /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/field_cache.rb +0 -0
  1679. /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/head_compiler.rb +0 -0
  1680. /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/merge_writes.rb +0 -0
  1681. /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/resumable_backfill.rb +0 -0
  1682. /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/transform_installer.rb +0 -0
  1683. /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/transform_sql.rb +0 -0
  1684. /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/sagas.rb +0 -0
  1685. /data/rust/host/{src → core/src}/field_hints.rs +0 -0
  1686. /data/rust/host/{src → core/src}/log.rs +0 -0
  1687. /data/rust/host/{src → core/src}/reference_transform.rs +0 -0
data/rust/host/src/web.rs CHANGED
@@ -9,13 +9,14 @@ use crate::extension::{self, Ctx};
9
9
  use crate::field_hints::{EMAIL_HINT, TEL_HINT, TEXTAREA_HINT, URL_HINT};
10
10
  use crate::host::Host;
11
11
  use crate::ir::ir;
12
- use crate::journal::LineageConfig;
13
12
  use crate::lambda_client::LambdaInvoker;
13
+ use crate::platform_signin::jti::JtiStore;
14
+ use crate::store::journal_sql::LineageConfig;
15
+ use crate::store::Store;
14
16
  use serde_json::{json, Value};
15
17
  use std::collections::HashMap;
16
18
  use std::path::Path;
17
- use tokio::sync::Mutex;
18
- use tokio_postgres::Client;
19
+ use std::sync::OnceLock;
19
20
 
20
21
  mod commerce;
21
22
  mod newsletter;
@@ -23,9 +24,11 @@ mod newsletter_send;
23
24
  mod registration_receipt;
24
25
  mod registrations;
25
26
  mod resend_webhook;
27
+ #[cfg(test)]
28
+ mod signin_tests;
26
29
 
27
30
  pub use commerce::Commerce;
28
- use registrations::payments_routes;
31
+ use registrations::{payments_routes, CheckoutRequest};
29
32
  // payments.rs, its tests and the boot check reach these through `web::`.
30
33
  pub use registrations::checkout_enabled;
31
34
  #[cfg(test)]
@@ -73,7 +76,7 @@ pub async fn render(body: &Value, host: Host<'_>) -> Option<Value> {
73
76
  raw_body: &raw_body,
74
77
  cookies: &cookies,
75
78
  domain_ir: ir(),
76
- client: host.client,
79
+ store: host.store,
77
80
  wasm_path: host.wasm_path,
78
81
  config: host.config,
79
82
  invoker: host.invoker,
@@ -189,7 +192,7 @@ async fn route(
189
192
  host: Host<'_>,
190
193
  ) -> Value {
191
194
  let Host {
192
- client, wasm_path, ..
195
+ store, wasm_path, ..
193
196
  } = host;
194
197
  let domain_name = domain_ir.get("name").and_then(|v| v.as_str()).unwrap_or("");
195
198
  let secret = session_secret();
@@ -259,7 +262,7 @@ async fn route(
259
262
  };
260
263
 
261
264
  if segments.len() == 2 {
262
- return aggregate_index(domain_name, aggregate, &format, client, wasm_path).await;
265
+ return aggregate_index(domain_name, aggregate, &format, store, wasm_path).await;
263
266
  }
264
267
 
265
268
  if segments.len() == 3 {
@@ -276,6 +279,7 @@ async fn route(
276
279
  method,
277
280
  query,
278
281
  raw_body,
282
+ crate::auth::caller(session.as_ref()),
279
283
  host,
280
284
  )
281
285
  .await;
@@ -285,7 +289,7 @@ async fn route(
285
289
  aggregate,
286
290
  &verb_or_id,
287
291
  &format,
288
- client,
292
+ store,
289
293
  wasm_path,
290
294
  )
291
295
  .await;
@@ -309,10 +313,9 @@ async fn auth_route(
309
313
  host: Host<'_>,
310
314
  ) -> Option<Value> {
311
315
  let Host {
312
- client,
316
+ store,
313
317
  wasm_path,
314
318
  config,
315
- invoker,
316
319
  ..
317
320
  } = host;
318
321
  let ctx = Ctx {
@@ -323,7 +326,7 @@ async fn auth_route(
323
326
  raw_body,
324
327
  cookies,
325
328
  domain_ir: Some(domain_ir),
326
- client: host.client,
329
+ store: host.store,
327
330
  wasm_path: host.wasm_path,
328
331
  config: host.config,
329
332
  invoker: host.invoker,
@@ -341,10 +344,7 @@ async fn auth_route(
341
344
  &login_page(query.get("error").map(std::string::String::as_str)),
342
345
  )),
343
346
 
344
- ("POST", "/logout") => Some(redirect_with_cookie(
345
- "/login",
346
- &format!("session=; Max-Age=0{}", cookie_flags()),
347
- )),
347
+ ("POST", "/logout") => Some(logout_response()),
348
348
 
349
349
  // No password login: Google OAuth is the only sign-in path, and the
350
350
  // /accounts/* routes here are just the session-cookie family that
@@ -360,15 +360,15 @@ async fn auth_route(
360
360
  ),
361
361
  )),
362
362
 
363
- ("GET", "/accounts/me") => {
364
- Some(accounts_me_route(domain_ir, cookies, secret, client).await)
365
- }
363
+ ("GET", "/accounts/me") => Some(accounts_me_route(domain_ir, cookies, secret, store).await),
364
+
365
+ ("POST", "/accounts/google-handoff") => Some(google_handoff_route(raw_body, secret)),
366
366
 
367
- ("GET", "/members") => Some(members_route(domain_ir, cookies, secret, client).await),
367
+ ("GET", "/members") => Some(members_route(domain_ir, cookies, secret, store).await),
368
368
 
369
369
  ("POST", "/members") => Some(
370
370
  add_member_route(
371
- domain_ir, raw_body, cookies, secret, client, wasm_path, config,
371
+ domain_ir, raw_body, cookies, secret, store, wasm_path, config,
372
372
  )
373
373
  .await,
374
374
  ),
@@ -376,46 +376,50 @@ async fn auth_route(
376
376
  ("POST", "/signups") => Some(signup_route(raw_body, secret, host).await),
377
377
 
378
378
  ("POST", "/members/disable") => Some(
379
- set_member_disabled_route(domain_ir, raw_body, cookies, secret, client, config, true)
379
+ set_member_disabled_route(domain_ir, raw_body, cookies, secret, store, config, true)
380
380
  .await,
381
381
  ),
382
382
 
383
383
  ("POST", "/members/enable") => Some(
384
- set_member_disabled_route(domain_ir, raw_body, cookies, secret, client, config, false)
384
+ set_member_disabled_route(domain_ir, raw_body, cookies, secret, store, config, false)
385
385
  .await,
386
386
  ),
387
387
 
388
388
  ("POST", "/members/role") => {
389
- Some(set_member_role_route(domain_ir, raw_body, cookies, secret, client, config).await)
389
+ Some(set_member_role_route(domain_ir, raw_body, cookies, secret, store, config).await)
390
390
  }
391
391
 
392
392
  ("POST", "/members/delete") => Some(
393
- set_member_deleted_route(domain_ir, raw_body, cookies, secret, client, config).await,
393
+ set_member_deleted_route(domain_ir, raw_body, cookies, secret, store, config).await,
394
394
  ),
395
395
 
396
396
  // cms/src/endpoints/sso.ts verifies this with the same account_token
397
397
  // wire format; deploy-aws/platform/template.yaml shares SessionSecret
398
398
  // with the cms container as AUTH_SECRET so both sides agree on it.
399
399
  ("GET", "/accounts/sso-token") => {
400
- Some(accounts_sso_token_route(domain_ir, cookies, secret, client).await)
400
+ Some(accounts_sso_token_route(domain_ir, cookies, secret, store).await)
401
401
  }
402
402
 
403
- ("GET", "/auth/google") => match auth::authorization_url(&redirect_uri(), secret) {
404
- Ok(url) => Some(redirect(&url)),
405
- Err(e) => Some(respond(
406
- 500,
407
- "text/plain",
408
- &format!("Google sign-in isn't configured: {e}"),
409
- )),
410
- },
403
+ ("GET", "/auth/google") => Some(google_start()),
411
404
 
412
- ("GET", "/auth/google/callback") => Some(
413
- google_callback(domain_ir, query, client, wasm_path, config, secret, invoker).await,
414
- ),
405
+ // The sign-in cookie is cleared whatever the outcome: one cookie, one attempt.
406
+ ("GET", "/auth/google/callback") => {
407
+ let secure = secure_cookies();
408
+ let response = match google_signin_for(query, cookies, secure) {
409
+ Some(signin) => {
410
+ google_callback(domain_ir, query, &signin.nonce, secret, host).await
411
+ }
412
+ None => redirect("/login?error=google_failed"),
413
+ };
414
+ Some(with_cookie(
415
+ response,
416
+ &auth::google_signin_clear_header(secure),
417
+ ))
418
+ }
415
419
 
416
420
  ("GET" | "POST", "/admin/members") => Some(
417
421
  admin_members_route(
418
- method, domain_ir, raw_body, cookies, session, secret, client, wasm_path, config,
422
+ method, domain_ir, raw_body, cookies, session, secret, store, wasm_path, config,
419
423
  )
420
424
  .await,
421
425
  ),
@@ -433,11 +437,11 @@ async fn members_page_caller(
433
437
  cookies: &HashMap<String, String>,
434
438
  session: Option<&Session>,
435
439
  secret: &str,
436
- client: &Mutex<Client>,
440
+ store: &Store,
437
441
  wasm_path: &Path,
438
442
  ) -> Result<(), Value> {
439
443
  if let Some(session) = session {
440
- if is_admin(client, wasm_path, domain_ir, &session.identity_id).await {
444
+ if is_admin(store, wasm_path, domain_ir, &session.identity_id).await {
441
445
  return Ok(());
442
446
  }
443
447
  }
@@ -445,7 +449,7 @@ async fn members_page_caller(
445
449
  .get(&auth::account_cookie_name())
446
450
  .and_then(|token| auth::verify_account_token(secret, token));
447
451
  if let Some(email) = &account {
448
- if auth::caller_is_admin(client, domain_ir, email)
452
+ if auth::caller_is_admin(store, domain_ir, email)
449
453
  .await
450
454
  .unwrap_or(false)
451
455
  {
@@ -469,17 +473,17 @@ async fn admin_members_route(
469
473
  cookies: &HashMap<String, String>,
470
474
  session: Option<&Session>,
471
475
  secret: &str,
472
- client: &Mutex<Client>,
476
+ store: &Store,
473
477
  wasm_path: &Path,
474
478
  config: &LineageConfig,
475
479
  ) -> Value {
476
480
  if let Err(response) =
477
- members_page_caller(domain_ir, cookies, session, secret, client, wasm_path).await
481
+ members_page_caller(domain_ir, cookies, session, secret, store, wasm_path).await
478
482
  {
479
483
  return response;
480
484
  }
481
485
  if method == "GET" {
482
- return html(200, &admin_members_page(client, domain_ir).await);
486
+ return html(200, &admin_members_page(store, domain_ir).await);
483
487
  }
484
488
 
485
489
  let form = parse_form(raw_body);
@@ -503,36 +507,72 @@ async fn admin_members_route(
503
507
  {
504
508
  return html(400, "<p>Give a valid email.</p>");
505
509
  }
510
+ let days = match expiry_days(
511
+ form.get("expires_in_days")
512
+ .map(|s| Value::String(s.clone())),
513
+ ) {
514
+ Ok(days) => days,
515
+ Err(message) => return html(400, &format!("<p>{}</p>", esc(message))),
516
+ };
506
517
  let name = form
507
518
  .get("name")
508
519
  .map(|s| s.trim().to_string())
509
520
  .filter(|s| !s.is_empty())
510
521
  .unwrap_or_else(|| email.split('@').next().unwrap_or_default().to_string());
511
522
  // Ok(false) is "already admitted": the grant below is then all there is to do.
512
- if let Err(e) = auth::admit_person(client, config, domain_ir, &email, &name).await {
523
+ if let Err(e) = auth::admit_person(store, config, domain_ir, &email, &name).await {
513
524
  return html(500, &format!("<p>{}</p>", esc(&e.to_string())));
514
525
  }
515
- match auth::grant_access(client, wasm_path, config, domain_ir, &email, &role).await {
516
- Ok(true) => redirect("/admin/members"),
517
- Ok(false) => html(404, "<p>No member with that email.</p>"),
526
+ let until = days.map(auth::expiry_after_days);
527
+ match auth::grant_access_until(
528
+ store,
529
+ wasm_path,
530
+ config,
531
+ domain_ir,
532
+ &email,
533
+ &role,
534
+ until.as_deref(),
535
+ )
536
+ .await
537
+ {
538
+ Ok(auth::GrantOutcome::Done) => redirect("/admin/members"),
539
+ Ok(auth::GrantOutcome::UnknownPerson) => html(404, "<p>No member with that email.</p>"),
540
+ Ok(auth::GrantOutcome::LastOpenAdmin) => {
541
+ html(409, &format!("<p>{}</p>", esc(NEEDS_OPEN_ADMIN)))
542
+ }
518
543
  Err(e) => html(500, &format!("<p>{}</p>", esc(&e.to_string()))),
519
544
  }
520
545
  }
521
546
 
522
- async fn is_admin(
523
- client: &Mutex<Client>,
524
- wasm_path: &Path,
525
- domain_ir: &Value,
526
- identity_id: &str,
527
- ) -> bool {
528
- let provider = crate::ir::authorization_provider(domain_ir);
529
- dispatch::read(client, wasm_path).await.is_ok_and(|read| {
530
- auth::holds_admin(
531
- read.get("instances").unwrap_or(&json!({})),
532
- identity_id,
533
- provider.as_ref(),
534
- )
535
- })
547
+ // The refusal when a change would leave no active admin without an end date.
548
+ const NEEDS_OPEN_ADMIN: &str = "a site must keep at least one admin without an end date";
549
+
550
+ // The optional `expires_in_days` of a grant: absent, null or blank means an
551
+ // open-ended grant; otherwise a whole number of days, 1 to `MAX_EXPIRY_DAYS`.
552
+ // A JSON number or a numeric string (a form field) both read.
553
+ fn expiry_days(raw: Option<Value>) -> Result<Option<u32>, &'static str> {
554
+ const MESSAGE: &str = "expires_in_days must be a whole number of days from 1 to 365";
555
+ let days = match raw {
556
+ None | Some(Value::Null) => return Ok(None),
557
+ Some(Value::Number(n)) => n.as_u64(),
558
+ Some(Value::String(s)) if s.trim().is_empty() => return Ok(None),
559
+ Some(Value::String(s)) => s.trim().parse::<u64>().ok(),
560
+ Some(_) => None,
561
+ };
562
+ days.filter(|d| (1..=u64::from(auth::MAX_EXPIRY_DAYS)).contains(d))
563
+ .map(|d| Some(d as u32))
564
+ .ok_or(MESSAGE)
565
+ }
566
+
567
+ async fn is_admin(store: &Store, wasm_path: &Path, domain_ir: &Value, identity_id: &str) -> bool {
568
+ let Some(query) = crate::ir::authorization_provider(domain_ir).and_then(|p| p.live_assignments)
569
+ else {
570
+ return false;
571
+ };
572
+ let args = json!({"actor_id": {"value": identity_id}});
573
+ dispatch::query(store, wasm_path, &query, args)
574
+ .await
575
+ .is_ok_and(|answer| auth::holds_admin(&auth::answer_rows(&answer)))
536
576
  }
537
577
 
538
578
  // Verifies the token AND that the person still has access, so a
@@ -541,7 +581,7 @@ async fn active_session_email(
541
581
  domain_ir: &Value,
542
582
  cookies: &HashMap<String, String>,
543
583
  secret: &str,
544
- client: &Mutex<Client>,
584
+ store: &Store,
545
585
  ) -> Result<String, Value> {
546
586
  let not_logged_in = || {
547
587
  respond(
@@ -556,7 +596,7 @@ async fn active_session_email(
556
596
  else {
557
597
  return Err(not_logged_in());
558
598
  };
559
- match auth::has_access(client, domain_ir, &email).await {
599
+ match auth::has_access(store, domain_ir, &email).await {
560
600
  Ok(true) => Ok(email),
561
601
  Ok(false) => Err(not_logged_in()),
562
602
  Err(e) => Err(respond(
@@ -571,10 +611,10 @@ async fn accounts_me_route(
571
611
  domain_ir: &Value,
572
612
  cookies: &HashMap<String, String>,
573
613
  secret: &str,
574
- client: &Mutex<Client>,
614
+ store: &Store,
575
615
  ) -> Value {
576
- match active_session_email(domain_ir, cookies, secret, client).await {
577
- Ok(email) => match auth::member_identity_id(client, domain_ir, &email).await {
616
+ match active_session_email(domain_ir, cookies, secret, store).await {
617
+ Ok(email) => match auth::member_identity_id(store, domain_ir, &email).await {
578
618
  Ok(identity_id) => respond(
579
619
  200,
580
620
  "application/json",
@@ -597,13 +637,12 @@ async fn members_route(
597
637
  domain_ir: &Value,
598
638
  cookies: &HashMap<String, String>,
599
639
  secret: &str,
600
- client: &Mutex<Client>,
640
+ store: &Store,
601
641
  ) -> Value {
602
- if let Err(response) = newsletter_send::require_admin(domain_ir, cookies, secret, client).await
603
- {
642
+ if let Err(response) = newsletter_send::require_admin(domain_ir, cookies, secret, store).await {
604
643
  return response;
605
644
  }
606
- match auth::all_people(client, domain_ir).await {
645
+ match auth::all_people(store, domain_ir).await {
607
646
  Ok(people) => respond(
608
647
  200,
609
648
  "application/json",
@@ -624,7 +663,7 @@ pub(crate) async fn add_member_route(
624
663
  raw_body: &str,
625
664
  cookies: &HashMap<String, String>,
626
665
  secret: &str,
627
- client: &Mutex<Client>,
666
+ store: &Store,
628
667
  wasm_path: &Path,
629
668
  config: &LineageConfig,
630
669
  ) -> Value {
@@ -634,7 +673,7 @@ pub(crate) async fn add_member_route(
634
673
  else {
635
674
  return json_error(401, "not logged in");
636
675
  };
637
- match auth::caller_is_admin(client, domain_ir, &caller).await {
676
+ match auth::caller_is_admin(store, domain_ir, &caller).await {
638
677
  Ok(true) => {}
639
678
  Ok(false) => return json_error(403, "admins only"),
640
679
  Err(e) => return json_error(500, &format!("members lookup failed: {e}")),
@@ -668,19 +707,36 @@ pub(crate) async fn add_member_route(
668
707
  );
669
708
  }
670
709
 
671
- match auth::admit_person(client, config, domain_ir, &email, &name).await {
710
+ let days = match expiry_days(body.get("expires_in_days").cloned()) {
711
+ Ok(days) => days,
712
+ Err(message) => return json_error(400, message),
713
+ };
714
+
715
+ match auth::admit_person(store, config, domain_ir, &email, &name).await {
672
716
  Ok(true) => {}
673
717
  Ok(false) => return json_error(409, "that email is already admitted"),
674
718
  Err(e) => return json_error(500, &format!("admit failed: {e}")),
675
719
  }
676
720
  let email = email.to_lowercase();
677
- match auth::grant_access(client, wasm_path, config, domain_ir, &email, &role).await {
678
- Ok(true) => respond(
721
+ let until = days.map(auth::expiry_after_days);
722
+ match auth::grant_access_until(
723
+ store,
724
+ wasm_path,
725
+ config,
726
+ domain_ir,
727
+ &email,
728
+ &role,
729
+ until.as_deref(),
730
+ )
731
+ .await
732
+ {
733
+ Ok(auth::GrantOutcome::Done) => respond(
679
734
  201,
680
735
  "application/json",
681
- &json!({"name": name, "email": email, "role": role, "linked": false, "granted": true, "disabled": false, "identity_id": null}).to_string(),
736
+ &json!({"name": name, "email": email, "role": role, "linked": false, "granted": true, "disabled": false, "access_expires_at": until, "expired": false, "identity_id": null}).to_string(),
682
737
  ),
683
- Ok(false) => json_error(500, &format!("the person was admitted but the {role} role was not granted")),
738
+ Ok(auth::GrantOutcome::UnknownPerson) => json_error(500, &format!("the person was admitted but the {role} role was not granted")),
739
+ Ok(auth::GrantOutcome::LastOpenAdmin) => json_error(409, NEEDS_OPEN_ADMIN),
684
740
  Err(e) => json_error(500, &format!("the person was admitted but the {role} role was not granted: {e}")),
685
741
  }
686
742
  }
@@ -739,7 +795,7 @@ async fn set_member_disabled_route(
739
795
  raw_body: &str,
740
796
  cookies: &HashMap<String, String>,
741
797
  secret: &str,
742
- client: &Mutex<Client>,
798
+ store: &Store,
743
799
  config: &LineageConfig,
744
800
  disable: bool,
745
801
  ) -> Value {
@@ -760,7 +816,7 @@ async fn set_member_disabled_route(
760
816
  return json_error(400, "an email is required");
761
817
  }
762
818
 
763
- match auth::set_person_disabled(client, config, domain_ir, &caller, &email, disable).await {
819
+ match auth::set_person_disabled(store, config, domain_ir, &caller, &email, disable).await {
764
820
  Ok(auth::DisableOutcome::Done) => {
765
821
  let key = if disable { "disabled" } else { "enabled" };
766
822
  respond(
@@ -774,6 +830,7 @@ async fn set_member_disabled_route(
774
830
  json_error(403, "you can't disable your own admin access")
775
831
  }
776
832
  Ok(auth::DisableOutcome::UnknownPerson) => json_error(404, "no member with that email"),
833
+ Ok(auth::DisableOutcome::LastOpenAdmin) => json_error(409, NEEDS_OPEN_ADMIN),
777
834
  Ok(auth::DisableOutcome::LastAdmin) => {
778
835
  json_error(409, "there must always be at least one admin")
779
836
  }
@@ -790,7 +847,7 @@ async fn set_member_deleted_route(
790
847
  raw_body: &str,
791
848
  cookies: &HashMap<String, String>,
792
849
  secret: &str,
793
- client: &Mutex<Client>,
850
+ store: &Store,
794
851
  config: &LineageConfig,
795
852
  ) -> Value {
796
853
  let Some(caller) = cookies
@@ -810,7 +867,7 @@ async fn set_member_deleted_route(
810
867
  return json_error(400, "an email is required");
811
868
  }
812
869
 
813
- match auth::set_person_deleted(client, config, domain_ir, &caller, &email).await {
870
+ match auth::set_person_deleted(store, config, domain_ir, &caller, &email).await {
814
871
  Ok(auth::DeleteOutcome::Done) => respond(
815
872
  200,
816
873
  "application/json",
@@ -830,7 +887,7 @@ async fn set_member_role_route(
830
887
  raw_body: &str,
831
888
  cookies: &HashMap<String, String>,
832
889
  secret: &str,
833
- client: &Mutex<Client>,
890
+ store: &Store,
834
891
  config: &LineageConfig,
835
892
  ) -> Value {
836
893
  let Some(caller) = cookies
@@ -858,7 +915,7 @@ async fn set_member_role_route(
858
915
  );
859
916
  }
860
917
 
861
- match auth::set_person_role(client, config, domain_ir, &caller, &email, &role).await {
918
+ match auth::set_person_role(store, config, domain_ir, &caller, &email, &role).await {
862
919
  Ok(auth::RoleOutcome::Done) => respond(
863
920
  200,
864
921
  "application/json",
@@ -866,6 +923,7 @@ async fn set_member_role_route(
866
923
  ),
867
924
  Ok(auth::RoleOutcome::CallerNotAdmin) => json_error(403, "admins only"),
868
925
  Ok(auth::RoleOutcome::UnknownPerson) => json_error(404, "no member with that email"),
926
+ Ok(auth::RoleOutcome::LastOpenAdmin) => json_error(409, NEEDS_OPEN_ADMIN),
869
927
  Ok(auth::RoleOutcome::LastAdmin) => {
870
928
  json_error(409, "there must always be at least one admin")
871
929
  }
@@ -879,7 +937,7 @@ async fn registrations_list_route(
879
937
  domain_ir: &Value,
880
938
  cookies: &HashMap<String, String>,
881
939
  secret: &str,
882
- client: &Mutex<Client>,
940
+ store: &Store,
883
941
  wasm_path: &Path,
884
942
  config: &LineageConfig,
885
943
  ) -> Value {
@@ -889,13 +947,13 @@ async fn registrations_list_route(
889
947
  else {
890
948
  return json_error(401, "not logged in");
891
949
  };
892
- match auth::caller_is_admin(client, domain_ir, &caller).await {
950
+ match auth::caller_is_admin(store, domain_ir, &caller).await {
893
951
  Ok(true) => {}
894
952
  Ok(false) => return json_error(403, "admins only"),
895
953
  Err(e) => return json_error(500, &format!("members lookup failed: {e}")),
896
954
  }
897
955
 
898
- let read = match dispatch::read(client, wasm_path).await {
956
+ let read = match dispatch::read(store, wasm_path).await {
899
957
  Ok(read) => read,
900
958
  Err(e) => return json_error(500, &format!("registrations lookup failed: {e:#}")),
901
959
  };
@@ -960,10 +1018,10 @@ async fn accounts_sso_token_route(
960
1018
  domain_ir: &Value,
961
1019
  cookies: &HashMap<String, String>,
962
1020
  secret: &str,
963
- client: &Mutex<Client>,
1021
+ store: &Store,
964
1022
  ) -> Value {
965
1023
  const SSO_TOKEN_TTL_SECS: u64 = 60;
966
- let email = match active_session_email(domain_ir, cookies, secret, client).await {
1024
+ let email = match active_session_email(domain_ir, cookies, secret, store).await {
967
1025
  Ok(email) => email,
968
1026
  Err(response) => return response,
969
1027
  };
@@ -974,29 +1032,100 @@ async fn accounts_sso_token_route(
974
1032
  &json!({"token": sso_token}).to_string(),
975
1033
  )
976
1034
  }
1035
+ // Logging out ends both sessions: the Governance `session` cookie and the account cookie.
1036
+ fn logout_response() -> Value {
1037
+ with_cookie(
1038
+ redirect_with_cookie("/login", &format!("session=; Max-Age=0{}", cookie_flags())),
1039
+ &format!(
1040
+ "{}=; Max-Age=0{}",
1041
+ auth::account_cookie_name(),
1042
+ cookie_flags()
1043
+ ),
1044
+ )
1045
+ }
1046
+
1047
+ // Starts a Google sign-in bound to this browser: the cookie set here holds the state and nonce
1048
+ // the callback must bring back (auth/google_signin.rs).
1049
+ fn google_start() -> Value {
1050
+ let signin = auth::GoogleSignin::fresh();
1051
+ match auth::authorization_url(&redirect_uri(), &signin) {
1052
+ Ok(url) => redirect_with_cookie(
1053
+ &url,
1054
+ &auth::google_signin_set_header(&signin, secure_cookies()),
1055
+ ),
1056
+ Err(e) => respond(
1057
+ 500,
1058
+ "text/plain",
1059
+ &format!("Google sign-in isn't configured: {e}"),
1060
+ ),
1061
+ }
1062
+ }
1063
+
1064
+ // The sign-in this browser started, when the callback's `state` is the one its cookie holds.
1065
+ fn google_signin_for(
1066
+ query: &HashMap<String, String>,
1067
+ cookies: &HashMap<String, String>,
1068
+ secure: bool,
1069
+ ) -> Option<auth::GoogleSignin> {
1070
+ let Some(signin) = auth::read_google_signin(cookies, secure) else {
1071
+ eprintln!("google_callback: no sign-in cookie");
1072
+ return None;
1073
+ };
1074
+ let state = query.get("state").map_or("", String::as_str);
1075
+ if !signin.state_matches(state) {
1076
+ eprintln!("google_callback: state does not match the sign-in cookie");
1077
+ return None;
1078
+ }
1079
+ Some(signin)
1080
+ }
1081
+
1082
+ // A site on another origin than this host trades the handoff token from the callback's redirect
1083
+ // for the account token here, server-to-server, so the long-lived token never rides a URL.
1084
+ fn google_handoff_route(raw_body: &str, secret: &str) -> Value {
1085
+ static SPENT: OnceLock<JtiStore> = OnceLock::new();
1086
+ let spent = SPENT.get_or_init(JtiStore::new);
1087
+ let token = serde_json::from_str::<Value>(raw_body)
1088
+ .ok()
1089
+ .and_then(|body| body.get("token")?.as_str().map(str::to_string));
1090
+ token
1091
+ .and_then(|token| auth::redeem_handoff_token(secret, &token, spent))
1092
+ .map_or_else(
1093
+ || {
1094
+ json_error(
1095
+ 401,
1096
+ "this sign-in handoff is invalid, expired or already used",
1097
+ )
1098
+ },
1099
+ |account| {
1100
+ respond(
1101
+ 200,
1102
+ "application/json",
1103
+ &json!({ "token": account }).to_string(),
1104
+ )
1105
+ },
1106
+ )
1107
+ }
1108
+
977
1109
  async fn google_callback(
978
1110
  domain_ir: &Value,
979
1111
  query: &HashMap<String, String>,
980
- client: &Mutex<Client>,
981
- wasm_path: &Path,
982
- config: &LineageConfig,
1112
+ nonce: &str,
983
1113
  secret: &str,
984
- invoker: &dyn LambdaInvoker,
1114
+ host: Host<'_>,
985
1115
  ) -> Value {
1116
+ let Host {
1117
+ store,
1118
+ wasm_path,
1119
+ config,
1120
+ invoker,
1121
+ ..
1122
+ } = host;
986
1123
  let Some(code) = query.get("code") else {
987
1124
  eprintln!("google_callback: missing code");
988
1125
  return redirect("/login?error=google_failed");
989
1126
  };
990
- let Some(state) = query.get("state") else {
991
- eprintln!("google_callback: missing state");
992
- return redirect("/login?error=google_failed");
993
- };
994
- if let Err(e) = auth::verify_state(state, secret) {
995
- eprintln!("google_callback: state: {e}");
996
- return redirect("/login?error=google_failed");
997
- }
998
1127
 
999
- let claims = match auth::verify(code, &redirect_uri()).await {
1128
+ let claims = match auth::verify(code, &redirect_uri(), nonce).await {
1000
1129
  Ok(c) => c,
1001
1130
  Err(e) => {
1002
1131
  eprintln!("google_callback: token exchange: {e}");
@@ -1004,7 +1133,7 @@ async fn google_callback(
1004
1133
  }
1005
1134
  };
1006
1135
 
1007
- let read = match dispatch::read(client, wasm_path).await {
1136
+ let read = match dispatch::read(store, wasm_path).await {
1008
1137
  Ok(r) => r,
1009
1138
  Err(e) => {
1010
1139
  eprintln!("google_callback: read: {e:#}");
@@ -1016,7 +1145,7 @@ async fn google_callback(
1016
1145
  // PostgresEra head first — Identity is not in the WASM journal.
1017
1146
  // Journal `instances` is only a fallback for Lambda-era rows.
1018
1147
  let identity_id =
1019
- match auth::resolve_identity_from_head(client, domain_ir, &claims.issuer, &claims.subject)
1148
+ match auth::resolve_identity_from_head(store, domain_ir, &claims.issuer, &claims.subject)
1020
1149
  .await
1021
1150
  {
1022
1151
  Ok(found) => found,
@@ -1030,7 +1159,7 @@ async fn google_callback(
1030
1159
 
1031
1160
  let session = match identity_id {
1032
1161
  Some(ref identity_id) => {
1033
- match auth::session_for_member_by_identity(client, domain_ir, identity_id).await {
1162
+ match auth::session_for_member_by_identity(store, domain_ir, identity_id).await {
1034
1163
  Ok(s) => s,
1035
1164
  Err(e) => {
1036
1165
  eprintln!("google_callback: member by identity: {e:#}");
@@ -1045,7 +1174,7 @@ async fn google_callback(
1045
1174
  None if claims.email_verified => {
1046
1175
  let email = claims.email.clone().unwrap_or_default();
1047
1176
  match auth::provision(
1048
- client,
1177
+ store,
1049
1178
  wasm_path,
1050
1179
  config,
1051
1180
  domain_ir,
@@ -1072,11 +1201,12 @@ async fn google_callback(
1072
1201
 
1073
1202
  // The site's admin authenticates on the account cookie, not this
1074
1203
  // host's own session cookie. Cross-origin, a cookie here would never
1075
- // reach the site, so the token is handed across via /api/google-handoff.
1204
+ // reach the site, so a one-minute handoff token goes to its
1205
+ // /api/google-handoff, which trades it at /accounts/google-handoff.
1076
1206
  let site = site_url();
1077
1207
  let site = site.trim_end_matches('/');
1078
- let token = auth::account_token(secret, &session.email, auth::SESSION_TTL_SECS);
1079
1208
  if same_origin(site, &redirect_uri()) {
1209
+ let token = auth::account_token(secret, &session.email, auth::SESSION_TTL_SECS);
1080
1210
  let cookie = format!(
1081
1211
  "{}={token}{}; Max-Age={}",
1082
1212
  auth::account_cookie_name(),
@@ -1089,7 +1219,7 @@ async fn google_callback(
1089
1219
  redirect(&format!(
1090
1220
  "{}/api/google-handoff?token={}",
1091
1221
  site,
1092
- auth::urlencode(&token)
1222
+ auth::urlencode(&auth::handoff_token(secret, &session.email))
1093
1223
  ))
1094
1224
  }
1095
1225
  }
@@ -1133,26 +1263,34 @@ fn login_page(error: Option<&str>) -> String {
1133
1263
  )
1134
1264
  }
1135
1265
 
1136
- async fn admin_members_page(client: &Mutex<Client>, domain_ir: &Value) -> String {
1137
- let people = auth::all_people(client, domain_ir)
1138
- .await
1139
- .unwrap_or_default();
1266
+ async fn admin_members_page(store: &Store, domain_ir: &Value) -> String {
1267
+ let people = auth::all_people(store, domain_ir).await.unwrap_or_default();
1140
1268
  let rows: String = people
1141
1269
  .iter()
1142
1270
  .map(|p| {
1143
1271
  let name = p.get("name").and_then(|v| v.as_str()).unwrap_or("");
1144
1272
  let email = p.get("email").and_then(|v| v.as_str()).unwrap_or("");
1145
1273
  let role = p.get("role").and_then(|v| v.as_str()).unwrap_or("—");
1146
- let access = if p.get("linked").and_then(serde_json::Value::as_bool) == Some(true) {
1147
- "linked"
1274
+ let until = p
1275
+ .get("access_expires_at")
1276
+ .and_then(|v| v.as_str())
1277
+ .unwrap_or("");
1278
+ let expired = p.get("expired").and_then(serde_json::Value::as_bool) == Some(true);
1279
+ let mut access = if expired {
1280
+ format!("access ended {until}")
1281
+ } else if p.get("linked").and_then(serde_json::Value::as_bool) == Some(true) {
1282
+ "linked".to_string()
1148
1283
  } else if p.get("granted").and_then(serde_json::Value::as_bool) == Some(true) {
1149
- "granted, not yet signed in"
1284
+ "granted, not yet signed in".to_string()
1150
1285
  } else {
1151
- "no access"
1286
+ "no access".to_string()
1152
1287
  };
1288
+ if !expired && !until.is_empty() {
1289
+ access = format!("{access}, until {until}");
1290
+ }
1153
1291
  format!(
1154
1292
  r#"<tr class="border-b border-slate-100"><td class="py-2 pr-4">{}</td><td class="py-2 pr-4">{}</td><td class="py-2 pr-4">{}</td><td class="py-2">{}</td></tr>"#,
1155
- esc(name), esc(email), esc(role), esc(access)
1293
+ esc(name), esc(email), esc(role), esc(&access)
1156
1294
  )
1157
1295
  })
1158
1296
  .collect::<Vec<_>>()
@@ -1164,10 +1302,11 @@ async fn admin_members_page(client: &Mutex<Client>, domain_ir: &Value) -> String
1164
1302
  <p class="mb-4"><a href="/" class="text-slate-500 hover:text-slate-700">&larr; back</a></p>
1165
1303
  <h1 class="text-xl font-semibold mb-4">Members</h1>
1166
1304
  <table class="w-full text-sm mb-6"><thead><tr class="text-left text-slate-500 text-xs uppercase"><th class="py-2 pr-4">Name</th><th class="py-2 pr-4">Email</th><th class="py-2 pr-4">Role</th><th class="py-2">Access</th></tr></thead><tbody>{rows}</tbody></table>
1167
- <form method="post" action="/admin/members" class="flex gap-2 items-center">
1305
+ <form method="post" action="/admin/members" class="flex flex-wrap gap-2 items-center">
1168
1306
  <input type="text" name="name" placeholder="Name" class="w-40 border border-slate-300 rounded-md px-3 py-2 text-sm" />
1169
1307
  <input type="email" name="email" placeholder="Email (their Google account)" required class="flex-1 border border-slate-300 rounded-md px-3 py-2 text-sm" />
1170
1308
  <select name="role" class="border border-slate-300 rounded-md px-3 py-2 text-sm"><option value="Admin">Admin</option><option value="Owner">Owner</option><option value="Member">Member</option></select>
1309
+ <input type="number" name="expires_in_days" min="1" max="365" step="1" placeholder="Days (optional)" title="Leave empty for access with no end date" class="w-32 border border-slate-300 rounded-md px-3 py-2 text-sm" />
1171
1310
  <button type="submit" class="px-4 py-2 bg-slate-900 text-white rounded-md text-sm">Grant access</button>
1172
1311
  </form></main></body></html>"#
1173
1312
  )
@@ -1276,7 +1415,7 @@ fn humanize(path: &str) -> String {
1276
1415
  const PRIMITIVES: &[&str] = &["String", "Integer", "Float", "TrueClass", "FalseClass"];
1277
1416
 
1278
1417
  // Dispatch order: list? -> reference? -> admits -> value object ->
1279
- // primitive. `domain_ir` is the whole chapter, needed for cross-aggregate lookups.
1418
+ // primitive. `domain_ir` is the whole bluebook, needed for cross-aggregate lookups.
1280
1419
  fn resolve_field(domain_ir: &Value, attribute: &Value, aggregate: &Value, path: &str) -> Field {
1281
1420
  let is_list = attribute
1282
1421
  .get("list")
@@ -1752,12 +1891,12 @@ async fn aggregate_index(
1752
1891
  domain_name: &str,
1753
1892
  aggregate: &Value,
1754
1893
  format: &str,
1755
- client: &Mutex<Client>,
1894
+ store: &Store,
1756
1895
  wasm_path: &Path,
1757
1896
  ) -> Value {
1758
1897
  let agg = agg_name(aggregate);
1759
1898
  let prefix = format!("{domain_name}::{agg}#");
1760
- let records = match dispatch::read(client, wasm_path).await {
1899
+ let records = match dispatch::read(store, wasm_path).await {
1761
1900
  Ok(result) => instances_for(&result, &prefix),
1762
1901
  Err(e) => return respond(500, "text/plain", &format!("{e:#}")),
1763
1902
  };
@@ -1841,12 +1980,12 @@ async fn record_show(
1841
1980
  aggregate: &Value,
1842
1981
  id: &str,
1843
1982
  format: &str,
1844
- client: &Mutex<Client>,
1983
+ store: &Store,
1845
1984
  wasm_path: &Path,
1846
1985
  ) -> Value {
1847
1986
  let agg = agg_name(aggregate);
1848
1987
  let prefix = format!("{domain_name}::{agg}#");
1849
- let result = match dispatch::read(client, wasm_path).await {
1988
+ let result = match dispatch::read(store, wasm_path).await {
1850
1989
  Ok(r) => r,
1851
1990
  Err(e) => return respond(500, "text/plain", &format!("{e:#}")),
1852
1991
  };
@@ -1923,10 +2062,11 @@ async fn command_route(
1923
2062
  method: &str,
1924
2063
  query: &HashMap<String, String>,
1925
2064
  raw_body: &str,
2065
+ caller: dispatch::Caller<'_>,
1926
2066
  host: Host<'_>,
1927
2067
  ) -> Value {
1928
2068
  let Host {
1929
- client,
2069
+ store,
1930
2070
  wasm_path,
1931
2071
  config,
1932
2072
  invoker,
@@ -1937,20 +2077,37 @@ async fn command_route(
1937
2077
 
1938
2078
  if format != "html" {
1939
2079
  if method == "GET" {
2080
+ if query.get("schema").map(String::as_str) == Some("1") {
2081
+ return crate::ir::command_schema(agg_name(aggregate), cname).map_or_else(
2082
+ || respond(404, "text/plain", &format!("no schema for {cname}")),
2083
+ |schema| respond(200, "application/json", &schema.to_string()),
2084
+ );
2085
+ }
1940
2086
  return respond(200, "application/json", &command.to_string());
1941
2087
  }
1942
2088
  let raw = parse_form(raw_body);
2089
+ if crate::validation::requested(query) {
2090
+ let facts = Preview {
2091
+ domain_name,
2092
+ aggregate,
2093
+ command,
2094
+ fields: &fields,
2095
+ raw: &raw,
2096
+ };
2097
+ return preview_submission(&facts, host, caller).await;
2098
+ }
1943
2099
  return submit(
1944
2100
  domain_name,
1945
2101
  aggregate,
1946
2102
  command,
1947
2103
  &fields,
1948
2104
  &raw,
1949
- client,
2105
+ store,
1950
2106
  wasm_path,
1951
2107
  config,
1952
2108
  true,
1953
2109
  invoker,
2110
+ caller,
1954
2111
  )
1955
2112
  .await;
1956
2113
  }
@@ -1980,14 +2137,54 @@ async fn command_route(
1980
2137
  command,
1981
2138
  &fields,
1982
2139
  &raw,
1983
- client,
2140
+ store,
1984
2141
  wasm_path,
1985
2142
  config,
1986
2143
  false,
1987
2144
  invoker,
2145
+ caller,
1988
2146
  )
1989
2147
  .await
1990
2148
  }
2149
+ // What a preview of a submitted form is asked about: the command and the values posted for it.
2150
+ struct Preview<'a> {
2151
+ domain_name: &'a str,
2152
+ aggregate: &'a Value,
2153
+ command: &'a Value,
2154
+ fields: &'a [Field],
2155
+ raw: &'a HashMap<String, String>,
2156
+ }
2157
+
2158
+ // Whether the submitted command would be accepted, answered as the client envelope with 200 whether
2159
+ // or not it would be refused, and kept nowhere. A malformed submission is a 400, as a real one is.
2160
+ async fn preview_submission(
2161
+ asked: &Preview<'_>,
2162
+ host: Host<'_>,
2163
+ caller: dispatch::Caller<'_>,
2164
+ ) -> Value {
2165
+ let Preview {
2166
+ domain_name,
2167
+ aggregate,
2168
+ command,
2169
+ fields,
2170
+ raw,
2171
+ } = *asked;
2172
+ let cname = command.get("name").and_then(|v| v.as_str()).unwrap_or("");
2173
+ let verb = format!("{domain_name}::{}.{cname}", agg_name(aggregate));
2174
+ let args = match extract_args(fields, raw) {
2175
+ Ok(args) => args,
2176
+ Err(e) => return bad_request(domain_name, aggregate, command, fields, raw, true, &e),
2177
+ };
2178
+ match dispatch::preview_request(host, &verb, args, caller).await {
2179
+ Ok(outcome) => respond(
2180
+ 200,
2181
+ "application/json",
2182
+ &crate::validation::answer(&outcome.result).to_string(),
2183
+ ),
2184
+ Err(e) => respond(500, "text/plain", &format!("{e:#}")),
2185
+ }
2186
+ }
2187
+
1991
2188
  #[allow(clippy::too_many_arguments)]
1992
2189
  async fn submit(
1993
2190
  domain_name: &str,
@@ -1995,11 +2192,12 @@ async fn submit(
1995
2192
  command: &Value,
1996
2193
  fields: &[Field],
1997
2194
  raw: &HashMap<String, String>,
1998
- client: &Mutex<Client>,
2195
+ store: &Store,
1999
2196
  wasm_path: &Path,
2000
2197
  config: &LineageConfig,
2001
2198
  json_mode: bool,
2002
2199
  invoker: &dyn LambdaInvoker,
2200
+ caller: dispatch::Caller<'_>,
2003
2201
  ) -> Value {
2004
2202
  let agg = agg_name(aggregate);
2005
2203
  let cname = command.get("name").and_then(|v| v.as_str()).unwrap_or("");
@@ -2012,13 +2210,12 @@ async fn submit(
2012
2210
  Err(e) => return bad_request(domain_name, aggregate, command, fields, raw, json_mode, &e),
2013
2211
  };
2014
2212
 
2015
- // `None`: this web UI has no concept of an authenticated caller's
2016
- // role yet (see auth.rs's own session handling for that).
2017
- let outcome = match dispatch::handle(
2018
- Host::new(client, wasm_path, config, invoker),
2213
+ // Dispatched as the signed-in caller, checked against the role the command declares.
2214
+ let outcome = match dispatch::handle_request(
2215
+ Host::new(store, wasm_path, config, invoker),
2019
2216
  &verb,
2020
2217
  args,
2021
- None,
2218
+ caller,
2022
2219
  )
2023
2220
  .await
2024
2221
  {
@@ -2414,6 +2611,20 @@ fn redirect_with_cookie(location: &str, cookie: &str) -> Value {
2414
2611
  json!({"statusCode": 302, "headers": {"location": location}, "cookies": [cookie], "body": "", "isBase64Encoded": false})
2415
2612
  }
2416
2613
 
2614
+ // `response` with one more `Set-Cookie` beside any it already carries.
2615
+ fn with_cookie(mut response: Value, cookie: &str) -> Value {
2616
+ match response.get_mut("cookies").and_then(Value::as_array_mut) {
2617
+ Some(cookies) => cookies.push(json!(cookie)),
2618
+ None => response["cookies"] = json!([cookie]),
2619
+ }
2620
+ response
2621
+ }
2622
+
2623
+ // Whether cookies this host sets carry `Secure` (see `cookie_flags`).
2624
+ fn secure_cookies() -> bool {
2625
+ cookie_flags() == COOKIE_FLAGS_SECURE
2626
+ }
2627
+
2417
2628
  const DEFAULT_SITE_URL: &str = "http://localhost:4321";
2418
2629
 
2419
2630
  /// The public site's origin: `SITE_URL`, else the local development address.
@@ -2441,6 +2652,8 @@ fn respond_with_cookie(status: u16, content_type: &str, body: &str, cookie: &str
2441
2652
  #[cfg(test)]
2442
2653
  mod tests {
2443
2654
  use super::*;
2655
+ #[allow(unused_imports)]
2656
+ use crate::store::StoreFixtures;
2444
2657
 
2445
2658
  // A record's id is user-supplied; left unescaped, a malicious one
2446
2659
  // would render as live markup — these tests pin that it can't.
@@ -2851,6 +3064,190 @@ mod tests {
2851
3064
  );
2852
3065
  }
2853
3066
 
3067
+ // A form submitted through the generated UI dispatches as the signed-in caller, so a role the
3068
+ // command does not declare is refused as it is over the internal protocol.
3069
+ #[tokio::test]
3070
+ async fn a_form_submit_dispatches_as_the_signed_in_caller() {
3071
+ let client = crate::dispatch_tests::tests::memory_store();
3072
+ let wasm = crate::dispatch_tests::tests::wasm_path();
3073
+ let ir: Value = serde_json::from_str(
3074
+ &std::fs::read_to_string(
3075
+ std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
3076
+ .join("../dist/banking.ir.json"),
3077
+ )
3078
+ .expect("hecks build_wasm writes banking.ir.json beside the wasm"),
3079
+ )
3080
+ .expect("valid IR");
3081
+ let lineage = LineageConfig {
3082
+ domain: "Banking".to_string(),
3083
+ era: None,
3084
+ mirrored: None,
3085
+ seal_every: None,
3086
+ };
3087
+ let invoker = crate::lambda_client::NeverInvoker;
3088
+ let submit_as = |role: &'static str, reference: &'static str| {
3089
+ let (client, wasm, ir, lineage, invoker) = (&client, &wasm, &ir, &lineage, &invoker);
3090
+ async move {
3091
+ let cookies = signed_in_cookies(role);
3092
+ let form = format!(
3093
+ "reference.value={reference}&name.given=Ada&name.family=Lovelace&email.address=ada%40example.com"
3094
+ );
3095
+ let host = Host::new(client, wasm, lineage, invoker);
3096
+ let path = "/Banking/Customer/Register.json";
3097
+ route(ir, "POST", path, &HashMap::new(), &form, &cookies, host).await
3098
+ }
3099
+ };
3100
+
3101
+ let teller = submit_as("Teller", "CUST-9201").await;
3102
+ assert_eq!(teller["statusCode"], 422, "{teller}");
3103
+ assert!(
3104
+ teller["body"]
3105
+ .as_str()
3106
+ .unwrap_or_default()
3107
+ .contains("Unauthorized"),
3108
+ "{teller}"
3109
+ );
3110
+ let clerk = submit_as("Branch clerk", "CUST-9202").await;
3111
+ assert_eq!(clerk["statusCode"], 201, "{clerk}");
3112
+ }
3113
+
3114
+ // `?dry_run=1` on the JSON command route: the signed-in caller's verdict, as the client envelope,
3115
+ // with nothing kept.
3116
+ #[tokio::test]
3117
+ async fn a_preview_of_a_submitted_form_answers_the_envelope_as_the_signed_in_caller() {
3118
+ let client = crate::dispatch_tests::tests::memory_store();
3119
+ let wasm = crate::dispatch_tests::tests::wasm_path();
3120
+ let ir: Value = serde_json::from_str(
3121
+ &std::fs::read_to_string(
3122
+ std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
3123
+ .join("../dist/banking.ir.json"),
3124
+ )
3125
+ .expect("hecks build_wasm writes banking.ir.json beside the wasm"),
3126
+ )
3127
+ .expect("valid IR");
3128
+ let lineage = LineageConfig {
3129
+ domain: "Banking".to_string(),
3130
+ era: None,
3131
+ mirrored: None,
3132
+ seal_every: None,
3133
+ };
3134
+ let invoker = crate::lambda_client::NeverInvoker;
3135
+ let post = |role: &'static str, asked: &'static str| {
3136
+ let (client, wasm, ir, lineage, invoker) = (&client, &wasm, &ir, &lineage, &invoker);
3137
+ async move {
3138
+ let cookies = signed_in_cookies(role);
3139
+ let form = "reference.value=CUST-9301&name.given=Ada&name.family=Lovelace&email.address=ada%40example.com";
3140
+ let query = HashMap::from([("dry_run".to_string(), asked.to_string())]);
3141
+ let host = Host::new(client, wasm, lineage, invoker);
3142
+ let path = "/Banking/Customer/Register.json";
3143
+ route(ir, "POST", path, &query, form, &cookies, host).await
3144
+ }
3145
+ };
3146
+ let verdict = |response: &Value| -> Value {
3147
+ serde_json::from_str(response["body"].as_str().unwrap_or("{}")).expect("a JSON body")
3148
+ };
3149
+
3150
+ let teller = post("Teller", "1").await;
3151
+ assert_eq!(teller["statusCode"], 200, "{teller}");
3152
+ let refusal = &verdict(&teller)["refusals"][0];
3153
+ assert_eq!(
3154
+ (refusal["kind"].as_str(), refusal["stage"].as_str()),
3155
+ (Some("unauthorized"), Some("role"))
3156
+ );
3157
+
3158
+ let clerk = post("Branch clerk", "1").await;
3159
+ assert_eq!(clerk["statusCode"], 200, "{clerk}");
3160
+ assert_eq!(
3161
+ verdict(&clerk),
3162
+ serde_json::json!({ "ok": true, "refusals": [] })
3163
+ );
3164
+
3165
+ // Kept nothing: the same registration, for real, is still accepted.
3166
+ let real = post("Branch clerk", "0").await;
3167
+ assert_eq!(real["statusCode"], 201, "{real}");
3168
+ }
3169
+
3170
+ // `?schema=1` on a command's JSON route: the JSON Schema the build wrote beside the IR.
3171
+ #[tokio::test]
3172
+ async fn a_command_json_route_serves_its_schema_when_asked() {
3173
+ let client = crate::dispatch_tests::tests::memory_store();
3174
+ let wasm = crate::dispatch_tests::tests::wasm_path();
3175
+ let ir_path =
3176
+ std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../dist/banking.ir.json");
3177
+ let ir: Value =
3178
+ serde_json::from_str(&std::fs::read_to_string(&ir_path).expect("banking.ir.json"))
3179
+ .expect("valid IR");
3180
+ crate::ir::install_schemas(ir_path.to_str());
3181
+ let lineage = LineageConfig {
3182
+ domain: "Banking".to_string(),
3183
+ era: None,
3184
+ mirrored: None,
3185
+ seal_every: None,
3186
+ };
3187
+ let invoker = crate::lambda_client::NeverInvoker;
3188
+ let host = Host::new(&client, &wasm, &lineage, &invoker);
3189
+ let cookies = signed_in_cookies("Branch clerk");
3190
+ let query = HashMap::from([("schema".to_string(), "1".to_string())]);
3191
+
3192
+ let found = route(
3193
+ &ir,
3194
+ "GET",
3195
+ "/Banking/Customer/Register.json",
3196
+ &query,
3197
+ "",
3198
+ &cookies,
3199
+ host,
3200
+ )
3201
+ .await;
3202
+ assert_eq!(found["statusCode"], 200, "{found}");
3203
+ let schema: Value =
3204
+ serde_json::from_str(found["body"].as_str().unwrap_or("{}")).expect("JSON");
3205
+ assert_eq!(
3206
+ schema["$schema"],
3207
+ "https://json-schema.org/draft/2020-12/schema"
3208
+ );
3209
+ assert!(schema["properties"].get("reference").is_some(), "{schema}");
3210
+
3211
+ let host = Host::new(&client, &wasm, &lineage, &invoker);
3212
+ let plain = route(
3213
+ &ir,
3214
+ "GET",
3215
+ "/Banking/Customer/Register.json",
3216
+ &HashMap::new(),
3217
+ "",
3218
+ &cookies,
3219
+ host,
3220
+ )
3221
+ .await;
3222
+ assert_eq!(plain["statusCode"], 200);
3223
+ assert!(
3224
+ plain["body"].as_str().unwrap_or("").contains("\"name\""),
3225
+ "the descriptor is unchanged"
3226
+ );
3227
+ }
3228
+
3229
+ // A session cookie signed with the host's own secret, for a caller Governance knows nothing of.
3230
+ fn signed_in_cookies(role: &str) -> HashMap<String, String> {
3231
+ if crate::secrets::var("SESSION_SECRET")
3232
+ .unwrap_or_default()
3233
+ .is_empty()
3234
+ {
3235
+ crate::secrets::provide("SESSION_SECRET", "a-session-secret-for-tests".to_string());
3236
+ }
3237
+ let session = Session {
3238
+ identity_id: String::new(),
3239
+ email: "clerk@example.com".to_string(),
3240
+ name: "Clerk".to_string(),
3241
+ role: Some(role.to_string()),
3242
+ };
3243
+ let mut cookies = HashMap::new();
3244
+ cookies.insert(
3245
+ "session".to_string(),
3246
+ auth::session_cookie(&session_secret(), &session),
3247
+ );
3248
+ cookies
3249
+ }
3250
+
2854
3251
  #[test]
2855
3252
  fn extract_args_surfaces_a_collision_as_an_error_rather_than_panicking() {
2856
3253
  let scalar_field = Field {
@@ -2925,9 +3322,9 @@ mod tests {
2925
3322
 
2926
3323
  // scratch_db provisions a real Postgres schema; only the Stripe hop
2927
3324
  // in payments.rs's own tests is faked, aimed at a local recording server.
2928
- use tokio_postgres::NoTls;
3325
+ use tokio_postgres::{Client, NoTls};
2929
3326
 
2930
- pub(super) async fn scratch_db(name: &str) -> Mutex<Client> {
3327
+ pub(super) async fn scratch_db(name: &str) -> Store {
2931
3328
  let (admin, conn) = tokio_postgres::connect(&crate::test_pg::conninfo("postgres"), NoTls)
2932
3329
  .await
2933
3330
  .expect("connect to postgres");
@@ -2949,8 +3346,10 @@ mod tests {
2949
3346
  tokio::spawn(async move {
2950
3347
  let _ = conn.await;
2951
3348
  });
2952
- crate::journal::ensure_schema(&client).await.unwrap();
2953
- Mutex::new(client)
3349
+ crate::store::journal_sql::ensure_schema(&client)
3350
+ .await
3351
+ .unwrap();
3352
+ crate::store::postgres_store(client)
2954
3353
  }
2955
3354
 
2956
3355
  // Duplicates dispatch.rs's own provision_lineage shape rather than
@@ -2975,7 +3374,7 @@ mod tests {
2975
3374
  .await
2976
3375
  .unwrap();
2977
3376
 
2978
- let journal_table = format!("hecks_journal_{}", crate::journal::snake(domain));
3377
+ let journal_table = format!("hecks_journal_{}", crate::store::journal_sql::snake(domain));
2979
3378
  client
2980
3379
  .batch_execute(&format!(
2981
3380
  "CREATE TABLE IF NOT EXISTS \"{journal_table}\" (
@@ -2993,9 +3392,12 @@ mod tests {
2993
3392
  for name in aggregate_storage_names {
2994
3393
  // domain-qualified (docs/decisions/0059) — matches what a real
2995
3394
  // `journal::append_lineage_mutation` write actually targets now.
2996
- let snapshot_table = crate::journal::qualified_name(
3395
+ let snapshot_table = crate::store::journal_sql::qualified_name(
2997
3396
  domain,
2998
- &format!("{}_head_snapshot_{era}", crate::journal::snake(name)),
3397
+ &format!(
3398
+ "{}_head_snapshot_{era}",
3399
+ crate::store::journal_sql::snake(name)
3400
+ ),
2999
3401
  );
3000
3402
  client
3001
3403
  .batch_execute(&format!("CREATE TABLE IF NOT EXISTS \"{snapshot_table}\" (id text PRIMARY KEY, ordinal bigint NOT NULL, state jsonb NOT NULL)"))
@@ -3098,7 +3500,7 @@ mod tests {
3098
3500
  // Same head-view shape auth.rs's own tests build for the Membership
3099
3501
  // aggregate, seeded with one granted, linked admin and one admitted
3100
3502
  // person with no access yet.
3101
- async fn scratch_members_db(name: &str) -> (Mutex<Client>, Value) {
3503
+ async fn scratch_members_db(name: &str) -> (Store, Value) {
3102
3504
  let client = scratch_db(name).await;
3103
3505
  {
3104
3506
  let guard = client.lock().await;
@@ -3154,11 +3556,11 @@ mod tests {
3154
3556
  assert_eq!(names, ["amy", "Zed"], "sorted case-insensitively by name");
3155
3557
  assert_eq!(
3156
3558
  people[1],
3157
- json!({"name": "Zed", "email": "zed@example.com", "role": "Admin", "linked": true, "granted": true, "disabled": false, "identity_id": "id-1"})
3559
+ json!({"name": "Zed", "email": "zed@example.com", "role": "Admin", "linked": true, "granted": true, "disabled": false, "access_expires_at": null, "expired": false, "identity_id": "id-1"})
3158
3560
  );
3159
3561
  assert_eq!(
3160
3562
  people[0],
3161
- json!({"name": "amy", "email": "amy@example.com", "role": null, "linked": false, "granted": false, "disabled": false, "identity_id": null})
3563
+ json!({"name": "amy", "email": "amy@example.com", "role": null, "linked": false, "granted": false, "disabled": false, "access_expires_at": null, "expired": false, "identity_id": null})
3162
3564
  );
3163
3565
  }
3164
3566
 
@@ -3249,6 +3651,7 @@ mod tests {
3249
3651
  domain: "Acme".to_string(),
3250
3652
  era: Some(1),
3251
3653
  mirrored: None,
3654
+ seal_every: None,
3252
3655
  }
3253
3656
  }
3254
3657
 
@@ -3498,7 +3901,7 @@ mod tests {
3498
3901
  body: &str,
3499
3902
  cookies: &HashMap<String, String>,
3500
3903
  secret: &str,
3501
- client: &Mutex<Client>,
3904
+ client: &Store,
3502
3905
  domain_ir: &Value,
3503
3906
  ) -> Value {
3504
3907
  admin_members_route(
@@ -3642,7 +4045,7 @@ mod tests {
3642
4045
  let created: Value = serde_json::from_str(response["body"].as_str().unwrap()).unwrap();
3643
4046
  assert_eq!(
3644
4047
  created,
3645
- json!({"name": "New Person", "email": "new@example.com", "role": "Admin", "linked": false, "granted": true, "disabled": false, "identity_id": null})
4048
+ json!({"name": "New Person", "email": "new@example.com", "role": "Admin", "linked": false, "granted": true, "disabled": false, "access_expires_at": null, "expired": false, "identity_id": null})
3646
4049
  );
3647
4050
 
3648
4051
  let listing = members_route(&domain_ir, &cookies, secret, &client).await;
@@ -3671,7 +4074,7 @@ mod tests {
3671
4074
 
3672
4075
  // The scratch members database with amy also granted Admin, so two
3673
4076
  // active admins exist and one can act on the other.
3674
- async fn scratch_two_admins(name: &str) -> (Mutex<Client>, Value) {
4077
+ async fn scratch_two_admins(name: &str) -> (Store, Value) {
3675
4078
  let (client, domain_ir) = scratch_members_db(name).await;
3676
4079
  let granted = auth::grant_access(
3677
4080
  &client,
@@ -3688,7 +4091,7 @@ mod tests {
3688
4091
  }
3689
4092
 
3690
4093
  async fn switch_access(
3691
- client: &Mutex<Client>,
4094
+ client: &Store,
3692
4095
  domain_ir: &Value,
3693
4096
  caller: &str,
3694
4097
  email: &str,
@@ -3713,7 +4116,7 @@ mod tests {
3713
4116
  body["error"].as_str().unwrap_or_default().to_string()
3714
4117
  }
3715
4118
 
3716
- async fn person(client: &Mutex<Client>, domain_ir: &Value, email: &str) -> Value {
4119
+ async fn person(client: &Store, domain_ir: &Value, email: &str) -> Value {
3717
4120
  let people = auth::all_people(client, domain_ir).await.unwrap();
3718
4121
  people
3719
4122
  .into_iter()
@@ -3721,7 +4124,7 @@ mod tests {
3721
4124
  .unwrap_or_else(|| panic!("{email} is not listed"))
3722
4125
  }
3723
4126
 
3724
- async fn journal_rows(client: &Mutex<Client>, id: &str) -> i64 {
4127
+ async fn journal_rows(client: &Store, id: &str) -> i64 {
3725
4128
  let guard = client.lock().await;
3726
4129
  guard
3727
4130
  .query_one(
@@ -3835,7 +4238,7 @@ mod tests {
3835
4238
  }
3836
4239
 
3837
4240
  async fn set_role(
3838
- client: &Mutex<Client>,
4241
+ client: &Store,
3839
4242
  domain_ir: &Value,
3840
4243
  caller: &str,
3841
4244
  email: &str,
@@ -4439,6 +4842,7 @@ mod tests {
4439
4842
  domain: "Studio".to_string(),
4440
4843
  era: Some(1),
4441
4844
  mirrored: None,
4845
+ seal_every: None,
4442
4846
  };
4443
4847
  // The wasm path doesn't exist: every refusal below must come before any registration read.
4444
4848
  let wasm_path = Path::new("does-not-exist.wasm");
@@ -4530,6 +4934,7 @@ mod tests {
4530
4934
  domain: "Studio".to_string(),
4531
4935
  era: Some(1),
4532
4936
  mirrored: None,
4937
+ seal_every: None,
4533
4938
  };
4534
4939
  // No wasm at all: clearing the admin gate then fails with a 500, never 401/403.
4535
4940
  let missing_wasm = Path::new("does-not-exist.wasm");
@@ -4874,6 +5279,931 @@ mod tests {
4874
5279
  );
4875
5280
  }
4876
5281
 
5282
+ // ---- time-boxed access: a window on the row that ends by itself ----
5283
+
5284
+ const PAST: &str = "2020-01-01T00:00:00Z";
5285
+ const FUTURE: &str = "2099-01-01T00:00:00Z";
5286
+
5287
+ // Writes `access_expires_at` straight onto a row, the way a window that
5288
+ // ran out (or was mangled) looks to the host, whatever wrote it.
5289
+ async fn set_window(client: &Store, email: &str, window: Value) {
5290
+ client
5291
+ .lock()
5292
+ .await
5293
+ .execute(
5294
+ "UPDATE acme_member_head_snapshot_1 SET state = jsonb_set(state, '{access_expires_at}', $2::jsonb) WHERE id = $1",
5295
+ &[&email, &window],
5296
+ )
5297
+ .await
5298
+ .unwrap();
5299
+ }
5300
+
5301
+ fn form_grant(email: &str, role: &str, days: &str) -> String {
5302
+ format!("email={email}&role={role}&expires_in_days={days}")
5303
+ }
5304
+
5305
+ const NEEDS_OPEN: &str = "a site must keep at least one admin without an end date";
5306
+
5307
+ // Posts a grant through the members form as `caller`; returns the status.
5308
+ async fn form_status(
5309
+ client: &Store,
5310
+ domain_ir: &Value,
5311
+ caller: &str,
5312
+ email: &str,
5313
+ role: &str,
5314
+ days: &str,
5315
+ ) -> Value {
5316
+ let secret = "s3cret";
5317
+ members_page(
5318
+ "POST",
5319
+ &form_grant(email, role, days),
5320
+ &session_cookies(secret, caller),
5321
+ secret,
5322
+ client,
5323
+ domain_ir,
5324
+ )
5325
+ .await
5326
+ }
5327
+
5328
+ async fn expires_of(client: &Store, domain_ir: &Value, email: &str) -> Value {
5329
+ person(client, domain_ir, email).await["access_expires_at"].clone()
5330
+ }
5331
+
5332
+ #[tokio::test]
5333
+ async fn a_lone_open_ended_admin_cannot_be_given_a_window_or_a_lesser_role() {
5334
+ let (client, domain_ir) = scratch_members_db("hecks_host_web_test_open_lone").await;
5335
+ let before = journal_rows(&client, "zed@example.com").await;
5336
+
5337
+ for days in ["7", "365"] {
5338
+ let r = form_status(
5339
+ &client,
5340
+ &domain_ir,
5341
+ "zed@example.com",
5342
+ "zed%40example.com",
5343
+ "Admin",
5344
+ days,
5345
+ )
5346
+ .await;
5347
+ assert_eq!(r["statusCode"], 409, "{r:?}");
5348
+ assert!(r["body"].as_str().unwrap().contains(NEEDS_OPEN), "{r:?}");
5349
+ }
5350
+ // The same grant that drops the admin role also takes the open-ended admin away.
5351
+ let r = form_status(
5352
+ &client,
5353
+ &domain_ir,
5354
+ "zed@example.com",
5355
+ "zed%40example.com",
5356
+ "Member",
5357
+ "",
5358
+ )
5359
+ .await;
5360
+ assert_eq!(r["statusCode"], 409, "{r:?}");
5361
+ assert_eq!(
5362
+ journal_rows(&client, "zed@example.com").await,
5363
+ before,
5364
+ "nothing was written"
5365
+ );
5366
+ assert_eq!(
5367
+ expires_of(&client, &domain_ir, "zed@example.com").await,
5368
+ Value::Null
5369
+ );
5370
+
5371
+ // A plain grant that keeps the role is fine, and so is anything for a non-admin.
5372
+ let r = form_status(
5373
+ &client,
5374
+ &domain_ir,
5375
+ "zed@example.com",
5376
+ "zed%40example.com",
5377
+ "Owner",
5378
+ "",
5379
+ )
5380
+ .await;
5381
+ assert_eq!(r["statusCode"], 302, "{r:?}");
5382
+ let r = form_status(
5383
+ &client,
5384
+ &domain_ir,
5385
+ "zed@example.com",
5386
+ "amy%40example.com",
5387
+ "Member",
5388
+ "5",
5389
+ )
5390
+ .await;
5391
+ assert_eq!(
5392
+ r["statusCode"], 302,
5393
+ "a non-admin takes a window freely: {r:?}"
5394
+ );
5395
+ assert!(expires_of(&client, &domain_ir, "amy@example.com")
5396
+ .await
5397
+ .is_string());
5398
+ }
5399
+
5400
+ #[tokio::test]
5401
+ async fn with_two_open_ended_admins_either_may_take_a_window_but_not_both() {
5402
+ let (client, domain_ir) = scratch_two_admins("hecks_host_web_test_open_two").await;
5403
+ let r = form_status(
5404
+ &client,
5405
+ &domain_ir,
5406
+ "zed@example.com",
5407
+ "amy%40example.com",
5408
+ "Admin",
5409
+ "10",
5410
+ )
5411
+ .await;
5412
+ assert_eq!(r["statusCode"], 302, "{r:?}");
5413
+ assert!(expires_of(&client, &domain_ir, "amy@example.com")
5414
+ .await
5415
+ .is_string());
5416
+
5417
+ // zed is now the one open-ended admin left.
5418
+ let r = form_status(
5419
+ &client,
5420
+ &domain_ir,
5421
+ "amy@example.com",
5422
+ "zed%40example.com",
5423
+ "Admin",
5424
+ "10",
5425
+ )
5426
+ .await;
5427
+ assert_eq!(r["statusCode"], 409, "{r:?}");
5428
+ assert_eq!(
5429
+ expires_of(&client, &domain_ir, "zed@example.com").await,
5430
+ Value::Null
5431
+ );
5432
+ }
5433
+
5434
+ #[tokio::test]
5435
+ async fn an_open_ended_admin_beside_a_windowed_one_is_protected_on_every_path() {
5436
+ let (client, domain_ir) = scratch_two_admins("hecks_host_web_test_open_beside").await;
5437
+ set_window(&client, "amy@example.com", json!(FUTURE)).await;
5438
+
5439
+ // Renewing the windowed admin, or lifting the window, is always allowed.
5440
+ for days in ["30", ""] {
5441
+ let r = form_status(
5442
+ &client,
5443
+ &domain_ir,
5444
+ "zed@example.com",
5445
+ "amy%40example.com",
5446
+ "Admin",
5447
+ days,
5448
+ )
5449
+ .await;
5450
+ assert_eq!(r["statusCode"], 302, "{r:?}");
5451
+ }
5452
+ set_window(&client, "amy@example.com", json!(FUTURE)).await;
5453
+
5454
+ // Windowing, disabling or demoting zed would leave only a window that will end.
5455
+ let r = form_status(
5456
+ &client,
5457
+ &domain_ir,
5458
+ "amy@example.com",
5459
+ "zed%40example.com",
5460
+ "Admin",
5461
+ "9",
5462
+ )
5463
+ .await;
5464
+ assert_eq!(r["statusCode"], 409, "{r:?}");
5465
+ let r = switch_access(
5466
+ &client,
5467
+ &domain_ir,
5468
+ "amy@example.com",
5469
+ "zed@example.com",
5470
+ true,
5471
+ )
5472
+ .await;
5473
+ assert_eq!(r["statusCode"], 409, "{r:?}");
5474
+ assert_eq!(error_of(&r), NEEDS_OPEN);
5475
+ let r = set_role(
5476
+ &client,
5477
+ &domain_ir,
5478
+ "amy@example.com",
5479
+ "zed@example.com",
5480
+ "Member",
5481
+ )
5482
+ .await;
5483
+ assert_eq!(r["statusCode"], 409, "{r:?}");
5484
+ assert_eq!(error_of(&r), NEEDS_OPEN);
5485
+ let zed = person(&client, &domain_ir, "zed@example.com").await;
5486
+ assert_eq!(
5487
+ (zed["role"].clone(), zed["disabled"].clone()),
5488
+ (json!("Admin"), json!(false))
5489
+ );
5490
+
5491
+ // The windowed admin itself may be disabled, demoted or re-enabled: zed stays.
5492
+ let r = switch_access(
5493
+ &client,
5494
+ &domain_ir,
5495
+ "zed@example.com",
5496
+ "amy@example.com",
5497
+ true,
5498
+ )
5499
+ .await;
5500
+ assert_eq!(r["statusCode"], 200, "{r:?}");
5501
+ let r = switch_access(
5502
+ &client,
5503
+ &domain_ir,
5504
+ "zed@example.com",
5505
+ "amy@example.com",
5506
+ false,
5507
+ )
5508
+ .await;
5509
+ assert_eq!(r["statusCode"], 200, "{r:?}");
5510
+ let r = set_role(
5511
+ &client,
5512
+ &domain_ir,
5513
+ "zed@example.com",
5514
+ "amy@example.com",
5515
+ "Member",
5516
+ )
5517
+ .await;
5518
+ assert_eq!(r["statusCode"], 200, "{r:?}");
5519
+ }
5520
+
5521
+ #[tokio::test]
5522
+ async fn a_lapsed_or_unreadable_admin_is_not_an_open_ended_active_one() {
5523
+ let (client, domain_ir) = scratch_two_admins("hecks_host_web_test_open_lapsed").await;
5524
+ set_window(&client, "amy@example.com", json!(PAST)).await;
5525
+ // amy's window ended, so zed is the only active admin and is protected.
5526
+ let r = form_status(
5527
+ &client,
5528
+ &domain_ir,
5529
+ "zed@example.com",
5530
+ "zed%40example.com",
5531
+ "Admin",
5532
+ "3",
5533
+ )
5534
+ .await;
5535
+ assert_eq!(r["statusCode"], 409, "{r:?}");
5536
+ let r = switch_access(
5537
+ &client,
5538
+ &domain_ir,
5539
+ "zed@example.com",
5540
+ "amy@example.com",
5541
+ true,
5542
+ )
5543
+ .await;
5544
+ assert_eq!(
5545
+ r["statusCode"], 200,
5546
+ "disabling an admin who already lapsed is harmless: {r:?}"
5547
+ );
5548
+
5549
+ // An unreadable window fails closed: zed is lapsed, so amy is the one left.
5550
+ let (client, domain_ir) = scratch_two_admins("hecks_host_web_test_open_unreadable").await;
5551
+ set_window(&client, "zed@example.com", json!("whenever")).await;
5552
+ let r = form_status(
5553
+ &client,
5554
+ &domain_ir,
5555
+ "amy@example.com",
5556
+ "amy%40example.com",
5557
+ "Admin",
5558
+ "3",
5559
+ )
5560
+ .await;
5561
+ assert_eq!(r["statusCode"], 409, "{r:?}");
5562
+ let r = set_role(
5563
+ &client,
5564
+ &domain_ir,
5565
+ "amy@example.com",
5566
+ "amy@example.com",
5567
+ "Member",
5568
+ )
5569
+ .await;
5570
+ assert_eq!(r["statusCode"], 409, "{r:?}");
5571
+ // Renewing the unreadable row, or clearing it with a plain grant, is allowed.
5572
+ let r = form_status(
5573
+ &client,
5574
+ &domain_ir,
5575
+ "amy@example.com",
5576
+ "zed%40example.com",
5577
+ "Admin",
5578
+ "3",
5579
+ )
5580
+ .await;
5581
+ assert_eq!(r["statusCode"], 302, "{r:?}");
5582
+ set_window(&client, "zed@example.com", json!("whenever")).await;
5583
+ let r = form_status(
5584
+ &client,
5585
+ &domain_ir,
5586
+ "amy@example.com",
5587
+ "zed%40example.com",
5588
+ "Admin",
5589
+ "",
5590
+ )
5591
+ .await;
5592
+ assert_eq!(r["statusCode"], 302, "{r:?}");
5593
+ assert_eq!(
5594
+ expires_of(&client, &domain_ir, "zed@example.com").await,
5595
+ Value::Null
5596
+ );
5597
+ }
5598
+
5599
+ #[tokio::test]
5600
+ async fn a_client_admin_stays_open_ended_while_an_operator_works_on_a_window() {
5601
+ let (client, domain_ir) = scratch_two_admins("hecks_host_web_test_open_operator").await;
5602
+ // amy is the operator, with a window; zed is the client's open-ended admin.
5603
+ let r = form_status(
5604
+ &client,
5605
+ &domain_ir,
5606
+ "zed@example.com",
5607
+ "amy%40example.com",
5608
+ "Admin",
5609
+ "30",
5610
+ )
5611
+ .await;
5612
+ assert_eq!(r["statusCode"], 302, "{r:?}");
5613
+ let r = form_status(
5614
+ &client,
5615
+ &domain_ir,
5616
+ "zed@example.com",
5617
+ "amy%40example.com",
5618
+ "Admin",
5619
+ "60",
5620
+ )
5621
+ .await;
5622
+ assert_eq!(r["statusCode"], 302, "the operator's window renews: {r:?}");
5623
+ let r = form_status(
5624
+ &client,
5625
+ &domain_ir,
5626
+ "amy@example.com",
5627
+ "zed%40example.com",
5628
+ "Admin",
5629
+ "1",
5630
+ )
5631
+ .await;
5632
+ assert_eq!(
5633
+ r["statusCode"], 409,
5634
+ "the operator cannot window the client admin: {r:?}"
5635
+ );
5636
+ // Even after the operator's window ends, the client admin still opens the gate.
5637
+ set_window(&client, "amy@example.com", json!(PAST)).await;
5638
+ assert!(
5639
+ auth::caller_is_admin(&client, &domain_ir, "zed@example.com")
5640
+ .await
5641
+ .unwrap()
5642
+ );
5643
+ assert!(
5644
+ !auth::caller_is_admin(&client, &domain_ir, "amy@example.com")
5645
+ .await
5646
+ .unwrap()
5647
+ );
5648
+ }
5649
+
5650
+ #[tokio::test]
5651
+ async fn the_first_admin_of_a_site_must_be_open_ended() {
5652
+ let (client, domain_ir) = scratch_members_db("hecks_host_web_test_open_first").await;
5653
+ // Nobody holds an admin role yet.
5654
+ client
5655
+ .lock()
5656
+ .await
5657
+ .execute("UPDATE acme_member_head_snapshot_1 SET state = state - 'role' WHERE id = 'zed@example.com'", &[])
5658
+ .await
5659
+ .unwrap();
5660
+ let outcome = |days: Option<&'static str>, who: &'static str| {
5661
+ let (client, domain_ir) = (&client, &domain_ir);
5662
+ async move {
5663
+ auth::grant_access_until(
5664
+ client,
5665
+ Path::new("unused"),
5666
+ &members_config(),
5667
+ domain_ir,
5668
+ who,
5669
+ "Admin",
5670
+ days,
5671
+ )
5672
+ .await
5673
+ .unwrap()
5674
+ }
5675
+ };
5676
+ assert_eq!(
5677
+ outcome(Some(FUTURE), "zed@example.com").await,
5678
+ auth::GrantOutcome::LastOpenAdmin
5679
+ );
5680
+ assert_eq!(
5681
+ person(&client, &domain_ir, "zed@example.com").await["role"],
5682
+ Value::Null
5683
+ );
5684
+
5685
+ // Open-ended first admin is allowed, and a windowed second one after it.
5686
+ assert_eq!(
5687
+ outcome(None, "zed@example.com").await,
5688
+ auth::GrantOutcome::Done
5689
+ );
5690
+ let r = form_status(
5691
+ &client,
5692
+ &domain_ir,
5693
+ "zed@example.com",
5694
+ "amy%40example.com",
5695
+ "Admin",
5696
+ "5",
5697
+ )
5698
+ .await;
5699
+ assert_eq!(r["statusCode"], 302, "{r:?}");
5700
+ }
5701
+
5702
+ #[tokio::test]
5703
+ async fn a_json_grant_to_a_new_person_never_trips_the_open_ended_guard() {
5704
+ let secret = "s3cret";
5705
+ let (client, domain_ir) = scratch_members_db("hecks_host_web_test_open_json").await;
5706
+ let body = json!({"email": "new@example.com", "name": "New", "role": "Admin", "expires_in_days": 5}).to_string();
5707
+ let r = add_member_route(
5708
+ &domain_ir,
5709
+ &body,
5710
+ &session_cookies(secret, "zed@example.com"),
5711
+ secret,
5712
+ &client,
5713
+ Path::new("unused"),
5714
+ &members_config(),
5715
+ )
5716
+ .await;
5717
+ assert_eq!(r["statusCode"], 201, "{r:?}");
5718
+ // The lone open-ended admin is still zed; windowing him is refused.
5719
+ let outcome = auth::grant_access_until(
5720
+ &client,
5721
+ Path::new("unused"),
5722
+ &members_config(),
5723
+ &domain_ir,
5724
+ "zed@example.com",
5725
+ "Admin",
5726
+ Some(FUTURE),
5727
+ )
5728
+ .await
5729
+ .unwrap();
5730
+ assert_eq!(outcome, auth::GrantOutcome::LastOpenAdmin);
5731
+ let outcome = auth::grant_access_until(
5732
+ &client,
5733
+ Path::new("unused"),
5734
+ &members_config(),
5735
+ &domain_ir,
5736
+ "nobody@example.com",
5737
+ "Admin",
5738
+ None,
5739
+ )
5740
+ .await
5741
+ .unwrap();
5742
+ assert_eq!(outcome, auth::GrantOutcome::UnknownPerson);
5743
+ }
5744
+
5745
+ #[tokio::test]
5746
+ async fn a_person_whose_window_has_ended_gets_no_access_anywhere_but_the_row_keeps_its_role() {
5747
+ let secret = "s3cret";
5748
+ let (client, domain_ir) = scratch_two_admins("hecks_host_web_test_expiry_access").await;
5749
+ client
5750
+ .lock()
5751
+ .await
5752
+ .execute(
5753
+ "UPDATE acme_member_head_snapshot_1 SET state = state || '{\"identity_id\": {\"value\": \"id-amy\"}}'::jsonb WHERE id = 'amy@example.com'",
5754
+ &[],
5755
+ )
5756
+ .await
5757
+ .unwrap();
5758
+ set_window(&client, "amy@example.com", json!(PAST)).await;
5759
+ let amy_cookies = session_cookies(secret, "amy@example.com");
5760
+
5761
+ assert!(!auth::has_access(&client, &domain_ir, "amy@example.com")
5762
+ .await
5763
+ .unwrap());
5764
+ assert_eq!(
5765
+ auth::active_role(&client, &domain_ir, "amy@example.com")
5766
+ .await
5767
+ .unwrap(),
5768
+ None
5769
+ );
5770
+ assert!(
5771
+ !auth::caller_is_admin(&client, &domain_ir, "amy@example.com")
5772
+ .await
5773
+ .unwrap()
5774
+ );
5775
+ // The CMS-facing session check and the SSO token route both refuse her.
5776
+ assert_eq!(
5777
+ accounts_me_route(&domain_ir, &amy_cookies, secret, &client).await["statusCode"],
5778
+ 401
5779
+ );
5780
+ assert_eq!(
5781
+ accounts_sso_token_route(&domain_ir, &amy_cookies, secret, &client).await["statusCode"],
5782
+ 401
5783
+ );
5784
+ // A new Google sign-in through her linked identity is refused too.
5785
+ assert!(
5786
+ auth::session_for_member_by_identity(&client, &domain_ir, "id-amy")
5787
+ .await
5788
+ .unwrap()
5789
+ .is_none()
5790
+ );
5791
+ let amy = person(&client, &domain_ir, "amy@example.com").await;
5792
+ assert_eq!(
5793
+ (
5794
+ amy["role"].as_str(),
5795
+ amy["granted"].clone(),
5796
+ amy["disabled"].clone(),
5797
+ amy["expired"].clone(),
5798
+ amy["access_expires_at"].as_str()
5799
+ ),
5800
+ (
5801
+ Some("Admin"),
5802
+ json!(false),
5803
+ json!(false),
5804
+ json!(true),
5805
+ Some(PAST)
5806
+ )
5807
+ );
5808
+ // Her window has not touched zed's access.
5809
+ assert!(auth::has_access(&client, &domain_ir, "zed@example.com")
5810
+ .await
5811
+ .unwrap());
5812
+ }
5813
+
5814
+ #[tokio::test]
5815
+ async fn a_window_still_open_changes_nothing_until_it_ends() {
5816
+ let secret = "s3cret";
5817
+ let (client, domain_ir) = scratch_two_admins("hecks_host_web_test_expiry_open").await;
5818
+ set_window(&client, "amy@example.com", json!(FUTURE)).await;
5819
+
5820
+ assert!(auth::has_access(&client, &domain_ir, "amy@example.com")
5821
+ .await
5822
+ .unwrap());
5823
+ assert!(
5824
+ auth::caller_is_admin(&client, &domain_ir, "amy@example.com")
5825
+ .await
5826
+ .unwrap()
5827
+ );
5828
+ assert_eq!(
5829
+ accounts_me_route(
5830
+ &domain_ir,
5831
+ &session_cookies(secret, "amy@example.com"),
5832
+ secret,
5833
+ &client
5834
+ )
5835
+ .await["statusCode"],
5836
+ 200
5837
+ );
5838
+ let amy = person(&client, &domain_ir, "amy@example.com").await;
5839
+ assert_eq!(
5840
+ (
5841
+ amy["granted"].clone(),
5842
+ amy["expired"].clone(),
5843
+ amy["access_expires_at"].as_str()
5844
+ ),
5845
+ (json!(true), json!(false), Some(FUTURE))
5846
+ );
5847
+ }
5848
+
5849
+ #[tokio::test]
5850
+ async fn a_malformed_window_counts_as_ended_and_fails_closed() {
5851
+ let (client, domain_ir) = scratch_two_admins("hecks_host_web_test_expiry_malformed").await;
5852
+ for bad in [
5853
+ json!("tomorrow"),
5854
+ json!(""),
5855
+ json!(12345),
5856
+ json!(true),
5857
+ json!("2099-13-40T00:00:00Z"),
5858
+ json!("2099-01-01T00:00:00+02:00"),
5859
+ json!({"value": "not a time"}),
5860
+ ] {
5861
+ set_window(&client, "amy@example.com", bad.clone()).await;
5862
+ assert!(
5863
+ !auth::has_access(&client, &domain_ir, "amy@example.com")
5864
+ .await
5865
+ .unwrap(),
5866
+ "{bad}"
5867
+ );
5868
+ assert_eq!(
5869
+ person(&client, &domain_ir, "amy@example.com").await["expired"],
5870
+ true,
5871
+ "{bad}"
5872
+ );
5873
+ }
5874
+ // A wrapped value reads like a bare one; null is no window at all.
5875
+ set_window(&client, "amy@example.com", json!({"value": FUTURE})).await;
5876
+ assert!(auth::has_access(&client, &domain_ir, "amy@example.com")
5877
+ .await
5878
+ .unwrap());
5879
+ set_window(&client, "amy@example.com", Value::Null).await;
5880
+ assert!(auth::has_access(&client, &domain_ir, "amy@example.com")
5881
+ .await
5882
+ .unwrap());
5883
+ }
5884
+
5885
+ #[tokio::test]
5886
+ async fn an_expired_admin_neither_passes_the_admin_gate_nor_counts_as_the_last_admin() {
5887
+ let (client, domain_ir) = scratch_two_admins("hecks_host_web_test_expiry_admins").await;
5888
+ set_window(&client, "amy@example.com", json!(PAST)).await;
5889
+
5890
+ // The expired admin cannot act as one.
5891
+ let response = switch_access(
5892
+ &client,
5893
+ &domain_ir,
5894
+ "amy@example.com",
5895
+ "zed@example.com",
5896
+ true,
5897
+ )
5898
+ .await;
5899
+ assert_eq!(response["statusCode"], 403, "{response:?}");
5900
+ let response = set_role(
5901
+ &client,
5902
+ &domain_ir,
5903
+ "amy@example.com",
5904
+ "zed@example.com",
5905
+ "Member",
5906
+ )
5907
+ .await;
5908
+ assert_eq!(response["statusCode"], 403, "{response:?}");
5909
+
5910
+ // Zed is the only admin that counts, so the last-admin guard protects him
5911
+ // even though two rows hold the Admin role.
5912
+ let response = set_role(
5913
+ &client,
5914
+ &domain_ir,
5915
+ "zed@example.com",
5916
+ "zed@example.com",
5917
+ "Member",
5918
+ )
5919
+ .await;
5920
+ assert_eq!(response["statusCode"], 409, "{response:?}");
5921
+ assert_eq!(
5922
+ auth::set_person_disabled(
5923
+ &client,
5924
+ &members_config(),
5925
+ &domain_ir,
5926
+ "zed@example.com",
5927
+ "zed@example.com",
5928
+ true
5929
+ )
5930
+ .await
5931
+ .unwrap(),
5932
+ auth::DisableOutcome::SelfDisable
5933
+ );
5934
+
5935
+ // Removing the expired admin is not refused: she was never one of the active admins.
5936
+ let response = switch_access(
5937
+ &client,
5938
+ &domain_ir,
5939
+ "zed@example.com",
5940
+ "amy@example.com",
5941
+ true,
5942
+ )
5943
+ .await;
5944
+ assert_eq!(response["statusCode"], 200, "{response:?}");
5945
+ assert_eq!(
5946
+ person(&client, &domain_ir, "amy@example.com").await["disabled"],
5947
+ true
5948
+ );
5949
+ }
5950
+
5951
+ #[tokio::test]
5952
+ async fn a_second_admin_whose_window_ended_does_not_unlock_removing_the_last_active_one() {
5953
+ let (client, domain_ir) = scratch_two_admins("hecks_host_web_test_expiry_last").await;
5954
+ // amy would be the second admin, but she has run out: demoting zed through any
5955
+ // other admin is impossible and zed alone is still protected from demotion.
5956
+ set_window(&client, "amy@example.com", json!(PAST)).await;
5957
+ assert_eq!(
5958
+ auth::set_person_role(
5959
+ &client,
5960
+ &members_config(),
5961
+ &domain_ir,
5962
+ "zed@example.com",
5963
+ "zed@example.com",
5964
+ "Member"
5965
+ )
5966
+ .await
5967
+ .unwrap(),
5968
+ auth::RoleOutcome::LastAdmin
5969
+ );
5970
+ }
5971
+
5972
+ #[tokio::test]
5973
+ async fn granting_with_a_window_sets_it_and_granting_again_renews_or_clears_it() {
5974
+ let secret = "s3cret";
5975
+ let (client, domain_ir) = scratch_members_db("hecks_host_web_test_expiry_grant").await;
5976
+ let admin = session_cookies(secret, "zed@example.com");
5977
+ let now = || {
5978
+ std::time::SystemTime::now()
5979
+ .duration_since(std::time::UNIX_EPOCH)
5980
+ .unwrap()
5981
+ .as_secs() as i64
5982
+ };
5983
+ let ends = |person: &Value| {
5984
+ crate::clock::parse_iso_utc(person["access_expires_at"].as_str().unwrap()).unwrap()
5985
+ };
5986
+
5987
+ let first = members_page(
5988
+ "POST",
5989
+ &form_grant("amy%40example.com", "Member", "7"),
5990
+ &admin,
5991
+ secret,
5992
+ &client,
5993
+ &domain_ir,
5994
+ )
5995
+ .await;
5996
+ assert_eq!(first["statusCode"], 302, "{first:?}");
5997
+ let amy = person(&client, &domain_ir, "amy@example.com").await;
5998
+ assert_eq!(amy["granted"], true);
5999
+ assert!(
6000
+ (ends(&amy) - (now() + 7 * 86_400)).abs() < 120,
6001
+ "a seven day window: {amy}"
6002
+ );
6003
+
6004
+ // Granting again extends the window from now.
6005
+ let renewed = members_page(
6006
+ "POST",
6007
+ &form_grant("amy%40example.com", "Member", "30"),
6008
+ &admin,
6009
+ secret,
6010
+ &client,
6011
+ &domain_ir,
6012
+ )
6013
+ .await;
6014
+ assert_eq!(renewed["statusCode"], 302, "{renewed:?}");
6015
+ let amy = person(&client, &domain_ir, "amy@example.com").await;
6016
+ assert!((ends(&amy) - (now() + 30 * 86_400)).abs() < 120, "{amy}");
6017
+
6018
+ // Renewal revives a lapsed row: the role comes back with the new window.
6019
+ set_window(&client, "amy@example.com", json!(PAST)).await;
6020
+ assert!(!auth::has_access(&client, &domain_ir, "amy@example.com")
6021
+ .await
6022
+ .unwrap());
6023
+ let revived = members_page(
6024
+ "POST",
6025
+ &form_grant("amy%40example.com", "Member", "1"),
6026
+ &admin,
6027
+ secret,
6028
+ &client,
6029
+ &domain_ir,
6030
+ )
6031
+ .await;
6032
+ assert_eq!(revived["statusCode"], 302, "{revived:?}");
6033
+ assert!(auth::has_access(&client, &domain_ir, "amy@example.com")
6034
+ .await
6035
+ .unwrap());
6036
+
6037
+ // A grant with an empty field makes the access open-ended again.
6038
+ let open = members_page(
6039
+ "POST",
6040
+ &form_grant("amy%40example.com", "Member", ""),
6041
+ &admin,
6042
+ secret,
6043
+ &client,
6044
+ &domain_ir,
6045
+ )
6046
+ .await;
6047
+ assert_eq!(open["statusCode"], 302, "{open:?}");
6048
+ let amy = person(&client, &domain_ir, "amy@example.com").await;
6049
+ assert_eq!(
6050
+ (amy["access_expires_at"].clone(), amy["expired"].clone()),
6051
+ (Value::Null, json!(false))
6052
+ );
6053
+ }
6054
+
6055
+ #[tokio::test]
6056
+ async fn a_grant_with_an_unusable_expires_in_days_is_refused_and_writes_nothing() {
6057
+ let secret = "s3cret";
6058
+ let (client, domain_ir) = scratch_members_db("hecks_host_web_test_expiry_refuse").await;
6059
+ let admin = session_cookies(secret, "zed@example.com");
6060
+ let before = journal_rows(&client, "amy@example.com").await;
6061
+
6062
+ for bad in ["0", "-3", "366", "1.5", "soon", "99999999999999999999"] {
6063
+ let form = members_page(
6064
+ "POST",
6065
+ &form_grant("amy%40example.com", "Member", bad),
6066
+ &admin,
6067
+ secret,
6068
+ &client,
6069
+ &domain_ir,
6070
+ )
6071
+ .await;
6072
+ assert_eq!(form["statusCode"], 400, "form {bad}: {form:?}");
6073
+ }
6074
+ for bad in [
6075
+ json!(0),
6076
+ json!(366),
6077
+ json!(1.5),
6078
+ json!("soon"),
6079
+ json!([7]),
6080
+ json!(-1),
6081
+ ] {
6082
+ let body = json!({"email": "new@example.com", "name": "New", "expires_in_days": bad})
6083
+ .to_string();
6084
+ let response = add_member_route(
6085
+ &domain_ir,
6086
+ &body,
6087
+ &admin,
6088
+ secret,
6089
+ &client,
6090
+ Path::new("unused"),
6091
+ &members_config(),
6092
+ )
6093
+ .await;
6094
+ assert_eq!(response["statusCode"], 400, "json {bad}: {response:?}");
6095
+ }
6096
+ assert_eq!(journal_rows(&client, "amy@example.com").await, before);
6097
+ assert_eq!(
6098
+ auth::all_people(&client, &domain_ir).await.unwrap().len(),
6099
+ 2,
6100
+ "nobody was admitted by a refused grant"
6101
+ );
6102
+ for edge in ["1", "365"] {
6103
+ let form = members_page(
6104
+ "POST",
6105
+ &form_grant("amy%40example.com", "Member", edge),
6106
+ &admin,
6107
+ secret,
6108
+ &client,
6109
+ &domain_ir,
6110
+ )
6111
+ .await;
6112
+ assert_eq!(form["statusCode"], 302, "{edge}: {form:?}");
6113
+ }
6114
+ }
6115
+
6116
+ #[tokio::test]
6117
+ async fn the_json_admit_and_grant_route_takes_expires_in_days_as_a_number_or_a_string() {
6118
+ let secret = "s3cret";
6119
+ let (client, domain_ir) = scratch_members_db("hecks_host_web_test_expiry_json").await;
6120
+ let admin = session_cookies(secret, "zed@example.com");
6121
+ for (email, days) in [("a@example.com", json!(14)), ("b@example.com", json!("14"))] {
6122
+ let body =
6123
+ json!({"email": email, "name": "N", "role": "Member", "expires_in_days": days})
6124
+ .to_string();
6125
+ let response = add_member_route(
6126
+ &domain_ir,
6127
+ &body,
6128
+ &admin,
6129
+ secret,
6130
+ &client,
6131
+ Path::new("unused"),
6132
+ &members_config(),
6133
+ )
6134
+ .await;
6135
+ assert_eq!(response["statusCode"], 201, "{response:?}");
6136
+ let created: Value = serde_json::from_str(response["body"].as_str().unwrap()).unwrap();
6137
+ assert_eq!(created["expired"], false);
6138
+ let listed = person(&client, &domain_ir, email).await;
6139
+ assert_eq!(listed["access_expires_at"], created["access_expires_at"]);
6140
+ assert!(listed["access_expires_at"].as_str().unwrap().ends_with('Z'));
6141
+ assert_eq!(listed["granted"], true);
6142
+ }
6143
+ }
6144
+
6145
+ #[tokio::test]
6146
+ async fn the_members_page_shows_each_window_and_offers_the_days_field() {
6147
+ let (client, domain_ir) = scratch_two_admins("hecks_host_web_test_expiry_page").await;
6148
+ set_window(&client, "amy@example.com", json!(PAST)).await;
6149
+ set_window(&client, "zed@example.com", json!(FUTURE)).await;
6150
+ let page = admin_members_page(&client, &domain_ir).await;
6151
+ assert!(page.contains(&format!("access ended {PAST}")), "{page}");
6152
+ assert!(page.contains(&format!("linked, until {FUTURE}")), "{page}");
6153
+ assert!(page.contains(r#"name="expires_in_days""#));
6154
+ }
6155
+
6156
+ #[tokio::test]
6157
+ async fn rows_without_a_window_behave_exactly_as_before() {
6158
+ let secret = "s3cret";
6159
+ let (client, domain_ir) = scratch_two_admins("hecks_host_web_test_expiry_none").await;
6160
+ assert!(auth::has_access(&client, &domain_ir, "amy@example.com")
6161
+ .await
6162
+ .unwrap());
6163
+ assert_eq!(
6164
+ auth::active_role(&client, &domain_ir, "amy@example.com")
6165
+ .await
6166
+ .unwrap()
6167
+ .as_deref(),
6168
+ Some("Admin")
6169
+ );
6170
+ assert_eq!(
6171
+ accounts_me_route(
6172
+ &domain_ir,
6173
+ &session_cookies(secret, "zed@example.com"),
6174
+ secret,
6175
+ &client
6176
+ )
6177
+ .await["statusCode"],
6178
+ 200
6179
+ );
6180
+ // The disabled flag still works on its own, and a plain grant adds no field to the row.
6181
+ assert_eq!(
6182
+ switch_access(
6183
+ &client,
6184
+ &domain_ir,
6185
+ "zed@example.com",
6186
+ "amy@example.com",
6187
+ true
6188
+ )
6189
+ .await["statusCode"],
6190
+ 200
6191
+ );
6192
+ assert!(!auth::has_access(&client, &domain_ir, "amy@example.com")
6193
+ .await
6194
+ .unwrap());
6195
+ let guard = client.lock().await;
6196
+ let with_field: i64 = guard
6197
+ .query_one(
6198
+ "SELECT count(*) FROM acme_member_head_snapshot_1 WHERE state ? 'access_expires_at'",
6199
+ &[],
6200
+ )
6201
+ .await
6202
+ .unwrap()
6203
+ .get(0);
6204
+ assert_eq!(with_field, 0);
6205
+ }
6206
+
4877
6207
  #[test]
4878
6208
  fn sorted_by_name_orders_case_insensitively_and_puts_a_missing_name_first() {
4879
6209
  let people = vec![