hecks 4.0.0 → 6.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 (1281) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/bluebook/pizzas.bluebook +1 -0
  3. data/exe/hecks +15 -3
  4. data/lib/hecks/adapters/driven/claude_code.rb +1 -1
  5. data/lib/hecks/adapters/driven/d1.rb +2 -1
  6. data/lib/hecks/adapters/driven/folder.rb +35 -28
  7. data/lib/hecks/adapters/driven/folder_overlay.rb +51 -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/identity_registry.rb +1 -1
  13. data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -2
  14. data/lib/hecks/adapters/driven/key_keeper.rb +42 -0
  15. data/lib/hecks/adapters/driven/lambda/client.rb +8 -2
  16. data/lib/hecks/adapters/driven/lambda/remote_dispatcher.rb +185 -0
  17. data/lib/hecks/adapters/driven/lambda.rb +14 -2
  18. data/lib/hecks/adapters/driven/memory/dispatch_claims.rb +25 -0
  19. data/lib/hecks/adapters/driven/memory.rb +15 -4
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +1 -1
  21. data/lib/hecks/adapters/driven/postgres/journal.rb +46 -0
  22. data/lib/hecks/adapters/driven/postgres/outbox.rb +31 -5
  23. data/lib/hecks/adapters/driven/postgres/projection.rb +43 -0
  24. data/lib/hecks/adapters/driven/postgres/repository.rb +1 -1
  25. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +2 -1
  26. data/lib/hecks/adapters/driven/postgres/set_dialect.rb +44 -0
  27. data/lib/hecks/adapters/driven/postgres.adapter +8 -0
  28. data/lib/hecks/adapters/driven/postgres.rb +8 -3
  29. data/lib/hecks/adapters/driven/postgres_dump/connection.rb +57 -0
  30. data/lib/hecks/adapters/driven/postgres_dump.rb +117 -0
  31. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/boot.rb +6 -3
  32. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/class_methods.rb +1 -1
  33. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/dialect.rb +1 -1
  34. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/chain_sql.rb +8 -4
  35. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/domain_rename.rb +2 -2
  36. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/era_integrity.rb +2 -2
  37. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/era_store.rb +3 -3
  38. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/mint_transaction.rb +2 -2
  39. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/sql_templates.rb +6 -2
  40. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/tail_merge.rb +1 -1
  41. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage.rb +1 -1
  42. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage_manager/context.rb +15 -5
  43. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage_manager/coverage_check.rb +13 -11
  44. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage_manager/era_resolver.rb +23 -6
  45. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage_manager/merge_coordinator.rb +2 -2
  46. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage_manager/minter.rb +12 -9
  47. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage_manager/refusals.rb +13 -1
  48. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage_manager.rb +6 -6
  49. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/query_phases.rb +8 -2
  50. data/lib/hecks/adapters/driven/postgres_era.adapter +7 -1
  51. data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era.rb +14 -13
  52. data/lib/hecks/adapters/driven/prism.rb +4 -3
  53. data/lib/hecks/adapters/driven/projection/read_model_walk.rb +126 -0
  54. data/lib/hecks/adapters/driven/projection/row_decoding.rb +57 -0
  55. data/lib/hecks/adapters/driven/set_transform/assignments.rb +101 -0
  56. data/lib/hecks/adapters/driven/set_transform/expressions.rb +138 -0
  57. data/lib/hecks/adapters/driven/set_transform.rb +81 -0
  58. data/lib/hecks/adapters/driven/sql_keyset.rb +61 -0
  59. data/lib/hecks/adapters/driven/sql_query_builder.rb +28 -9
  60. data/lib/hecks/adapters/driven/sqlite/codec.rb +1 -1
  61. data/lib/hecks/adapters/driven/sqlite/ddl.rb +7 -0
  62. data/lib/hecks/adapters/driven/sqlite/dialect.rb +2 -1
  63. data/lib/hecks/adapters/driven/sqlite/dispatch_claims.rb +28 -0
  64. data/lib/hecks/adapters/driven/sqlite/journal.rb +46 -0
  65. data/lib/hecks/adapters/driven/sqlite/outbox.rb +3 -2
  66. data/lib/hecks/adapters/driven/sqlite/projection.rb +9 -135
  67. data/lib/hecks/adapters/driven/sqlite/repository.rb +1 -1
  68. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +16 -4
  69. data/lib/hecks/adapters/driven/sqlite/set_dialect.rb +39 -0
  70. data/lib/hecks/adapters/driven/sqlite.adapter +2 -0
  71. data/lib/hecks/adapters/driven/sqlite.rb +11 -2
  72. data/lib/hecks/adapters/driven.rb +8 -3
  73. data/lib/hecks/adapters/driving/cli.rb +1 -1
  74. data/lib/hecks/adapters/driving/cli_runner/answers.rb +23 -16
  75. data/lib/hecks/adapters/driving/cli_runner/resolution.rb +25 -24
  76. data/lib/hecks/adapters/driving/cli_runner/settling.rb +9 -5
  77. data/lib/hecks/adapters/driving/cli_runner/streaming.rb +1 -1
  78. data/lib/hecks/adapters/driving/cli_runner/suggestions.rb +15 -7
  79. data/lib/hecks/adapters/driving/cli_runner/verb_names.rb +49 -0
  80. data/lib/hecks/adapters/driving/cli_runner.rb +4 -2
  81. data/lib/hecks/adapters/driving/github_webhook.rb +2 -1
  82. data/lib/hecks/adapters/driving/handle/verb_methods.rb +46 -0
  83. data/lib/hecks/adapters/driving/handle.rb +9 -17
  84. data/lib/hecks/adapters/driving/json.rb +19 -17
  85. data/lib/hecks/adapters/driving/launcher_options.rb +37 -32
  86. data/lib/hecks/adapters/driving/mcp_scope/env_problems.rb +3 -1
  87. data/lib/hecks/adapters/driving/mcp_scope.rb +8 -7
  88. data/lib/hecks/adapters/driving/newcomer.rb +6 -5
  89. data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +13 -12
  90. data/lib/hecks/adapters/driving/ruby/bulk_verbs.rb +127 -0
  91. data/lib/hecks/adapters/driving/ruby/{chapter.rb → declaration.rb} +27 -27
  92. data/lib/hecks/adapters/driving/ruby/query_rows.rb +27 -0
  93. data/lib/hecks/adapters/driving/ruby.rb +25 -24
  94. data/lib/hecks/adapters/driving/usage_cache.rb +14 -3
  95. data/lib/hecks/assembly.rb +101 -0
  96. data/lib/hecks/behaviors/dsl.rb +13 -3
  97. data/lib/hecks/behaviors/expectations/boot.rb +7 -6
  98. data/lib/hecks/behaviors/expectations/checks.rb +1 -1
  99. data/lib/hecks/behaviors/expectations/dispatching.rb +10 -9
  100. data/lib/hecks/behaviors/expectations/qualify.rb +9 -9
  101. data/lib/hecks/behaviors/expectations.rb +24 -10
  102. data/lib/hecks/behaviors/ir.rb +4 -2
  103. data/lib/hecks/behaviors/rspec.rb +1 -1
  104. data/lib/hecks/behaviors/runner.rb +31 -4
  105. data/lib/hecks/bench/environment.rb +6 -2
  106. data/lib/hecks/bench/postgres_probe.rb +3 -1
  107. data/lib/hecks/bench/report.rb +4 -1
  108. data/lib/hecks/bench/suite.rb +9 -3
  109. data/lib/hecks/bluebook/acknowledgements.rb +81 -0
  110. data/lib/hecks/bluebook/aggregate.rb +13 -4
  111. data/lib/hecks/bluebook/ask_resolution.rb +17 -17
  112. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +8 -5
  113. data/lib/hecks/bluebook/assembly/contracts/structure.rb +11 -7
  114. data/lib/hecks/bluebook/assembly/marks.rb +10 -1
  115. data/lib/hecks/bluebook/assembly/scalars.rb +8 -2
  116. data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
  117. data/lib/hecks/bluebook/assembly.rb +10 -10
  118. data/lib/hecks/bluebook/behaviour/aggregate.rb +2 -1
  119. data/lib/hecks/bluebook/behaviour/attribute.rb +12 -0
  120. data/lib/hecks/bluebook/behaviour/command.rb +1 -1
  121. data/lib/hecks/bluebook/behaviour/{chapter.rb → declaration.rb} +17 -17
  122. data/lib/hecks/bluebook/behaviour/domain_port.rb +1 -1
  123. data/lib/hecks/bluebook/behaviour/entity.rb +1 -0
  124. data/lib/hecks/bluebook/behaviour/hecksagon.rb +8 -1
  125. data/lib/hecks/bluebook/behaviour/process_manager.rb +4 -1
  126. data/lib/hecks/bluebook/behaviour/query.rb +5 -0
  127. data/lib/hecks/bluebook/behaviour/read_model.rb +4 -1
  128. data/lib/hecks/bluebook/behaviour/traits.rb +13 -0
  129. data/lib/hecks/bluebook/capabilities/attribute_values.rb +17 -16
  130. data/lib/hecks/bluebook/capabilities/package_declared.rb +65 -0
  131. data/lib/hecks/bluebook/capabilities.rb +32 -132
  132. data/lib/hecks/bluebook/command.rb +22 -7
  133. data/lib/hecks/bluebook/context_map.rb +104 -0
  134. data/lib/hecks/bluebook/{chapter.rb → declaration.rb} +13 -13
  135. data/lib/hecks/bluebook/dsl/adapter_builder.rb +28 -4
  136. data/lib/hecks/bluebook/dsl/aggregate_builder/draining.rb +2 -2
  137. data/lib/hecks/bluebook/dsl/aggregate_builder/one_of_reference.rb +54 -0
  138. data/lib/hecks/bluebook/dsl/aggregate_builder/query_field_sealing.rb +11 -2
  139. data/lib/hecks/bluebook/dsl/aggregate_builder/query_sealing.rb +15 -5
  140. data/lib/hecks/bluebook/dsl/aggregate_builder/relationships.rb +21 -3
  141. data/lib/hecks/bluebook/dsl/aggregate_builder/rules.rb +22 -22
  142. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +2 -2
  143. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +27 -22
  144. data/lib/hecks/bluebook/dsl/bluebook_builder/pending_givens.rb +22 -22
  145. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/computed_sources.rb +27 -21
  146. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/correlation_keys.rb +3 -5
  147. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/event_shapes.rb +5 -4
  148. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/fan_out_rows.rb +3 -3
  149. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/hop_tails.rb +13 -5
  150. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/projected_fields.rb +4 -4
  151. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/provisions.rb +38 -84
  152. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/query_hops.rb +5 -5
  153. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/references.rb +6 -6
  154. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/with_projections.rb +6 -6
  155. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +3 -3
  156. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +67 -67
  157. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
  158. data/lib/hecks/bluebook/dsl/command_builder/folded_sets.rb +48 -0
  159. data/lib/hecks/bluebook/dsl/command_builder.rb +28 -15
  160. data/lib/hecks/bluebook/dsl/context_map_builder.rb +73 -0
  161. data/lib/hecks/bluebook/dsl/entity_builder/rules.rb +17 -17
  162. data/lib/hecks/bluebook/dsl/entity_builder/scope.rb +3 -3
  163. data/lib/hecks/bluebook/dsl/entity_builder/sealing.rb +2 -2
  164. data/lib/hecks/bluebook/dsl/entity_builder.rb +3 -3
  165. data/lib/hecks/bluebook/dsl/hecksagon_builder/{chapter_scope.rb → bluebook_scope.rb} +14 -14
  166. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +33 -27
  167. data/lib/hecks/bluebook/dsl/identity_declaration.rb +6 -2
  168. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +4 -1
  169. data/lib/hecks/bluebook/dsl/query_builder.rb +14 -8
  170. data/lib/hecks/bluebook/dsl/read_model_builder/sealing.rb +6 -11
  171. data/lib/hecks/bluebook/dsl/read_model_builder.rb +1 -2
  172. data/lib/hecks/bluebook/dsl/word_gate.rb +2 -1
  173. data/lib/hecks/bluebook/dsl/world_builder.rb +2 -2
  174. data/lib/hecks/bluebook/dsl.rb +3 -2
  175. data/lib/hecks/bluebook/expression/ast_json/emitters.rb +4 -0
  176. data/lib/hecks/bluebook/expression/ast_json.rb +5 -3
  177. data/lib/hecks/bluebook/expression/ast_reader/blocks.rb +41 -0
  178. data/lib/hecks/bluebook/expression/ast_reader.rb +2 -10
  179. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -4
  180. data/lib/hecks/bluebook/expression/evaluator/parser.rb +4 -1
  181. data/lib/hecks/bluebook/expression/evaluator.rb +13 -9
  182. data/lib/hecks/bluebook/expression/projection.json +6 -0
  183. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +50 -2
  184. data/lib/hecks/bluebook/expression/resolver/lookups.rb +2 -1
  185. data/lib/hecks/bluebook/expression/resolver/operations.rb +1 -0
  186. data/lib/hecks/bluebook/expression/resolver/parsing.rb +21 -15
  187. data/lib/hecks/bluebook/expression/resolver/scanning.rb +4 -1
  188. data/lib/hecks/bluebook/expression.rb +1 -1
  189. data/lib/hecks/bluebook/hecksagon.rb +48 -23
  190. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +4 -1
  191. data/lib/hecks/bluebook/meta_validator/judge/cells.rb +1 -1
  192. data/lib/hecks/bluebook/meta_validator/judge/identity.rb +2 -1
  193. data/lib/hecks/bluebook/meta_validator/modes.rb +27 -27
  194. data/lib/hecks/bluebook/meta_validator/plan.rb +5 -5
  195. data/lib/hecks/bluebook/meta_validator/readings/fields.rb +3 -3
  196. data/lib/hecks/bluebook/meta_validator/reconstruction/heads.rb +11 -3
  197. data/lib/hecks/bluebook/meta_validator/reconstruction/reading.rb +1 -1
  198. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +19 -19
  199. data/lib/hecks/bluebook/meta_validator/shapes.rb +6 -1
  200. data/lib/hecks/bluebook/meta_validator/syntax_boot/disk_cache.rb +18 -15
  201. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +21 -15
  202. data/lib/hecks/bluebook/meta_validator/verdict_cache/prebuilt.rb +4 -1
  203. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +11 -10
  204. data/lib/hecks/bluebook/meta_validator.rb +25 -22
  205. data/lib/hecks/bluebook/model_check/aggregate_checks.rb +68 -0
  206. data/lib/hecks/bluebook/model_check/ask_checks.rb +6 -5
  207. data/lib/hecks/bluebook/model_check/client_profile.rb +8 -42
  208. data/lib/hecks/bluebook/model_check/context_checks.rb +97 -0
  209. data/lib/hecks/bluebook/model_check/context_map_checks.rb +158 -0
  210. data/lib/hecks/bluebook/model_check/lifecycle_checks.rb +27 -5
  211. data/lib/hecks/bluebook/model_check/one_of_checks.rb +106 -0
  212. data/lib/hecks/bluebook/model_check/orphan_checks.rb +129 -0
  213. data/lib/hecks/bluebook/model_check/redundant_relationships.rb +38 -0
  214. data/lib/hecks/bluebook/model_check/reference_checks.rb +11 -1
  215. data/lib/hecks/bluebook/model_check/saga_checks.rb +8 -3
  216. data/lib/hecks/bluebook/model_check/verbs.rb +4 -4
  217. data/lib/hecks/bluebook/model_check.rb +32 -12
  218. data/lib/hecks/bluebook/named_pattern.rb +12 -1
  219. data/lib/hecks/bluebook/one_of_union.rb +35 -0
  220. data/lib/hecks/bluebook/pattern_subset.rb +12 -3
  221. data/lib/hecks/bluebook/project_loader.rb +3 -2
  222. data/lib/hecks/bluebook/project_register.rb +21 -5
  223. data/lib/hecks/bluebook/query.rb +4 -1
  224. data/lib/hecks/bluebook/read_model.rb +5 -5
  225. data/lib/hecks/bluebook/reference.rb +3 -3
  226. data/lib/hecks/bluebook/smoke_test.rb +15 -13
  227. data/lib/hecks/bluebook/synthesizer.rb +48 -13
  228. data/lib/hecks/bluebook/translation_expression/parser.rb +14 -5
  229. data/lib/hecks/bluebook.rb +6 -3
  230. data/lib/hecks/bulk/bluebook/adapters/row_sweeper.adapter +7 -0
  231. data/lib/hecks/bulk/bluebook/bulk.behaviors +18 -0
  232. data/lib/hecks/bulk/bluebook/bulk.bluebook +126 -0
  233. data/lib/hecks/bulk/bluebook/bulk.hecksagon +11 -0
  234. data/lib/hecks/bulk/bluebook/bulk.ports.hecksagon +13 -0
  235. data/lib/hecks/bulk/plan.rb +60 -0
  236. data/lib/hecks/bulk/row_sweeper.rb +34 -0
  237. data/lib/hecks/bulk/set_arguments.rb +68 -0
  238. data/lib/hecks/bulk/set_eligibility.rb +50 -0
  239. data/lib/hecks/bulk/set_planner.rb +100 -0
  240. data/lib/hecks/bulk/set_sweep.rb +78 -0
  241. data/lib/hecks/bulk/sweep.rb +67 -0
  242. data/lib/hecks/cli/check_era.rb +6 -6
  243. data/lib/hecks/cli/command_table.rb +2 -2
  244. data/lib/hecks/cli/console.rb +11 -6
  245. data/lib/hecks/cli/document.rb +14 -9
  246. data/lib/hecks/cli/domain_writer.rb +4 -1
  247. data/lib/hecks/cli/expected_era.rb +139 -0
  248. data/lib/hecks/cli/expression_projection.rb +2 -2
  249. data/lib/hecks/cli/follow.rb +3 -3
  250. data/lib/hecks/cli/history.rb +4 -1
  251. data/lib/hecks/cli/interview_draft.rb +8 -1
  252. data/lib/hecks/cli/interview_record.rb +7 -3
  253. data/lib/hecks/cli/interview_run.rb +7 -5
  254. data/lib/hecks/cli/interview_session/proposals.rb +5 -2
  255. data/lib/hecks/cli/interview_session.rb +3 -2
  256. data/lib/hecks/cli/interview_shape/lifecycle.rb +92 -0
  257. data/lib/hecks/cli/interview_shape.rb +19 -73
  258. data/lib/hecks/cli/model_check/examination.rb +63 -14
  259. data/lib/hecks/cli/model_check/sources.rb +4 -2
  260. data/lib/hecks/cli/model_check.rb +1 -1
  261. data/lib/hecks/cli/persistence_notice.rb +45 -0
  262. data/lib/hecks/cli/present.rb +1 -1
  263. data/lib/hecks/cli/project_cli/launcher_source.rb +12 -10
  264. data/lib/hecks/cli/project_cli/templates.rb +24 -3
  265. data/lib/hecks/cli/project_cli.rb +11 -13
  266. data/lib/hecks/cli/project_diagrams.rb +20 -20
  267. data/lib/hecks/cli/project_glossary.rb +14 -14
  268. data/lib/hecks/cli/reference.rb +1 -1
  269. data/lib/hecks/cli/refresh_projections.rb +16 -11
  270. data/lib/hecks/cli/run.rb +27 -5
  271. data/lib/hecks/cli/seed_semantics_corpus.rb +14 -4
  272. data/lib/hecks/cli/shape.rb +3 -4
  273. data/lib/hecks/cli/smoke_http/transport.rb +3 -2
  274. data/lib/hecks/cli/statements.rb +7 -7
  275. data/lib/hecks/cli/stores.rb +70 -2
  276. data/lib/hecks/cli/stress_concurrency_specs/reporting.rb +4 -1
  277. data/lib/hecks/codemod.rb +3 -3
  278. data/lib/hecks/composer/adapters/claude_composer.rb +3 -2
  279. data/lib/hecks/composer/agent.rb +1 -1
  280. data/lib/hecks/composer/assistant.rb +2 -2
  281. data/lib/hecks/composer/behavior_run.rb +7 -1
  282. data/lib/hecks/composer/bluebook/composer.bluebook +18 -25
  283. data/lib/hecks/composer/bluebook/composer.hecksagon +1 -1
  284. data/lib/hecks/composer/board.rb +6 -6
  285. data/lib/hecks/composer/board_edit/command_text.rb +4 -1
  286. data/lib/hecks/composer/board_edit/source.rb +18 -9
  287. data/lib/hecks/composer/board_edit.rb +2 -2
  288. data/lib/hecks/composer/check.rb +22 -22
  289. data/lib/hecks/composer/domains.rb +1 -1
  290. data/lib/hecks/composer/live.rb +19 -13
  291. data/lib/hecks/composer/outline.rb +4 -1
  292. data/lib/hecks/composer/page.html +1 -1
  293. data/lib/hecks/composer/panels.rb +6 -3
  294. data/lib/hecks/composer/parse.rb +53 -36
  295. data/lib/hecks/composer/session.rb +22 -16
  296. data/lib/hecks/composer/toolbox.rb +9 -6
  297. data/lib/hecks/composer/web.rb +3 -2
  298. data/lib/hecks/composer/workbench.rb +12 -9
  299. data/lib/hecks/composer.rb +1 -1
  300. data/lib/hecks/construct.rb +7 -7
  301. data/lib/hecks/corpus/adapters.rb +2 -1
  302. data/lib/hecks/corpus/routes.rb +25 -5
  303. data/lib/hecks/corpus/rust.rb +59 -24
  304. data/lib/hecks/corpus/sources.rb +12 -11
  305. data/lib/hecks/corpus/worlds.rb +2 -1
  306. data/lib/hecks/corpus.rb +30 -22
  307. data/lib/hecks/deploy/bluebook/deploy.bluebook +304 -518
  308. data/lib/hecks/doc/reference/harvester.rb +4 -1
  309. data/lib/hecks/doc/reference/readme.rb +11 -6
  310. data/lib/hecks/doc/reference/rendering.rb +11 -5
  311. data/lib/hecks/doc/reference.rb +7 -5
  312. data/lib/hecks/embryonaut_bluebook/lock.rb +5 -1
  313. data/lib/hecks/embryonaut_bluebook/manifest.rb +7 -1
  314. data/lib/hecks/embryonaut_bluebook/registry/changelog.rb +4 -1
  315. data/lib/hecks/embryonaut_bluebook/registry/versions.rb +13 -2
  316. data/lib/hecks/embryonaut_bluebook/registry.rb +5 -2
  317. data/lib/hecks/embryonaut_bluebook/shape.rb +1 -1
  318. data/lib/hecks/embryonaut_bluebook/vendor.rb +5 -2
  319. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +15 -1
  320. data/lib/hecks/embryonaut_bluebook.rb +26 -12
  321. data/lib/hecks/engine_agreement.rb +4 -1
  322. data/lib/hecks/forms/app/commands.rb +35 -2
  323. data/lib/hecks/forms/app/records.rb +12 -12
  324. data/lib/hecks/forms/app/responses.rb +4 -2
  325. data/lib/hecks/forms/app.rb +22 -19
  326. data/lib/hecks/forms/command_form_renderer.rb +2 -2
  327. data/lib/hecks/forms/field_controls.rb +6 -1
  328. data/lib/hecks/forms/field_shape.rb +14 -6
  329. data/lib/hecks/forms/html.rb +8 -7
  330. data/lib/hecks/forms/html_pattern.rb +80 -0
  331. data/lib/hecks/forms/index_renderer.rb +11 -11
  332. data/lib/hecks/forms/params.rb +2 -1
  333. data/lib/hecks/forms/port_argument.rb +4 -1
  334. data/lib/hecks/forms/query_form_renderer.rb +1 -1
  335. data/lib/hecks/forms/record_renderer.rb +2 -2
  336. data/lib/hecks/forms/record_table.rb +1 -1
  337. data/lib/hecks/forms/reference_options.rb +2 -1
  338. data/lib/hecks/forms/text_shape.rb +4 -1
  339. data/lib/hecks/forms/value_object_shape.rb +1 -1
  340. data/lib/hecks/forms.rb +7 -7
  341. data/lib/hecks/fqn.rb +8 -2
  342. data/lib/hecks/framework/bluebook/change.bluebook +10 -15
  343. data/lib/hecks/framework/bluebook/change.hecksagon +1 -1
  344. data/lib/hecks/framework/bluebook/compliance.bluebook +27 -126
  345. data/lib/hecks/framework/bluebook/console_settings.bluebook +199 -114
  346. data/lib/hecks/framework/bluebook/framework.hecksagon +7 -0
  347. data/lib/hecks/framework/bluebook/governance.bluebook +36 -14
  348. data/lib/hecks/framework/bluebook/identity.bluebook +1 -1
  349. data/lib/hecks/framework/bluebook/privacy.bluebook +123 -19
  350. data/lib/hecks/framework/bluebook/privacy.key_keeper.adapter +7 -0
  351. data/lib/hecks/framework/bluebook/privacy.ports.hecksagon +19 -0
  352. data/lib/hecks/framework/compliance.oidc.json +6 -36
  353. data/lib/hecks/framework/console_settings.oidc.json +39 -24
  354. data/lib/hecks/framework/privacy.oidc.json +25 -0
  355. data/lib/hecks/framework.rb +24 -8
  356. data/lib/hecks/framework_bluebooks.rb +156 -0
  357. data/lib/hecks/freezer.rb +7 -1
  358. data/lib/hecks/fuzzing/combination_miner.rb +9 -6
  359. data/lib/hecks/fuzzing/concurrent_dispatch/world_copy.rb +3 -1
  360. data/lib/hecks/fuzzing/concurrent_dispatch.rb +2 -2
  361. data/lib/hecks/fuzzing/domain_generator.rb +8 -3
  362. data/lib/hecks/fuzzing/era_boundary.rb +1 -1
  363. data/lib/hecks/fuzzing/failure_triage.rb +13 -6
  364. data/lib/hecks/fuzzing/form_census/{chapter_forms.rb → bluebook_forms.rb} +12 -12
  365. data/lib/hecks/fuzzing/form_census.rb +14 -12
  366. data/lib/hecks/fuzzing/generated_domain_check.rb +2 -1
  367. data/lib/hecks/fuzzing/isolated_boot/postgres_scratch.rb +5 -5
  368. data/lib/hecks/fuzzing/isolated_boot/scratch_schema.rb +19 -9
  369. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +13 -8
  370. data/lib/hecks/fuzzing/isolated_boot.rb +4 -5
  371. data/lib/hecks/fuzzing/mutation/checks.rb +2 -1
  372. data/lib/hecks/fuzzing/mutation/finders.rb +25 -8
  373. data/lib/hecks/fuzzing/mutation/observation.rb +3 -1
  374. data/lib/hecks/fuzzing/mutation/source_map.rb +3 -1
  375. data/lib/hecks/fuzzing/nondeterministic.rb +10 -4
  376. data/lib/hecks/fuzzing/outside_world.rb +1 -1
  377. data/lib/hecks/fuzzing/projection_agreement/binding.rb +63 -0
  378. data/lib/hecks/fuzzing/projection_agreement/comparison.rb +104 -0
  379. data/lib/hecks/fuzzing/projection_agreement.rb +68 -0
  380. data/lib/hecks/fuzzing/properties/aggregation_recompute.rb +1 -1
  381. data/lib/hecks/fuzzing/properties/bulk_parity.rb +118 -0
  382. data/lib/hecks/fuzzing/properties/catalog.rb +7 -0
  383. data/lib/hecks/fuzzing/properties/cursor_walks.rb +56 -0
  384. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +39 -1
  385. data/lib/hecks/fuzzing/properties/group_by_recompute.rb +6 -2
  386. data/lib/hecks/fuzzing/properties/grouped_reduction_recompute.rb +88 -0
  387. data/lib/hecks/fuzzing/properties/guards.rb +4 -1
  388. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +12 -2
  389. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +2 -1
  390. data/lib/hecks/fuzzing/properties/mutation_recompute.rb +3 -2
  391. data/lib/hecks/fuzzing/properties/piece_invariants.rb +3 -2
  392. data/lib/hecks/fuzzing/properties/query_rows.rb +5 -3
  393. data/lib/hecks/fuzzing/properties/querying.rb +6 -3
  394. data/lib/hecks/fuzzing/properties/read_model_names.rb +8 -2
  395. data/lib/hecks/fuzzing/properties/read_models.rb +2 -1
  396. data/lib/hecks/fuzzing/properties/refusals.rb +35 -0
  397. data/lib/hecks/fuzzing/properties/role_checks.rb +3 -2
  398. data/lib/hecks/fuzzing/properties/set_transform_parity.rb +73 -0
  399. data/lib/hecks/fuzzing/properties/tenant_scope.rb +3 -2
  400. data/lib/hecks/fuzzing/properties.rb +6 -0
  401. data/lib/hecks/fuzzing/qa_settings.rb +4 -4
  402. data/lib/hecks/fuzzing/replay/fan_out.rb +4 -3
  403. data/lib/hecks/fuzzing/replay/filters.rb +2 -2
  404. data/lib/hecks/fuzzing/replay/guard_check.rb +2 -2
  405. data/lib/hecks/fuzzing/replay/mutation_trace.rb +23 -10
  406. data/lib/hecks/fuzzing/replay/observation.rb +11 -7
  407. data/lib/hecks/fuzzing/replay/read_only_steps.rb +40 -3
  408. data/lib/hecks/fuzzing/replay/refusal_trace.rb +44 -0
  409. data/lib/hecks/fuzzing/replay/role_check.rb +2 -2
  410. data/lib/hecks/fuzzing/replay/session.rb +38 -18
  411. data/lib/hecks/fuzzing/replay/validation_reports.rb +61 -0
  412. data/lib/hecks/fuzzing/replay.rb +2 -2
  413. data/lib/hecks/fuzzing/rust_gap_manifest.rb +3 -3
  414. data/lib/hecks/fuzzing/self_consistency/saga_redelivery.rb +18 -8
  415. data/lib/hecks/fuzzing/self_consistency/sagas.rb +2 -2
  416. data/lib/hecks/fuzzing/self_consistency/value_objects.rb +9 -2
  417. data/lib/hecks/fuzzing/self_consistency.rb +4 -8
  418. data/lib/hecks/fuzzing/sequence_generator/adversary/argument_mutations.rb +15 -5
  419. data/lib/hecks/fuzzing/sequence_generator/adversary/caller_draw.rb +14 -10
  420. data/lib/hecks/fuzzing/sequence_generator/adversary/precedence_mutations.rb +1 -1
  421. data/lib/hecks/fuzzing/sequence_generator/argument_drawing.rb +2 -1
  422. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +21 -13
  423. data/lib/hecks/fuzzing/sequence_generator/picker.rb +1 -1
  424. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +3 -3
  425. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +2 -2
  426. data/lib/hecks/fuzzing/sequence_generator.rb +1 -2
  427. data/lib/hecks/fuzzing/shrinker.rb +4 -1
  428. data/lib/hecks/fuzzing/value_generator.rb +14 -8
  429. data/lib/hecks/fuzzing.rb +1 -0
  430. data/lib/hecks/grammar/dialects.rb +56 -0
  431. data/lib/hecks/grammar/evolve/arguments.rb +1 -1
  432. data/lib/hecks/grammar/evolve/keywords.rb +1 -1
  433. data/lib/hecks/grammar/evolve/seeds.rb +34 -8
  434. data/lib/hecks/grammar/expression.bluebook +3 -0
  435. data/lib/hecks/grammar/expression_operators.json +1071 -1
  436. data/lib/hecks/grammar/operators.rb +45 -22
  437. data/lib/hecks/grammar/translation.bluebook +2 -0
  438. data/lib/hecks/grammar/translation_ledger.rb +45 -0
  439. data/lib/hecks/grammar/translation_rules.json +646 -0
  440. data/lib/hecks/grammar.rb +27 -23
  441. data/lib/hecks/hecks/adapters/claude_agents.adapter +7 -0
  442. data/lib/hecks/hecks/adapters/claude_agents.rb +119 -0
  443. data/lib/hecks/hecks/adapters/claude_transcript.rb +71 -0
  444. data/lib/hecks/hecks/adapters/codebase/cache_clearing.rb +32 -0
  445. data/lib/hecks/hecks/adapters/codebase/cargo_tests.rb +50 -0
  446. data/lib/hecks/hecks/adapters/codebase/composing.rb +3 -2
  447. data/lib/hecks/hecks/adapters/codebase/conformance.rb +17 -1
  448. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +1 -1
  449. data/lib/hecks/hecks/adapters/codebase/declarations.rb +144 -0
  450. data/lib/hecks/hecks/adapters/codebase/inspection.rb +5 -4
  451. data/lib/hecks/hecks/adapters/codebase/language.rb +1 -0
  452. data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +7 -1
  453. data/lib/hecks/hecks/adapters/codebase/previewing.rb +62 -0
  454. data/lib/hecks/hecks/adapters/codebase/promotion/gate.rb +1 -1
  455. data/lib/hecks/hecks/adapters/codebase/reading.rb +94 -0
  456. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +6 -2
  457. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +1 -1
  458. data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +5 -2
  459. data/lib/hecks/hecks/adapters/codebase/search.rb +91 -0
  460. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +37 -2
  461. data/lib/hecks/hecks/adapters/codebase/static_directory.rb +59 -0
  462. data/lib/hecks/hecks/adapters/codebase/style.rb +2 -2
  463. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +30 -11
  464. data/lib/hecks/hecks/adapters/codebase/test_suite.rb +41 -3
  465. data/lib/hecks/hecks/adapters/console_capture.rb +27 -0
  466. data/lib/hecks/hecks/adapters/deploy_plans.rb +12 -3
  467. data/lib/hecks/hecks/adapters/deploy_toolchain/answers.rb +14 -5
  468. data/lib/hecks/hecks/adapters/deploy_toolchain/scripts.rb +6 -3
  469. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +3 -3
  470. data/lib/hecks/hecks/adapters/desktop_alert/alert.rb +50 -0
  471. data/lib/hecks/hecks/adapters/desktop_alert.adapter +7 -0
  472. data/lib/hecks/hecks/adapters/desktop_alert.rb +99 -0
  473. data/lib/hecks/hecks/adapters/finding_github.rb +4 -1
  474. data/lib/hecks/hecks/adapters/first_admin.rb +19 -12
  475. data/lib/hecks/hecks/adapters/git.rb +5 -5
  476. data/lib/hecks/hecks/adapters/git_checkout.adapter +7 -0
  477. data/lib/hecks/hecks/adapters/git_checkout.rb +99 -0
  478. data/lib/hecks/hecks/adapters/git_landing/merger.rb +85 -0
  479. data/lib/hecks/hecks/adapters/git_landing.adapter +7 -0
  480. data/lib/hecks/hecks/adapters/git_landing.rb +103 -0
  481. data/lib/hecks/hecks/adapters/git_launcher.adapter +7 -0
  482. data/lib/hecks/hecks/adapters/git_launcher.rb +82 -0
  483. data/lib/hecks/hecks/adapters/git_refs.rb +8 -4
  484. data/lib/hecks/hecks/adapters/git_train/bench.rb +76 -0
  485. data/lib/hecks/hecks/adapters/git_train/carriage.rb +116 -0
  486. data/lib/hecks/hecks/adapters/git_train/local_branch.rb +69 -0
  487. data/lib/hecks/hecks/adapters/git_train/reading.rb +26 -0
  488. data/lib/hecks/hecks/adapters/git_train/survey.rb +60 -0
  489. data/lib/hecks/hecks/adapters/git_train.adapter +7 -0
  490. data/lib/hecks/hecks/adapters/git_train.rb +88 -0
  491. data/lib/hecks/hecks/adapters/git_worktrees.rb +81 -0
  492. data/lib/hecks/hecks/adapters/host_http.rb +5 -4
  493. data/lib/hecks/hecks/adapters/in_process_access.rb +3 -2
  494. data/lib/hecks/hecks/adapters/in_process_boot.rb +62 -35
  495. data/lib/hecks/hecks/adapters/in_process_operations.rb +30 -4
  496. data/lib/hecks/hecks/adapters/journal_store/changes.rb +7 -7
  497. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +14 -7
  498. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +9 -7
  499. data/lib/hecks/hecks/adapters/journal_store/examination.rb +3 -3
  500. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +17 -5
  501. data/lib/hecks/hecks/adapters/journal_store/readings/attestation.rb +3 -3
  502. data/lib/hecks/hecks/adapters/journal_store/readings/scaffolding.rb +7 -4
  503. data/lib/hecks/hecks/adapters/journal_store/readings.rb +2 -2
  504. data/lib/hecks/hecks/adapters/journal_store.rb +2 -2
  505. data/lib/hecks/hecks/adapters/ledger/chain.rb +63 -0
  506. data/lib/hecks/hecks/adapters/ledger/journal.rb +105 -0
  507. data/lib/hecks/hecks/adapters/ledger.adapter +7 -0
  508. data/lib/hecks/hecks/adapters/ledger.rb +89 -0
  509. data/lib/hecks/hecks/adapters/local_files.rb +15 -1
  510. data/lib/hecks/hecks/adapters/pg_admin.rb +7 -1
  511. data/lib/hecks/hecks/adapters/rails_app_files.rb +64 -0
  512. data/lib/hecks/hecks/adapters/rehearsal/audit_report.rb +4 -1
  513. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +9 -9
  514. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +2 -2
  515. data/lib/hecks/hecks/adapters/rehearsal/postgres_census.rb +4 -4
  516. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +3 -3
  517. data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +4 -1
  518. data/lib/hecks/hecks/adapters/rehearsal/rust_audit.rb +1 -1
  519. data/lib/hecks/hecks/adapters/rehearsal/subject.rb +34 -5
  520. data/lib/hecks/hecks/adapters/rust_toolchain.rb +5 -3
  521. data/lib/hecks/hecks/adapters/settings_reading.adapter +7 -0
  522. data/lib/hecks/hecks/adapters/settings_reading.rb +142 -0
  523. data/lib/hecks/hecks/adapters/shell.rb +3 -0
  524. data/lib/hecks/hecks/adapters/site_toolchain.rb +8 -8
  525. data/lib/hecks/hecks/adapters/world_clearing.rb +2 -1
  526. data/lib/hecks/hecks/agents.bluebook +246 -0
  527. data/lib/hecks/hecks/codebase.behaviors +13 -1
  528. data/lib/hecks/hecks/codebase.bluebook +554 -586
  529. data/lib/hecks/hecks/composer.behaviors +10 -10
  530. data/lib/hecks/hecks/context_map.hecksagon +7 -6
  531. data/lib/hecks/hecks/custodian.behaviors +96 -1
  532. data/lib/hecks/hecks/custodian.bluebook +553 -157
  533. data/lib/hecks/hecks/deploy.behaviors +1 -1
  534. data/lib/hecks/hecks/deploy.world +3 -4
  535. data/lib/hecks/hecks/hecks.behaviors +2 -2
  536. data/lib/hecks/hecks/hecks.bluebook +9 -14
  537. data/lib/hecks/hecks/hecks.contextmap +25 -0
  538. data/lib/hecks/hecks/hecks.hecksagon +139 -6
  539. data/lib/hecks/hecks/hecks.world +22 -21
  540. data/lib/hecks/hecks/ledger.bluebook +190 -0
  541. data/lib/hecks/hecks/rehearsal.bluebook +43 -77
  542. data/lib/hecks/hecks/shapes.json +17 -0
  543. data/lib/hecks/hecks/site.behaviors +1 -1
  544. data/lib/hecks/hecks/site.world +3 -3
  545. data/lib/hecks/hecks/tooling.bluebook +170 -174
  546. data/lib/hecks/hecks/translations/2-148748.bluebook +2 -0
  547. data/lib/hecks/hecks/translations/2-15a2e4.bluebook +2 -0
  548. data/lib/hecks/hecks/translations/2-1b447c.bluebook +5 -0
  549. data/lib/hecks/hecks/translations/2-240474.bluebook +2 -0
  550. data/lib/hecks/hecks/translations/2-44446d.bluebook +2 -0
  551. data/lib/hecks/hecks/translations/2-46ca26.bluebook +2 -0
  552. data/lib/hecks/hecks/translations/2-473eab.bluebook +2 -0
  553. data/lib/hecks/hecks/translations/2-75cc28.bluebook +2 -0
  554. data/lib/hecks/hecks/translations/2-781c07.bluebook +2 -0
  555. data/lib/hecks/hecks/translations/2-84c18b.bluebook +2 -0
  556. data/lib/hecks/hecks/translations/2-aa5fff.bluebook +2 -0
  557. data/lib/hecks/hecks/translations/2-bfdb48.bluebook +2 -0
  558. data/lib/hecks/hecks/translations/2-cd96d8.bluebook +10 -0
  559. data/lib/hecks/hecks/translations/2-d43f71.bluebook +2 -0
  560. data/lib/hecks/hecks/translations/2-eea577.bluebook +2 -0
  561. data/lib/hecks/hecks/translations/2-f53248.bluebook +2 -0
  562. data/lib/hecks/hecks/translations/2-fbb677.bluebook +2 -0
  563. data/lib/hecks/hecks/translations/2-ffdf12.bluebook +6 -0
  564. data/lib/hecks/ir.rb +8 -1
  565. data/lib/hecks/kernel_capabilities/templates.rb +4 -3
  566. data/lib/hecks/kernel_capabilities.rb +6 -6
  567. data/lib/hecks/language/adapter.bluebook +61 -0
  568. data/lib/hecks/language/bluebook/aggregate.bluebook +36 -6
  569. data/lib/hecks/language/bluebook/attaches/paging.bluebook +1 -1
  570. data/lib/hecks/language/bluebook/bluebook.behaviors +6 -6
  571. data/lib/hecks/language/bluebook/bluebook.bluebook +28 -51
  572. data/lib/hecks/language/bluebook/bluebook.hecksagon +1 -1
  573. data/lib/hecks/language/bluebook/command.bluebook +34 -2
  574. data/lib/hecks/language/bluebook/entity.bluebook +2 -2
  575. data/lib/hecks/language/bluebook/policy.behaviors +2 -2
  576. data/lib/hecks/language/bluebook/policy.bluebook +3 -3
  577. data/lib/hecks/language/bluebook/process_manager.bluebook +3 -3
  578. data/lib/hecks/language/bluebook/projection.bluebook +11 -10
  579. data/lib/hecks/language/bluebook/query.bluebook +1 -0
  580. data/lib/hecks/language/bluebook/shape.bluebook +1 -1
  581. data/lib/hecks/language/bluebook/syntax.bluebook +3 -1
  582. data/lib/hecks/language/bluebook/vocabulary.bluebook +287 -6
  583. data/lib/hecks/language/context_map/context_map.bluebook +90 -0
  584. data/lib/hecks/language/hecksagon/domain_port.bluebook +2 -2
  585. data/lib/hecks/language/hecksagon/hecksagon.bluebook +8 -8
  586. data/lib/hecks/language/oidc.json +10 -0
  587. data/lib/hecks/language/port.bluebook +1 -1
  588. data/lib/hecks/language/sentence/sentence.bluebook +210 -0
  589. data/lib/hecks/language/world/world.bluebook +6 -6
  590. data/lib/hecks/literal.rb +8 -2
  591. data/lib/hecks/naming.rb +44 -14
  592. data/lib/hecks/persistence_legacy_fixture/postgres_stores.rb +7 -6
  593. data/lib/hecks/persistence_legacy_fixture/regenerate.rb +6 -2
  594. data/lib/hecks/ports/access_control.rb +2 -2
  595. data/lib/hecks/ports/agent/answers.rb +2 -2
  596. data/lib/hecks/ports/agent.rb +2 -2
  597. data/lib/hecks/ports/authentication.port +6 -0
  598. data/lib/hecks/ports/authentication.rb +67 -0
  599. data/lib/hecks/ports/authorization.rb +3 -3
  600. data/lib/hecks/ports/dispatch.port +5 -0
  601. data/lib/hecks/ports/dispatch.rb +28 -0
  602. data/lib/hecks/ports/key_vault.rb +9 -6
  603. data/lib/hecks/ports/loading.rb +23 -7
  604. data/lib/hecks/ports/persistence/append_only.rb +18 -1
  605. data/lib/hecks/ports/persistence/binding_policy.rb +70 -11
  606. data/lib/hecks/ports/persistence/codec_boundary.rb +3 -2
  607. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +10 -10
  608. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +4 -5
  609. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +11 -11
  610. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +1 -1
  611. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +6 -6
  612. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -3
  613. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +1 -1
  614. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +32 -19
  615. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +2 -2
  616. data/lib/hecks/ports/persistence/plugins/era.rb +0 -1
  617. data/lib/hecks/ports/persistence/remote_runtime.rb +3 -2
  618. data/lib/hecks/ports/persistence/set_statement.rb +29 -0
  619. data/lib/hecks/ports/persistence/set_transforming.rb +30 -0
  620. data/lib/hecks/ports/persistence/state_codec.rb +3 -3
  621. data/lib/hecks/ports/persistence.rb +0 -4
  622. data/lib/hecks/ports/projection.rb +4 -1
  623. data/lib/hecks/ports/query/cursor.rb +131 -0
  624. data/lib/hecks/ports/query/in_memory.rb +17 -10
  625. data/lib/hecks/ports/query.rb +4 -2
  626. data/lib/hecks/ports.rb +1 -0
  627. data/lib/hecks/projection_files/expression_tables.rb +3 -3
  628. data/lib/hecks/projection_files/sentence_table.rb +17 -0
  629. data/lib/hecks/projection_files.rb +16 -13
  630. data/lib/hecks/projections/bootstrap_table.rb +3 -3
  631. data/lib/hecks/projections/command_schema/shape.rb +95 -0
  632. data/lib/hecks/projections/command_schema.rb +74 -0
  633. data/lib/hecks/projections/context_map_diagram.rb +135 -0
  634. data/lib/hecks/projections/context_relationships.rb +125 -0
  635. data/lib/hecks/projections/deploy/box/access.rb +7 -4
  636. data/lib/hecks/projections/deploy/box/caddy.rb +2 -1
  637. data/lib/hecks/projections/deploy/box/compose.rb +4 -8
  638. data/lib/hecks/projections/deploy/box/edge.rb +2 -1
  639. data/lib/hecks/projections/deploy/box/hosting.rb +4 -2
  640. data/lib/hecks/projections/deploy/box/hosting_parts.rb +27 -12
  641. data/lib/hecks/projections/deploy/box/logging.rb +2 -3
  642. data/lib/hecks/projections/deploy/box/roll_recipe.rb +1 -3
  643. data/lib/hecks/projections/deploy/box/roll_scripts.rb +1 -16
  644. data/lib/hecks/projections/deploy/box/settings/containers.rb +29 -15
  645. data/lib/hecks/projections/deploy/box/settings/patterns.rb +5 -6
  646. data/lib/hecks/projections/deploy/box/settings/types.rb +5 -10
  647. data/lib/hecks/projections/deploy/box/settings.rb +9 -8
  648. data/lib/hecks/projections/deploy/box/stack_yaml.rb +10 -14
  649. data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +0 -1
  650. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +16 -3
  651. data/lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl +7 -6
  652. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +2 -6
  653. data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +2 -6
  654. data/lib/hecks/projections/deploy/box.rb +16 -9
  655. data/lib/hecks/projections/deploy/fargate/assembly.rb +7 -2
  656. data/lib/hecks/projections/deploy/fargate/cdn/options.rb +4 -1
  657. data/lib/hecks/projections/deploy/fargate/check.rb +3 -1
  658. data/lib/hecks/projections/deploy/fargate/containers/rendering.rb +8 -5
  659. data/lib/hecks/projections/deploy/fargate/containers/routing.rb +2 -1
  660. data/lib/hecks/projections/deploy/fargate/monitoring/reading.rb +4 -1
  661. data/lib/hecks/projections/deploy/fargate/sections.rb +7 -4
  662. data/lib/hecks/projections/deploy/fargate/settings/ids.rb +4 -1
  663. data/lib/hecks/projections/deploy/fargate/settings/tuning.rb +13 -4
  664. data/lib/hecks/projections/deploy/fargate/stack/blocks.rb +6 -1
  665. data/lib/hecks/projections/deploy/fargate/stack/readers.rb +2 -1
  666. data/lib/hecks/projections/deploy/fargate/stack.rb +11 -6
  667. data/lib/hecks/projections/deploy/fargate/yaml.rb +10 -3
  668. data/lib/hecks/projections/deploy/fargate.rb +8 -3
  669. data/lib/hecks/projections/deploy/lambda/stack/blocks.rb +4 -2
  670. data/lib/hecks/projections/deploy/lambda/stack/deploy_chain.rb +2 -1
  671. data/lib/hecks/projections/deploy/lambda/stack/reading.rb +12 -9
  672. data/lib/hecks/projections/deploy/lambda/stack/reading_web.rb +7 -2
  673. data/lib/hecks/projections/deploy/lambda/stack/web.rb +2 -1
  674. data/lib/hecks/projections/deploy/lambda.rb +8 -3
  675. data/lib/hecks/projections/deploy/preview/containers.rb +14 -1
  676. data/lib/hecks/projections/deploy/preview/preview.sh.erb +2 -2
  677. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +8 -6
  678. data/lib/hecks/projections/deploy/preview/settings.rb +23 -5
  679. data/lib/hecks/projections/deploy/preview/task_yaml.rb +1 -1
  680. data/lib/hecks/projections/deploy/preview/yaml_text.rb +2 -1
  681. data/lib/hecks/projections/deploy/scripts/settings.rb +5 -2
  682. data/lib/hecks/projections/deploy/shared.rb +12 -1
  683. data/lib/hecks/projections/deploy/shared_database_instance.rb +1 -1
  684. data/lib/hecks/projections/deploy/smoke.rb +3 -1
  685. data/lib/hecks/projections/deploy/template_diff/normalizer.rb +4 -1
  686. data/lib/hecks/projections/deploy/templates/lambda/mint_era_shared.tmpl +2 -2
  687. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  688. data/lib/hecks/projections/deploy/vercel.rb +1 -1
  689. data/lib/hecks/projections/diagrams/dispatch.rb +5 -3
  690. data/lib/hecks/projections/diagrams/statecharts.rb +8 -3
  691. data/lib/hecks/projections/diagrams/structure.rb +9 -6
  692. data/lib/hecks/projections/diagrams.rb +9 -7
  693. data/lib/hecks/projections/glossary/entries.rb +4 -4
  694. data/lib/hecks/projections/glossary/html/page.rb +4 -1
  695. data/lib/hecks/projections/glossary/html/parser.rb +13 -4
  696. data/lib/hecks/projections/glossary/html/sections.rb +4 -1
  697. data/lib/hecks/projections/glossary/html.rb +10 -3
  698. data/lib/hecks/projections/glossary/markdown.rb +4 -3
  699. data/lib/hecks/projections/glossary/mermaid.rb +8 -5
  700. data/lib/hecks/projections/glossary/sections.rb +1 -1
  701. data/lib/hecks/projections/glossary/sensitivity.rb +11 -4
  702. data/lib/hecks/projections/glossary/sentences/actions.rb +5 -2
  703. data/lib/hecks/projections/glossary/sentences/queries.rb +2 -1
  704. data/lib/hecks/projections/glossary/sentences/value_objects.rb +15 -6
  705. data/lib/hecks/projections/glossary/sentences.rb +6 -3
  706. data/lib/hecks/projections/glossary/slugs.rb +8 -1
  707. data/lib/hecks/projections/glossary.rb +3 -3
  708. data/lib/hecks/projections/model.rb +10 -6
  709. data/lib/hecks/projections/oidc.rb +19 -7
  710. data/lib/hecks/projections/parser_table.rb +10 -10
  711. data/lib/hecks/projections/phrase_table.rb +105 -0
  712. data/lib/hecks/projections/plain_english.rb +14 -4
  713. data/lib/hecks/projections/rails_app.rb +77 -0
  714. data/lib/hecks/projections/reference.rb +2 -2
  715. data/lib/hecks/projections/rust_vocabulary/arguments.rb +1 -1
  716. data/lib/hecks/projections/rust_vocabulary/tables.rb +20 -0
  717. data/lib/hecks/projections/rust_vocabulary.rb +14 -17
  718. data/lib/hecks/projections/site/admin.rb +7 -7
  719. data/lib/hecks/projections/site/admin_cms.rb +1 -1
  720. data/lib/hecks/projections/site/cms_editor/blocks.rb +5 -2
  721. data/lib/hecks/projections/site/cms_editor/checks.rb +9 -6
  722. data/lib/hecks/projections/site/cms_editor/color.rb +4 -1
  723. data/lib/hecks/projections/site/cms_editor/composition.rb +20 -18
  724. data/lib/hecks/projections/site/cms_editor/destructive.rb +4 -1
  725. data/lib/hecks/projections/site/cms_editor/drafts.rb +6 -2
  726. data/lib/hecks/projections/site/cms_editor/key_kinds.rb +4 -1
  727. data/lib/hecks/projections/site/cms_editor/limits.rb +1 -1
  728. data/lib/hecks/projections/site/cms_editor/media.rb +1 -1
  729. data/lib/hecks/projections/site/cms_editor/moment.rb +3 -1
  730. data/lib/hecks/projections/site/cms_editor/pickers.rb +1 -1
  731. data/lib/hecks/projections/site/cms_editor/preview.rb +10 -4
  732. data/lib/hecks/projections/site/cms_editor/relations.rb +3 -2
  733. data/lib/hecks/projections/site/cms_editor/scale_checks.rb +32 -25
  734. data/lib/hecks/projections/site/cms_editor/scheduling.rb +1 -1
  735. data/lib/hecks/projections/site/cms_editor/schema.rb +31 -29
  736. data/lib/hecks/projections/site/cms_editor/setting.rb +34 -27
  737. data/lib/hecks/projections/site/cms_editor/targets.rb +9 -6
  738. data/lib/hecks/projections/site/cms_editor/templates/src/app.ts.tmpl +3 -3
  739. data/lib/hecks/projections/site/cms_editor/templates/src/commands.ts.tmpl +2 -2
  740. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +8 -3
  741. data/lib/hecks/projections/site/cms_editor/templates/src/media/handler.ts.tmpl +4 -4
  742. data/lib/hecks/projections/site/cms_editor/templates/src/names.ts.tmpl +14 -14
  743. data/lib/hecks/projections/site/cms_editor/templates/src/schema.ts.tmpl +12 -12
  744. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pages.ts.tmpl +12 -12
  745. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pickers.ts.tmpl +1 -1
  746. data/lib/hecks/projections/site/cms_editor/templates/src/ui/schedule.ts.tmpl +2 -2
  747. data/lib/hecks/projections/site/cms_editor/templates/src/ui/shell.ts.tmpl +11 -11
  748. data/lib/hecks/projections/site/cms_editor/templates/src/ui/world.ts.tmpl +3 -3
  749. data/lib/hecks/projections/site/cms_editor/theme_checks.rb +8 -2
  750. data/lib/hecks/projections/site/cms_editor.rb +19 -16
  751. data/lib/hecks/projections/site/edge/checks.rb +1 -1
  752. data/lib/hecks/projections/site/edge/function_checks.rb +1 -1
  753. data/lib/hecks/projections/site/edge/function_code.rb +4 -1
  754. data/lib/hecks/projections/site/edge/pattern.rb +4 -1
  755. data/lib/hecks/projections/site/edge/route_checks.rb +2 -2
  756. data/lib/hecks/projections/site/edge/rules.rb +1 -1
  757. data/lib/hecks/projections/site/edge.rb +14 -8
  758. data/lib/hecks/projections/site/live_edge.rb +2 -2
  759. data/lib/hecks/projections/site/payload/fields/definitions.rb +1 -1
  760. data/lib/hecks/projections/site/payload/lifecycle/text.rb +1 -1
  761. data/lib/hecks/projections/site/payload/model.rb +16 -12
  762. data/lib/hecks/projections/site/payload/specs.rb +4 -2
  763. data/lib/hecks/projections/site/payload_driver.rb +9 -8
  764. data/lib/hecks/projections/site/probe.rb +12 -3
  765. data/lib/hecks/projections/site/regions.rb +9 -1
  766. data/lib/hecks/projections/site/role_probe.rb +7 -7
  767. data/lib/hecks/projections/site/root_rows.rb +4 -4
  768. data/lib/hecks/projections/site/routes_ts/listings.rb +13 -6
  769. data/lib/hecks/projections/site/routes_ts.rb +6 -3
  770. data/lib/hecks/projections/site/site_admin.rb +1 -1
  771. data/lib/hecks/projections/site/site_cdn.rb +3 -2
  772. data/lib/hecks/projections/site/site_host.rb +1 -1
  773. data/lib/hecks/projections/site/site_root.rb +4 -4
  774. data/lib/hecks/projections/site/table/checks.rb +4 -1
  775. data/lib/hecks/projections/site/table/navigation_checks.rb +2 -2
  776. data/lib/hecks/projections/site/table/row_builder.rb +7 -7
  777. data/lib/hecks/projections/site/table/row_defaults.rb +1 -1
  778. data/lib/hecks/projections/site/table.rb +32 -26
  779. data/lib/hecks/projections/statements/actions.rb +4 -1
  780. data/lib/hecks/projections/statements/relationships.rb +15 -2
  781. data/lib/hecks/projections/statements.rb +2 -2
  782. data/lib/hecks/projections/vocabulary.rb +6 -6
  783. data/lib/hecks/projections/world_skeleton.rb +68 -0
  784. data/lib/hecks/projections.rb +5 -0
  785. data/lib/hecks/projector/cli_audience.rb +12 -9
  786. data/lib/hecks/projector/cli_projector/claims.rb +8 -8
  787. data/lib/hecks/projector/cli_projector/command_specs.rb +9 -5
  788. data/lib/hecks/projector/cli_projector/listing.rb +20 -8
  789. data/lib/hecks/projector/cli_projector/option_specs.rb +1 -1
  790. data/lib/hecks/projector/cli_projector/query_specs.rb +1 -1
  791. data/lib/hecks/projector/cli_projector/usage.rb +12 -8
  792. data/lib/hecks/projector/cli_projector.rb +2 -2
  793. data/lib/hecks/projector/docs_projector/behaviour.rb +5 -4
  794. data/lib/hecks/projector/docs_projector/closing.rb +3 -3
  795. data/lib/hecks/projector/docs_projector.rb +12 -7
  796. data/lib/hecks/projector/exporter/adapters.rb +15 -2
  797. data/lib/hecks/projector/exporter/bindings.rb +39 -134
  798. data/lib/hecks/projector/exporter/plugins.rb +55 -0
  799. data/lib/hecks/projector/exporter.rb +2 -0
  800. data/lib/hecks/projector/narrate_projector/commands.rb +4 -3
  801. data/lib/hecks/projector/narrate_projector/reactions.rb +2 -2
  802. data/lib/hecks/projector/narrate_projector.rb +9 -8
  803. data/lib/hecks/projector/target.rb +8 -8
  804. data/lib/hecks/projector.rb +9 -9
  805. data/lib/hecks/quality_control/adapters/agent.rb +2 -1
  806. data/lib/hecks/quality_control/adapters/github_checks.adapter +1 -1
  807. data/lib/hecks/quality_control/adapters/github_issues.adapter +1 -1
  808. data/lib/hecks/quality_control/adapters/github_issues.rb +6 -2
  809. data/lib/hecks/quality_control/adapters/target_tools.rb +9 -3
  810. data/lib/hecks/quality_control/cli/child.rb +1 -0
  811. data/lib/hecks/quality_control/cli/qa_domain_novelty/verdict.rb +1 -1
  812. data/lib/hecks/quality_control/cli/qa_generated_domains/evaluation.rb +12 -2
  813. data/lib/hecks/quality_control/cli/qa_generated_domains/promotion.rb +4 -1
  814. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +4 -1
  815. data/lib/hecks/quality_control/cli/qa_log_bug/proof.rb +16 -3
  816. data/lib/hecks/quality_control/cli/qa_mine_combinations/booting.rb +4 -2
  817. data/lib/hecks/quality_control/cli/qa_mutate.rb +161 -0
  818. data/lib/hecks/quality_control/cli/qa_sweep/all_mode/report.rb +4 -1
  819. data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +2 -1
  820. data/lib/hecks/quality_control/cli/qa_sweep.rb +4 -1
  821. data/lib/hecks/quality_control/cli/qa_tick/output.rb +28 -26
  822. data/lib/hecks/quality_control/cli/qa_tick/preflight.rb +12 -3
  823. data/lib/hecks/quality_control/cli/qa_tick.rb +24 -8
  824. data/lib/hecks/quality_control/quality_control.bluebook +93 -92
  825. data/lib/hecks/quality_control/quality_control.hecksagon +3 -3
  826. data/lib/hecks/quality_control/quality_control.ports.hecksagon +3 -2
  827. data/lib/hecks/query_ir/rules.rb +9 -9
  828. data/lib/hecks/query_ir/touchpoints.rb +5 -2
  829. data/lib/hecks/query_ir.rb +3 -3
  830. data/lib/hecks/query_specification/common/comparison.rb +1 -1
  831. data/lib/hecks/query_specification/common/dsl.rb +3 -4
  832. data/lib/hecks/query_specification/hop_path.rb +7 -4
  833. data/lib/hecks/rails_app/bluebook/rails_app.bluebook +60 -0
  834. data/lib/hecks/rails_app/bluebook/rails_app.hecksagon +4 -0
  835. data/lib/hecks/refused.rb +104 -0
  836. data/lib/hecks/release/approval.rb +9 -2
  837. data/lib/hecks/release/gem_pin.rb +7 -3
  838. data/lib/hecks/release/lane.rb +1 -1
  839. data/lib/hecks/release/runner/clean_tree.rb +4 -1
  840. data/lib/hecks/release/runner/console.rb +2 -1
  841. data/lib/hecks/router/aggregate_reader.rb +3 -4
  842. data/lib/hecks/router/namespace_installer.rb +31 -17
  843. data/lib/hecks/router.rb +2 -10
  844. data/lib/hecks/runtime/adapter_lookup.rb +30 -7
  845. data/lib/hecks/runtime/adapter_namespace.rb +21 -0
  846. data/lib/hecks/runtime/asking.rb +30 -0
  847. data/lib/hecks/runtime/caller.rb +22 -0
  848. data/lib/hecks/runtime/capability_graph.rb +2 -1
  849. data/lib/hecks/runtime/collecting.rb +86 -0
  850. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +13 -8
  851. data/lib/hecks/runtime/command_interpreter/delegation.rb +46 -17
  852. data/lib/hecks/runtime/command_interpreter/hydration.rb +17 -21
  853. data/lib/hecks/runtime/command_interpreter/list_mutations.rb +2 -1
  854. data/lib/hecks/runtime/command_interpreter/persisting.rb +13 -12
  855. data/lib/hecks/runtime/command_interpreter.rb +17 -10
  856. data/lib/hecks/runtime/command_rules/admissibility.rb +17 -10
  857. data/lib/hecks/runtime/command_rules/authorization.rb +8 -6
  858. data/lib/hecks/runtime/command_rules/guard_state.rb +10 -10
  859. data/lib/hecks/runtime/command_rules/invariants.rb +3 -1
  860. data/lib/hecks/runtime/command_rules/missing_reference.rb +25 -0
  861. data/lib/hecks/runtime/command_rules/one_of_references.rb +44 -0
  862. data/lib/hecks/runtime/command_rules/references.rb +14 -14
  863. data/lib/hecks/runtime/command_rules.rb +2 -0
  864. data/lib/hecks/runtime/declarations.rb +66 -0
  865. data/lib/hecks/runtime/dependency_planning/analyzer.rb +2 -1
  866. data/lib/hecks/runtime/dependency_planning/expression_reads.rb +3 -2
  867. data/lib/hecks/runtime/dependency_planning/mutation_analysis.rb +2 -1
  868. data/lib/hecks/runtime/dispatch_claim.rb +72 -0
  869. data/lib/hecks/runtime/dispatch_context.rb +41 -0
  870. data/lib/hecks/runtime/dispatcher/queries.rb +17 -0
  871. data/lib/hecks/runtime/dispatcher/routing.rb +34 -12
  872. data/lib/hecks/runtime/dispatcher/validating.rb +73 -0
  873. data/lib/hecks/runtime/dispatcher.rb +39 -23
  874. data/lib/hecks/runtime/entity_element/location.rb +15 -7
  875. data/lib/hecks/runtime/entity_element/mutations.rb +4 -1
  876. data/lib/hecks/runtime/entity_interpreter/enforcement.rb +1 -1
  877. data/lib/hecks/runtime/entity_interpreter/locating.rb +1 -1
  878. data/lib/hecks/runtime/entity_interpreter/saving.rb +5 -5
  879. data/lib/hecks/runtime/entity_interpreter.rb +11 -4
  880. data/lib/hecks/runtime/errors.rb +30 -17
  881. data/lib/hecks/runtime/identity.rb +9 -0
  882. data/lib/hecks/runtime/instance.rb +3 -2
  883. data/lib/hecks/runtime/interpreting.rb +63 -6
  884. data/lib/hecks/runtime/invocation_builder.rb +7 -4
  885. data/lib/hecks/runtime/invocation_facts.rb +13 -8
  886. data/lib/hecks/runtime/loader/boot_steps.rb +3 -2
  887. data/lib/hecks/runtime/loader/described.rb +4 -0
  888. data/lib/hecks/runtime/loader.rb +52 -62
  889. data/lib/hecks/runtime/outbox/redrive.rb +105 -0
  890. data/lib/hecks/runtime/outbox/relay.rb +11 -39
  891. data/lib/hecks/runtime/outbox/runnable.rb +60 -0
  892. data/lib/hecks/runtime/outbox.rb +15 -1
  893. data/lib/hecks/runtime/policy_interpreter/arguments.rb +7 -1
  894. data/lib/hecks/runtime/policy_interpreter/fan_out.rb +8 -35
  895. data/lib/hecks/runtime/policy_interpreter.rb +8 -0
  896. data/lib/hecks/runtime/port_operation_interpreter.rb +27 -13
  897. data/lib/hecks/runtime/query_interpreter/outside_answer.rb +1 -1
  898. data/lib/hecks/runtime/query_interpreter/paging.rb +42 -0
  899. data/lib/hecks/runtime/query_interpreter/plugin_answer.rb +54 -0
  900. data/lib/hecks/runtime/query_interpreter/store_specification.rb +32 -0
  901. data/lib/hecks/runtime/query_interpreter.rb +16 -9
  902. data/lib/hecks/runtime/reaction_invocation/target_resolution.rb +10 -4
  903. data/lib/hecks/runtime/reaction_invocation.rb +5 -2
  904. data/lib/hecks/runtime/read_model_interpreter/head_ordering.rb +1 -1
  905. data/lib/hecks/runtime/read_model_interpreter/reductions.rb +23 -15
  906. data/lib/hecks/runtime/read_model_interpreter/shaping.rb +18 -7
  907. data/lib/hecks/runtime/read_model_interpreter.rb +5 -2
  908. data/lib/hecks/runtime/reference_hop.rb +3 -2
  909. data/lib/hecks/runtime/refusal_wording.rb +15 -6
  910. data/lib/hecks/runtime/registry/adapter_capabilities.rb +43 -0
  911. data/lib/hecks/runtime/registry/ask_verification.rb +15 -4
  912. data/lib/hecks/runtime/registry/{chapter_verification.rb → bluebook_verification.rb} +23 -23
  913. data/lib/hecks/runtime/registry/context_maps.rb +30 -0
  914. data/lib/hecks/runtime/registry/declarations.rb +8 -6
  915. data/lib/hecks/runtime/registry/durability_warnings.rb +18 -2
  916. data/lib/hecks/runtime/registry/hecksagon_rules.rb +27 -24
  917. data/lib/hecks/runtime/registry/plugin_query_verification.rb +45 -0
  918. data/lib/hecks/runtime/registry/providers.rb +34 -99
  919. data/lib/hecks/runtime/registry/query_verification.rb +30 -21
  920. data/lib/hecks/runtime/registry/runtime_state.rb +4 -3
  921. data/lib/hecks/runtime/registry/saga_persistence.rb +7 -16
  922. data/lib/hecks/runtime/registry/verification.rb +9 -5
  923. data/lib/hecks/runtime/registry/world_defaults.rb +8 -5
  924. data/lib/hecks/runtime/registry.rb +52 -27
  925. data/lib/hecks/runtime/row_sweep.rb +104 -0
  926. data/lib/hecks/runtime/saga_interpreter/compensation.rb +5 -4
  927. data/lib/hecks/runtime/saga_interpreter/delivery.rb +13 -10
  928. data/lib/hecks/runtime/saga_interpreter/redrive.rb +141 -0
  929. data/lib/hecks/runtime/saga_interpreter/transitions.rb +35 -6
  930. data/lib/hecks/runtime/saga_interpreter.rb +36 -4
  931. data/lib/hecks/runtime/saga_pending_dispatch.rb +4 -2
  932. data/lib/hecks/runtime/scalar_field.rb +73 -0
  933. data/lib/hecks/runtime/tenant_scope.rb +2 -1
  934. data/lib/hecks/runtime/validation.rb +24 -0
  935. data/lib/hecks/runtime/value/admission.rb +11 -11
  936. data/lib/hecks/runtime/value/coercion.rb +7 -7
  937. data/lib/hecks/runtime/value/field_checks.rb +13 -13
  938. data/lib/hecks/runtime/value/invariant_violation.rb +4 -1
  939. data/lib/hecks/runtime/value/references.rb +7 -4
  940. data/lib/hecks/runtime/value/unwrapping.rb +60 -0
  941. data/lib/hecks/runtime/value/validation.rb +7 -4
  942. data/lib/hecks/runtime/value.rb +3 -27
  943. data/lib/hecks/runtime/wasm_plugin.rb +98 -0
  944. data/lib/hecks/runtime.rb +14 -14
  945. data/lib/hecks/rust_build/command_schemas.rb +27 -0
  946. data/lib/hecks/rust_build/conformance/surface.rb +50 -7
  947. data/lib/hecks/rust_build/conformance.rb +25 -3
  948. data/lib/hecks/rust_build/conformance_fuzz.rb +2 -2
  949. data/lib/hecks/rust_build/domain_name.rb +4 -1
  950. data/lib/hecks/rust_build/host.rb +15 -9
  951. data/lib/hecks/rust_build/native_build.rb +2 -1
  952. data/lib/hecks/rust_build/prebuilt_binary.rb +25 -5
  953. data/lib/hecks/rust_build/project_rust/codegen_run.rb +11 -10
  954. data/lib/hecks/rust_build/project_rust/domain_loader.rb +15 -2
  955. data/lib/hecks/rust_build/project_rust/root_mod/notes.rb +1 -1
  956. data/lib/hecks/rust_build/project_rust/target_ir.rb +12 -3
  957. data/lib/hecks/rust_build/project_rust.rb +39 -19
  958. data/lib/hecks/rust_build/target_dir.rb +40 -0
  959. data/lib/hecks/rust_build/wasm.rb +46 -17
  960. data/lib/hecks/rust_build/wasm_browser.rb +8 -2
  961. data/lib/hecks/rust_build.rb +29 -2
  962. data/lib/hecks/sentence_form.rb +212 -0
  963. data/lib/hecks/served_options.rb +2 -1
  964. data/lib/hecks/shape_ledger.rb +149 -0
  965. data/lib/hecks/site/bluebook/site.bluebook +39 -62
  966. data/lib/hecks/site/bluebook/site.ports.hecksagon +1 -1
  967. data/lib/hecks/sme/bluebook/sme.bluebook +7 -1
  968. data/lib/hecks/storehouse/audit.rb +4 -1
  969. data/lib/hecks/storehouse/bluebook_words.rb +35 -0
  970. data/lib/hecks/storehouse/dispatching.rb +6 -3
  971. data/lib/hecks/storehouse/introspection.rb +4 -4
  972. data/lib/hecks/storehouse/querying.rb +2 -2
  973. data/lib/hecks/storehouse.rb +14 -4
  974. data/lib/hecks/switches.rb +21 -0
  975. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +5 -9
  976. data/lib/hecks/text.rb +57 -0
  977. data/lib/hecks/tickets/bluebook/tickets.bluebook +30 -51
  978. data/lib/hecks/tickets/bluebook/tickets.hecksagon +1 -1
  979. data/lib/hecks/tickets/bluebook/tickets.ports.hecksagon +1 -1
  980. data/lib/hecks/tools/argument_gate_matrix/dispatching.rb +1 -1
  981. data/lib/hecks/tools/argument_gate_matrix/rows.rb +7 -7
  982. data/lib/hecks/tools/argument_gate_matrix/violations.rb +1 -1
  983. data/lib/hecks/tools/argument_gate_matrix.rb +3 -3
  984. data/lib/hecks/tools/ci_gate_decision.rb +5 -2
  985. data/lib/hecks/tools/ci_gates.rb +1 -1
  986. data/lib/hecks/tools/comment_style/caps_words.rb +11 -8
  987. data/lib/hecks/tools/comment_style/dictionary.rb +7 -2
  988. data/lib/hecks/tools/comment_style/fixing.rb +10 -5
  989. data/lib/hecks/tools/comment_style/long_blocks.rb +1 -1
  990. data/lib/hecks/tools/comment_style/masking.rb +5 -5
  991. data/lib/hecks/tools/comment_style/run.rb +4 -1
  992. data/lib/hecks/tools/comment_style/source_file.rb +10 -3
  993. data/lib/hecks/tools/comment_style/structure_checks.rb +13 -4
  994. data/lib/hecks/tools/comment_style/text_checks.rb +1 -1
  995. data/lib/hecks/tools/comment_style.rb +25 -2
  996. data/lib/hecks/tools/deploy_recipe/generation.rb +1 -1
  997. data/lib/hecks/tools/deploy_recipe/targets.rb +1 -1
  998. data/lib/hecks/tools/deploy_recipe/worlds.rb +15 -9
  999. data/lib/hecks/tools/deploy_recipe.rb +17 -10
  1000. data/lib/hecks/tools/deploy_recipe_lint/fixtures.rb +1 -1
  1001. data/lib/hecks/tools/deploy_recipe_lint/makefile.rb +7 -1
  1002. data/lib/hecks/tools/drop_implicit_append_fields.rb +4 -1
  1003. data/lib/hecks/tools/era_projection_backfill.rb +11 -6
  1004. data/lib/hecks/tools/era_reattest.rb +11 -6
  1005. data/lib/hecks/tools/fuzz_sweep/findings.rb +18 -2
  1006. data/lib/hecks/tools/fuzz_sweep/options.rb +5 -1
  1007. data/lib/hecks/tools/fuzz_sweep.rb +17 -5
  1008. data/lib/hecks/tools/gate.rb +27 -5
  1009. data/lib/hecks/tools/hoist_local_givens/rewriting.rb +3 -2
  1010. data/lib/hecks/tools/hoist_local_givens.rb +2 -4
  1011. data/lib/hecks/tools/lanes/prebuilt_workflow.rb +1 -1
  1012. data/lib/hecks/tools/lanes/rows.rb +9 -3
  1013. data/lib/hecks/tools/lanes/ruleset.rb +1 -1
  1014. data/lib/hecks/tools/lanes/watch_workflow.rb +1 -1
  1015. data/lib/hecks/tools/lanes/workflow.rb +1 -1
  1016. data/lib/hecks/tools/lanes.rb +1 -1
  1017. data/lib/hecks/tools/oidc_manifests.rb +16 -19
  1018. data/lib/hecks/tools/regeneration_run.rb +22 -17
  1019. data/lib/hecks/tools/release_only.rb +5 -2
  1020. data/lib/hecks/tools/rust_comment_style/caps_words.rb +5 -5
  1021. data/lib/hecks/tools/rust_comment_style/fixing.rb +1 -1
  1022. data/lib/hecks/tools/rust_comment_style/masking.rb +7 -4
  1023. data/lib/hecks/tools/rust_comment_style/source_file.rb +7 -2
  1024. data/lib/hecks/tools/rust_comment_style/text_checks.rb +1 -1
  1025. data/lib/hecks/tools/rust_comment_style/tokenizer.rb +4 -1
  1026. data/lib/hecks/tools/site_routes/editor_files.rb +16 -13
  1027. data/lib/hecks/tools/site_routes/root_files.rb +14 -13
  1028. data/lib/hecks/tools/site_routes/templates.rb +5 -4
  1029. data/lib/hecks/tools/site_routes.rb +18 -15
  1030. data/lib/hecks/tools/tail_merge.rb +3 -3
  1031. data/lib/hecks/tools/tenant_provisioning.rb +6 -6
  1032. data/lib/hecks/tools/tools_doc.rb +16 -16
  1033. data/lib/hecks/tools/translation_audit/domain_loading.rb +3 -3
  1034. data/lib/hecks/tools/translation_audit.rb +15 -4
  1035. data/lib/hecks/tools/translation_scaffold/reporting.rb +4 -1
  1036. data/lib/hecks/tools/translation_scaffold.rb +4 -14
  1037. data/lib/hecks/vendoring/git_source.rb +7 -4
  1038. data/lib/hecks/version.rb +1 -1
  1039. data/lib/hecks/vocabulary.rb +110 -3
  1040. data/lib/hecks.rb +37 -18
  1041. data/lib/rubocop/cop/hecks/inline_regexp.rb +29 -0
  1042. data/lib/rubocop/cop/hecks/no_transport_in_runtime.rb +78 -0
  1043. data/lib/rubocop/cop/hecks/rails_model_rules.rb +46 -0
  1044. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +1 -1
  1045. data/lib/rubocop/cop/hecks/train_wreck.rb +69 -0
  1046. data/lib/rubocop/cop/hecks/unnamed_pipeline.rb +51 -0
  1047. data/prebuilt/31cf81e12f16236ad26249be590cfea3799c9db67432e6a4ef54b801c355b6ae.marshal +0 -0
  1048. data/prebuilt/68ecb2bd3abb1a221007db4f2b0375834c13aaccac8f2b99778e8517f11aa113.marshal +0 -0
  1049. data/prebuilt/verdicts-0328c2403383837d780ad5ed81d1da351c56436b894cf9015b5d56b8f1f1bf10.json +1 -0
  1050. data/qa/settings.yml +9 -0
  1051. data/rust/Cargo.lock +8 -1
  1052. data/rust/Cargo.toml +6 -7
  1053. data/rust/build/src/binding_pass.rs +13 -17
  1054. data/rust/build/src/cargo_sync.rs +1 -1
  1055. data/rust/build/src/lineage_pass.rs +4 -71
  1056. data/rust/build/src/pipeline.rs +99 -70
  1057. data/rust/build/src/resolve.rs +85 -29
  1058. data/rust/build/src/seams_pass.rs +117 -99
  1059. data/rust/build/src/world_pass.rs +21 -12
  1060. data/rust/codegen/src/domain_generator.rs +30 -2
  1061. data/rust/codegen/src/expr_emitter.rs +6 -0
  1062. data/rust/codegen/src/json_codec.rs +48 -10
  1063. data/rust/codegen/src/main.rs +41 -34
  1064. data/rust/codegen/src/manifest.rs +1 -1
  1065. data/rust/codegen/src/naming.rs +5 -5
  1066. data/rust/codegen/src/one_of_references.rs +124 -0
  1067. data/rust/codegen/src/presenters.rs +335 -0
  1068. data/rust/codegen/src/queries.rs +56 -7
  1069. data/rust/codegen/src/reactions.rs +19 -14
  1070. data/rust/codegen/src/read_models.rs +20 -71
  1071. data/rust/codegen/src/registry.rs +89 -15
  1072. data/rust/codegen/src/sidecars.rs +1 -1
  1073. data/rust/expr/Cargo.lock +54 -0
  1074. data/rust/expr/Cargo.toml +52 -0
  1075. data/rust/{src/kernel → expr/src}/attribute_shapes/composite.rs +45 -14
  1076. data/rust/{src/kernel → expr/src}/attribute_shapes/list.rs +1 -1
  1077. data/rust/{src/kernel → expr/src}/attribute_shapes/mod.rs +2 -2
  1078. data/rust/{src/kernel → expr/src}/attribute_shapes/optional.rs +1 -1
  1079. data/rust/{src/kernel → expr/src}/attribute_shapes/scalar.rs +3 -3
  1080. data/rust/{src/kernel → expr/src}/expr.rs +30 -8
  1081. data/rust/{src/kernel → expr/src}/expression_operators/arithmetic.rs +41 -18
  1082. data/rust/{src/kernel → expr/src}/expression_operators/comparison.rs +54 -8
  1083. data/rust/{src/kernel → expr/src}/expression_operators/enumeration.rs +73 -6
  1084. data/rust/{src/kernel → expr/src}/expression_operators/logical.rs +2 -2
  1085. data/rust/expr/src/expression_operators/membership.rs +33 -0
  1086. data/rust/{src/kernel → expr/src}/expression_operators/mod.rs +2 -2
  1087. data/rust/expr/src/expression_operators/pattern_match.rs +179 -0
  1088. data/rust/{src/kernel → expr/src}/expression_operators/positional.rs +8 -6
  1089. data/rust/{src/kernel → expr/src}/expression_operators/presence.rs +7 -6
  1090. data/rust/{src/kernel → expr/src}/expression_operators/sign_test.rs +14 -4
  1091. data/rust/{src/kernel → expr/src}/expression_operators/sized.rs +16 -9
  1092. data/rust/{src/kernel → expr/src}/expression_operators/text.rs +30 -17
  1093. data/rust/{src/kernel → expr/src}/expression_operators/to_string.rs +8 -6
  1094. data/rust/expr/src/lib.rs +17 -0
  1095. data/rust/expr/src/refusal.rs +62 -0
  1096. data/rust/expr/src/rendering.rs +135 -0
  1097. data/rust/{src/kernel → expr/src}/ruby_float.rs +7 -6
  1098. data/rust/host/Cargo.lock +137 -123
  1099. data/rust/host/Cargo.toml +7 -3
  1100. data/rust/host/HECKS_RELEASE +1 -1
  1101. data/rust/host/core/Cargo.toml +64 -0
  1102. data/rust/host/core/clippy.toml +4 -0
  1103. data/rust/host/core/src/arg_limits.rs +48 -0
  1104. data/rust/host/core/src/clock.rs +146 -0
  1105. data/rust/host/core/src/dispatch.rs +920 -0
  1106. data/rust/host/core/src/expr_json.rs +1137 -0
  1107. data/rust/host/{src → core/src}/host.rs +13 -14
  1108. data/rust/host/{src → core/src}/ir.rs +227 -18
  1109. data/rust/host/core/src/journal.rs +126 -0
  1110. data/rust/host/{src → core/src}/lambda_client.rs +27 -96
  1111. data/rust/host/core/src/lib.rs +37 -0
  1112. data/rust/host/{src → core/src}/needs.rs +66 -20
  1113. data/rust/host/core/src/plugins.rs +269 -0
  1114. data/rust/host/core/src/presentation.rs +471 -0
  1115. data/rust/host/{src → core/src}/presentation_write.rs +75 -43
  1116. data/rust/host/{src → core/src}/query_step.rs +54 -16
  1117. data/rust/host/{src → core/src}/reference_validate.rs +6 -6
  1118. data/rust/host/{src → core/src}/storage_shape.rs +2 -2
  1119. data/rust/host/core/src/store/memory.rs +635 -0
  1120. data/rust/host/core/src/store.rs +418 -0
  1121. data/rust/host/{src → core/src}/ui_schema.rs +8 -8
  1122. data/rust/host/core/src/validation.rs +129 -0
  1123. data/rust/host/core/src/wasm_runner.rs +410 -0
  1124. data/rust/host/fuzz/.gitignore +4 -0
  1125. data/rust/host/fuzz/Cargo.lock +4479 -0
  1126. data/rust/host/fuzz/Cargo.toml +42 -0
  1127. data/rust/host/fuzz/README.md +55 -0
  1128. data/rust/host/fuzz/fuzz_targets/postgres_equivalence.rs +24 -0
  1129. data/rust/host/fuzz/src/backend.rs +273 -0
  1130. data/rust/host/fuzz/src/equivalence.rs +195 -0
  1131. data/rust/host/fuzz/src/footprint.rs +182 -0
  1132. data/rust/host/fuzz/src/lib.rs +17 -0
  1133. data/rust/host/fuzz/src/ops.rs +345 -0
  1134. data/rust/host/fuzz/tests/smoke.rs +50 -0
  1135. data/rust/host/src/api.rs +316 -105
  1136. data/rust/host/src/auth/google_signin.rs +183 -0
  1137. data/rust/host/src/auth.rs +527 -370
  1138. data/rust/host/src/aws_lambda_invoker.rs +61 -0
  1139. data/rust/host/src/bin/expr_harness.rs +9 -4
  1140. data/rust/host/src/bin/ledger.rs +84 -0
  1141. data/rust/host/src/bin/lineage_harness.rs +17 -21
  1142. data/rust/host/src/bin/mint_harness.rs +11 -36
  1143. data/rust/host/src/bin/validate_harness.rs +1 -6
  1144. data/rust/host/src/boot.rs +465 -174
  1145. data/rust/host/src/boundary_fuzz/ir.rs +10 -5
  1146. data/rust/host/src/boundary_fuzz/server.rs +6 -5
  1147. data/rust/host/src/checkout.rs +4 -1
  1148. data/rust/host/src/commerce_ir.rs +126 -105
  1149. data/rust/host/src/{dispatch.rs → dispatch_tests.rs} +391 -824
  1150. data/rust/host/src/extension.rs +3 -4
  1151. data/rust/host/src/hecksagon_boundary_tests.rs +149 -0
  1152. data/rust/host/src/ledger.rs +683 -0
  1153. data/rust/host/src/lib.rs +21 -28
  1154. data/rust/host/src/payments/tests.rs +136 -38
  1155. data/rust/host/src/payments.rs +36 -32
  1156. data/rust/host/src/platform_signin/extension.rs +1 -1
  1157. data/rust/host/src/platform_signin/mapping_tests.rs +12 -9
  1158. data/rust/host/src/platform_signin/session.rs +7 -14
  1159. data/rust/host/src/platform_signin/verify.rs +0 -2
  1160. data/rust/host/src/presentation_tests.rs +382 -0
  1161. data/rust/host/src/rate_limit.rs +18 -2
  1162. data/rust/host/src/server.rs +287 -36
  1163. data/rust/host/src/{journal.rs → store/journal_sql.rs} +21 -127
  1164. data/rust/host/src/{mint.rs → store/mint.rs} +12 -12
  1165. data/rust/host/src/store/postgres.rs +493 -0
  1166. data/rust/host/src/store.rs +97 -0
  1167. data/rust/host/src/store_conformance.rs +1265 -0
  1168. data/rust/host/src/web/commerce.rs +10 -18
  1169. data/rust/host/src/web/newsletter/cooldown.rs +107 -22
  1170. data/rust/host/src/web/newsletter/naming.rs +104 -0
  1171. data/rust/host/src/web/newsletter/tests.rs +53 -0
  1172. data/rust/host/src/web/newsletter.rs +73 -50
  1173. data/rust/host/src/web/newsletter_send.rs +12 -13
  1174. data/rust/host/src/web/registration_receipt.rs +2 -2
  1175. data/rust/host/src/web/registrations.rs +754 -375
  1176. data/rust/host/src/web/resend_webhook.rs +3 -3
  1177. data/rust/host/src/web/signin_tests.rs +131 -0
  1178. data/rust/host/src/web.rs +1479 -149
  1179. data/rust/lsp/README.md +27 -27
  1180. data/rust/lsp/src/diagnostics.rs +75 -15
  1181. data/rust/lsp/src/main.rs +101 -5
  1182. data/rust/parser/fuzz/Cargo.toml +2 -2
  1183. data/rust/parser/fuzz/README.md +3 -3
  1184. data/rust/parser/fuzz/fuzz_targets/{chapter.rs → bluebook.rs} +4 -4
  1185. data/rust/parser/fuzz/fuzz_targets/resolve.rs +2 -2
  1186. data/rust/parser/src/build/named_pattern.rs +22 -0
  1187. data/rust/parser/src/build/references.rs +52 -0
  1188. data/rust/parser/src/diag.rs +22 -1
  1189. data/rust/parser/src/emit.rs +68 -4
  1190. data/rust/parser/src/expr/ast_json.rs +10 -0
  1191. data/rust/parser/src/expr/resolver.rs +49 -9
  1192. data/rust/parser/src/ir.rs +34 -7
  1193. data/rust/parser/src/keywords.rs +454 -457
  1194. data/rust/parser/src/lex.rs +115 -87
  1195. data/rust/parser/src/main.rs +180 -32
  1196. data/rust/parser/src/parse/adapter.rs +28 -0
  1197. data/rust/parser/src/parse/aggregate.rs +55 -35
  1198. data/rust/parser/src/parse/command.rs +100 -15
  1199. data/rust/parser/src/parse/{chapter.rs → declaration.rs} +85 -54
  1200. data/rust/parser/src/parse/domain_port.rs +60 -9
  1201. data/rust/parser/src/parse/entity.rs +35 -32
  1202. data/rust/parser/src/parse/file.rs +1 -1
  1203. data/rust/parser/src/parse/hecksagon.rs +15 -5
  1204. data/rust/parser/src/parse/mod.rs +84 -2
  1205. data/rust/parser/src/parse/process_manager.rs +1 -1
  1206. data/rust/parser/src/parse/query.rs +13 -3
  1207. data/rust/parser/src/parse/read_model.rs +2 -1
  1208. data/rust/parser/src/phrases.rs +1258 -0
  1209. data/rust/parser/src/render_ruby.rs +540 -0
  1210. data/rust/parser/src/sentence/changes.rs +437 -0
  1211. data/rust/parser/src/sentence/command_step.rs +206 -0
  1212. data/rust/parser/src/sentence/complete.rs +276 -0
  1213. data/rust/parser/src/sentence/entity.rs +41 -0
  1214. data/rust/parser/src/sentence/examples.rs +1012 -0
  1215. data/rust/parser/src/sentence/format.rs +882 -0
  1216. data/rust/parser/src/sentence/lower.rs +1366 -0
  1217. data/rust/parser/src/sentence/mod.rs +444 -0
  1218. data/rust/parser/src/sentence/names.rs +69 -0
  1219. data/rust/parser/src/sentence/options.rs +154 -0
  1220. data/rust/parser/src/sentence/pattern.rs +433 -0
  1221. data/rust/parser/src/sentence/process.rs +196 -0
  1222. data/rust/parser/src/sentence/queries.rs +431 -0
  1223. data/rust/parser/src/sentence/report.rs +283 -0
  1224. data/rust/parser/src/sentence/token.rs +106 -0
  1225. data/rust/parser/src/sentence/values.rs +498 -0
  1226. data/rust/src/exemplar/queries.rs +1 -0
  1227. data/rust/src/exemplar/reactions.rs +1 -1
  1228. data/rust/src/exemplar/registry.rs +30 -0
  1229. data/rust/src/kernel/cli.rs +233 -20
  1230. data/rust/src/kernel/collecting.rs +174 -0
  1231. data/rust/src/kernel/dispatch.rs +55 -27
  1232. data/rust/src/kernel/json.rs +15 -1
  1233. data/rust/src/kernel/mod.rs +8 -67
  1234. data/rust/src/kernel/named_query.rs +225 -11
  1235. data/rust/src/kernel/orchestrate.rs +213 -25
  1236. data/rust/src/kernel/orchestrate_guard_tests.rs +3 -0
  1237. data/rust/src/kernel/present.rs +261 -0
  1238. data/rust/src/kernel/query_cursor.rs +251 -0
  1239. data/rust/src/kernel/query_ordering.rs +6 -6
  1240. data/rust/src/kernel/read_model.rs +162 -17
  1241. data/rust/src/kernel/repository.rs +107 -17
  1242. data/rust/src/kernel/vocab/mod.rs +4 -0
  1243. data/rust/src/kernel/vocab/reducer.rs +109 -0
  1244. data/rust/src/kernel/vocab/refusal_class.rs +124 -0
  1245. data/rust/src/kernel/vocab/refusal_template.rs +2 -2
  1246. data/rust/src/lib.rs +1 -1
  1247. data/rust/web/Cargo.lock +47 -0
  1248. metadata +331 -93
  1249. data/lib/hecks/chapters.rb +0 -147
  1250. data/lib/hecks/gate/stages.yml +0 -234
  1251. data/lib/hecks/hecks/governance.world +0 -7
  1252. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +0 -137
  1253. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +0 -59
  1254. data/lib/hecks/ports/persistence/postgres_dump.rb +0 -100
  1255. data/lib/hecks/projections/deploy/box/templates/tunnel-egress.yaml.tmpl +0 -11
  1256. data/lib/hecks/projector/exporter/capability_values.rb +0 -55
  1257. data/lib/hecks/runtime/remote_dispatcher.rb +0 -176
  1258. data/prebuilt/02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal +0 -0
  1259. data/prebuilt/bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal +0 -0
  1260. data/prebuilt/verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json +0 -1
  1261. data/rust/host/src/clock.rs +0 -8
  1262. data/rust/host/src/expr_json.rs +0 -1983
  1263. data/rust/host/src/presentation.rs +0 -842
  1264. data/rust/host/src/wasm_runner.rs +0 -206
  1265. data/rust/src/kernel/expression_operators/membership.rs +0 -20
  1266. data/rust/src/kernel/expression_operators/pattern_match.rs +0 -108
  1267. /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/events.rb +0 -0
  1268. /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/head_writes.rb +0 -0
  1269. /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/journal.rb +0 -0
  1270. /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/base_sql.rb +0 -0
  1271. /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/field_cache.rb +0 -0
  1272. /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/head_compiler.rb +0 -0
  1273. /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/merge_writes.rb +0 -0
  1274. /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/provisioning.rb +0 -0
  1275. /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/resumable_backfill.rb +0 -0
  1276. /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/transform_installer.rb +0 -0
  1277. /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/transform_sql.rb +0 -0
  1278. /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/sagas.rb +0 -0
  1279. /data/rust/host/{src → core/src}/field_hints.rs +0 -0
  1280. /data/rust/host/{src → core/src}/log.rs +0 -0
  1281. /data/rust/host/{src → core/src}/reference_transform.rs +0 -0
@@ -5,17 +5,24 @@ use crate::b64;
5
5
  use crate::crypto::{constant_time_eq, hex_encode};
6
6
  use crate::dispatch;
7
7
  use crate::host::Host;
8
- use crate::journal;
9
- use crate::journal::LineageConfig;
10
8
  use crate::lambda_client::LambdaInvoker;
9
+ use crate::needs::{Clock, ProcessClock};
10
+ use crate::store::journal_sql as journal;
11
+ use crate::store::journal_sql::LineageConfig;
12
+ use crate::store::{Store, StoreError};
11
13
  use serde_json::{json, Value};
12
14
  use std::path::Path;
13
15
  use std::time::{SystemTime, UNIX_EPOCH};
14
- use tokio::sync::Mutex;
15
- use tokio_postgres::Client;
16
+
17
+ mod google_signin;
18
+
19
+ pub use google_signin::GoogleSignin;
20
+ pub use google_signin::{
21
+ clear_header as google_signin_clear_header, read as read_google_signin,
22
+ set_header as google_signin_set_header,
23
+ };
16
24
 
17
25
  const ISSUER: &str = "https://accounts.google.com";
18
- const STATE_TTL_SECS: u64 = 600;
19
26
 
20
27
  /// A verified Google ID token's claims.
21
28
  pub struct Claims {
@@ -33,32 +40,33 @@ pub struct Session {
33
40
  pub role: Option<String>,
34
41
  }
35
42
 
36
- pub fn authorization_url(redirect_uri: &str, secret: &str) -> Result<String, String> {
43
+ /// The caller a signed-in session dispatches as: the role it carries and the identity
44
+ /// Governance holds assignments for. No session is an unidentified caller.
45
+ pub fn caller(session: Option<&Session>) -> crate::dispatch::Caller<'_> {
46
+ session.map_or_else(crate::dispatch::Caller::default, |session| {
47
+ crate::dispatch::Caller {
48
+ role: session.role.as_deref(),
49
+ actor_id: Some(session.identity_id.as_str()).filter(|id| !id.is_empty()),
50
+ }
51
+ })
52
+ }
53
+
54
+ /// Google's consent page for one sign-in: `signin`'s `state` comes back on the callback and its
55
+ /// `nonce` inside the ID token, and both must match the cookie `/auth/google` set beside them.
56
+ pub fn authorization_url(redirect_uri: &str, signin: &GoogleSignin) -> Result<String, String> {
37
57
  let client_id = crate::secrets::var("GOOGLE_CLIENT_ID")
38
58
  .map_err(|_| "GOOGLE_CLIENT_ID not set".to_string())?;
39
- let payload = now_secs().to_string();
40
- let state = format!("{payload}.{}", sign(secret, &payload));
41
59
  Ok(format!(
42
- "https://accounts.google.com/o/oauth2/v2/auth?client_id={}&redirect_uri={}&response_type=code&scope={}&state={}",
60
+ "https://accounts.google.com/o/oauth2/v2/auth?client_id={}&redirect_uri={}&response_type=code&scope={}&state={}&nonce={}",
43
61
  urlencode(&client_id),
44
62
  urlencode(redirect_uri),
45
63
  urlencode("openid email profile"),
46
- urlencode(&state),
64
+ urlencode(&signin.state),
65
+ urlencode(&signin.nonce),
47
66
  ))
48
67
  }
49
68
 
50
- // No server-side stash to compare against — recomputing the HMAC here
51
- // is the whole check: a forged, mismatched, or expired state fails on it.
52
- pub fn verify_state(state: &str, secret: &str) -> Result<(), String> {
53
- let payload = verify_sig(secret, state).ok_or_else(|| "state mismatch".to_string())?;
54
- let minted: u64 = payload.parse().map_err(|_| "state mismatch".to_string())?;
55
- if now_secs().saturating_sub(minted) > STATE_TTL_SECS {
56
- return Err("state expired".to_string());
57
- }
58
- Ok(())
59
- }
60
-
61
- pub async fn verify(code: &str, redirect_uri: &str) -> Result<Claims, String> {
69
+ pub async fn verify(code: &str, redirect_uri: &str, nonce: &str) -> Result<Claims, String> {
62
70
  let client_id = crate::secrets::var("GOOGLE_CLIENT_ID")
63
71
  .map_err(|_| "GOOGLE_CLIENT_ID not set".to_string())?;
64
72
  let client_secret = crate::secrets::var("GOOGLE_CLIENT_SECRET")
@@ -91,13 +99,13 @@ pub async fn verify(code: &str, redirect_uri: &str) -> Result<Claims, String> {
91
99
  .and_then(|v| v.as_str())
92
100
  .ok_or_else(|| "no id_token in the response".to_string())?;
93
101
 
94
- verify_id_token(id_token, &client_id).await
102
+ verify_id_token(id_token, &client_id, nonce).await
95
103
  }
96
104
 
97
105
  // Fetched fresh, never cached: this is a real sign-in, not a hot path,
98
106
  // and missing Google's own key rotation would be worse than one extra
99
107
  // HTTPS round trip per login.
100
- async fn verify_id_token(id_token: &str, client_id: &str) -> Result<Claims, String> {
108
+ async fn verify_id_token(id_token: &str, client_id: &str, nonce: &str) -> Result<Claims, String> {
101
109
  let header =
102
110
  jsonwebtoken::decode_header(id_token).map_err(|e| format!("bad id_token header: {e}"))?;
103
111
  let kid = header
@@ -140,6 +148,7 @@ async fn verify_id_token(id_token: &str, client_id: &str) -> Result<Claims, Stri
140
148
  let token = jsonwebtoken::decode::<Value>(id_token, &decoding_key, &validation)
141
149
  .map_err(|err| format!("id_token signature/claims invalid: {err}"))?;
142
150
  let claims = token.claims;
151
+ check_nonce(&claims, nonce)?;
143
152
 
144
153
  Ok(Claims {
145
154
  issuer: claims
@@ -161,6 +170,17 @@ async fn verify_id_token(id_token: &str, client_id: &str) -> Result<Claims, Stri
161
170
  })
162
171
  }
163
172
 
173
+ // The ID token must carry the nonce this browser's sign-in cookie holds, so a token minted for
174
+ // someone else's sign-in cannot finish this one.
175
+ fn check_nonce(claims: &Value, expected: &str) -> Result<(), String> {
176
+ let carried = claims.get("nonce").and_then(Value::as_str).unwrap_or("");
177
+ if constant_time_eq(carried.as_bytes(), expected.as_bytes()) {
178
+ Ok(())
179
+ } else {
180
+ Err("id_token nonce does not match this sign-in".to_string())
181
+ }
182
+ }
183
+
164
184
  /// The cookie name the account token travels in when a deploy names none.
165
185
  /// A deploy naming its own uses `HECKS_SESSION_COOKIE` instead.
166
186
  pub const DEFAULT_ACCOUNT_COOKIE: &str = "hecks_session";
@@ -234,6 +254,39 @@ fn purpose_key(secret: &str, purpose: &str) -> String {
234
254
  format!("{purpose}:{secret}")
235
255
  }
236
256
 
257
+ const HANDOFF_PURPOSE: &str = "google-handoff";
258
+ const HANDOFF_TTL_SECS: u64 = 60;
259
+
260
+ /// A one-minute, single-use stand-in for `email`'s account token. It rides the redirect onto a
261
+ /// site on another origin, where a URL is logged and kept in history, and the site trades it for
262
+ /// the account token server-to-server (`redeem_handoff_token`).
263
+ pub fn handoff_token(secret: &str, email: &str) -> String {
264
+ let jti = uuid::Uuid::new_v4().simple().to_string();
265
+ purpose_token(
266
+ secret,
267
+ HANDOFF_PURPOSE,
268
+ json!({ "email": email, "jti": jti }),
269
+ HANDOFF_TTL_SECS,
270
+ )
271
+ }
272
+
273
+ /// The account token a handoff token stands for, the first time it is redeemed; `None` when it is
274
+ /// forged, expired, minted for another purpose, or already spent in `spent`.
275
+ pub fn redeem_handoff_token(
276
+ secret: &str,
277
+ token: &str,
278
+ spent: &crate::platform_signin::jti::JtiStore,
279
+ ) -> Option<String> {
280
+ let claims = verify_purpose_token(secret, HANDOFF_PURPOSE, token)?;
281
+ let email = claims.get("email")?.as_str()?;
282
+ let jti = claims.get("jti")?.as_str()?;
283
+ let expires_at = i64::try_from(claims.get("exp")?.as_u64()?).ok()?;
284
+ let now = i64::try_from(now_secs()).ok()?;
285
+ spent
286
+ .claim(jti, expires_at + 1, now)
287
+ .then(|| account_token(secret, email, SESSION_TTL_SECS))
288
+ }
289
+
237
290
  /// How long a session cookie, and the account token minted beside it, stays valid.
238
291
  pub const SESSION_TTL_SECS: u64 = 60 * 60 * 24 * 14;
239
292
 
@@ -302,7 +355,7 @@ fn identity_id_from_state(state: &Value) -> Option<String> {
302
355
  }
303
356
 
304
357
  /// Resolves an external identity the way the Google callback does: the PostgresEra head first,
305
- /// then the flat journal's `instances`. A domain whose Identity chapter keeps no head of its own
358
+ /// then the flat journal's `instances`. A domain whose Identity bluebook keeps no head of its own
306
359
  /// (its heads cover its own aggregates and the membership aggregate) holds the link only there.
307
360
  ///
308
361
  /// # Errors
@@ -311,18 +364,18 @@ fn identity_id_from_state(state: &Value) -> Option<String> {
311
364
  /// head that cannot be read is logged and skipped, as the Google callback does, so the flat journal
312
365
  /// still gets its turn.
313
366
  pub async fn resolve_identity_anywhere(
314
- client: &Mutex<Client>,
367
+ store: &Store,
315
368
  wasm_path: &Path,
316
369
  domain_ir: &Value,
317
370
  issuer: &str,
318
371
  subject: &str,
319
372
  ) -> anyhow::Result<Option<String>> {
320
- match resolve_identity_from_head(client, domain_ir, issuer, subject).await {
373
+ match resolve_identity_from_head(store, domain_ir, issuer, subject).await {
321
374
  Ok(Some(found)) => return Ok(Some(found)),
322
375
  Ok(None) => {}
323
376
  Err(error) => eprintln!("resolve_identity: identity head: {error:#}"),
324
377
  }
325
- let read = match dispatch::read(client, wasm_path).await {
378
+ let read = match dispatch::read(store, wasm_path).await {
326
379
  Ok(read) => read,
327
380
  Err(error) if flat_journal_is_absent(&error) => return Ok(None),
328
381
  Err(error) => return Err(error),
@@ -338,14 +391,14 @@ pub async fn resolve_identity_anywhere(
338
391
  fn flat_journal_is_absent(error: &anyhow::Error) -> bool {
339
392
  error
340
393
  .chain()
341
- .filter_map(|cause| cause.downcast_ref::<tokio_postgres::Error>())
342
- .any(|db| db.code() == Some(&tokio_postgres::error::SqlState::UNDEFINED_TABLE))
394
+ .filter_map(|cause| cause.downcast_ref::<StoreError>())
395
+ .any(StoreError::is_not_provisioned)
343
396
  }
344
397
 
345
398
  /// Identity lives in the PostgresEra head, not `dispatch::read`'s
346
399
  /// `instances` — tries both the domain-qualified and the bare relation name.
347
400
  pub async fn resolve_identity_from_head(
348
- client: &Mutex<Client>,
401
+ store: &Store,
349
402
  domain_ir: &Value,
350
403
  issuer: &str,
351
404
  subject: &str,
@@ -354,36 +407,21 @@ pub async fn resolve_identity_from_head(
354
407
  return Ok(None);
355
408
  };
356
409
  let domain = domain_ir.get("name").and_then(|v| v.as_str()).unwrap_or("");
357
- let mut names = vec![
358
- journal::head_view(domain, "external_identifier"),
359
- journal::head_view("Identity", "external_identifier"),
360
- "external_identifier_head".to_string(),
361
- ];
362
- names.dedup();
363
- let guard = client.lock().await;
364
- for name in names {
365
- let sql = format!(
366
- "SELECT state FROM {} WHERE state->'issuer'->>'value' = $1 AND state->'subject'->>'value' = $2",
367
- journal::quote_ident(&name)
368
- );
369
- match guard.query_opt(&sql, &[&issuer, &subject]).await {
370
- Ok(Some(row)) => {
371
- let state: Value = row.get(0);
372
- return Ok(identity_id_from_state(&state));
373
- }
374
- Ok(None) => return Ok(None),
375
- Err(e) if e.code() == Some(&tokio_postgres::error::SqlState::UNDEFINED_TABLE) => {}
376
- Err(e) => return Err(e.into()),
377
- }
378
- }
379
- Ok(None)
410
+ let found = store
411
+ .head_find(
412
+ &[domain, "Identity"],
413
+ "external_identifier",
414
+ &[("issuer", issuer), ("subject", subject)],
415
+ )
416
+ .await?;
417
+ Ok(found.and_then(|state| identity_id_from_state(&state)))
380
418
  }
381
419
 
382
420
  // Member lives in the PostgresEra head, not `dispatch::read`'s journal
383
421
  // `instances` — reading `instances` for an already-linked member misses
384
422
  // it entirely, so this queries the head view directly instead.
385
423
  async fn member_row_by_email(
386
- client: &Mutex<Client>,
424
+ store: &Store,
387
425
  domain_ir: &Value,
388
426
  email: &str,
389
427
  ) -> anyhow::Result<Option<Value>> {
@@ -391,27 +429,22 @@ async fn member_row_by_email(
391
429
  // docs/decisions/0059: `head_view` needs the domain name too, the
392
430
  // same one `ir.rs`/`web.rs` already extract this way.
393
431
  let domain = domain_ir.get("name").and_then(|v| v.as_str()).unwrap_or("");
394
- let guard = client.lock().await;
395
- journal::read_lineage_head_by_id(&*guard, domain, &storage_name, email).await
432
+ Ok(store.head_by_id(domain, &storage_name, email).await?)
396
433
  }
397
434
 
398
- async fn member_rows(
399
- client: &Mutex<Client>,
400
- domain_ir: &Value,
401
- ) -> anyhow::Result<Vec<(String, Value)>> {
435
+ async fn member_rows(store: &Store, domain_ir: &Value) -> anyhow::Result<Vec<(String, Value)>> {
402
436
  let (_, storage_name) = membership_aggregate(domain_ir)?;
403
437
  let domain = domain_ir.get("name").and_then(|v| v.as_str()).unwrap_or("");
404
- let guard = client.lock().await;
405
- journal::read_lineage_head_all(&*guard, domain, &storage_name).await
438
+ Ok(store.head_all(domain, &storage_name).await?)
406
439
  }
407
440
 
408
441
  // The membership aggregate comes from `ir.json`'s declared `membership`
409
442
  // key, never an env var or a lookup against lineage_capable_aggregates —
410
- // the chapter declaration is the fact, same as Governance's authorization.
443
+ // the bluebook declaration is the fact, same as Governance's authorization.
411
444
  fn membership_aggregate(domain_ir: &Value) -> anyhow::Result<(String, String)> {
412
445
  let provider = crate::ir::membership_provider(domain_ir).ok_or_else(|| {
413
446
  anyhow::anyhow!(
414
- "this domain attaches no chapter that provides \"membership\" — cannot resolve who may sign in"
447
+ "this domain attaches no bluebook that provides \"membership\" — cannot resolve who may sign in"
415
448
  )
416
449
  })?;
417
450
  Ok(membership_names(&provider.aggregate))
@@ -423,7 +456,7 @@ fn membership_names(aggregate: &str) -> (String, String) {
423
456
  .next()
424
457
  .unwrap_or(aggregate)
425
458
  .to_string();
426
- let storage_name = crate::journal::snake(&bare);
459
+ let storage_name = crate::store::journal_sql::snake(&bare);
427
460
  (bare, storage_name)
428
461
  }
429
462
 
@@ -431,60 +464,40 @@ fn membership_names(aggregate: &str) -> (String, String) {
431
464
  // + snapshot-upsert transaction every Member write uses. The advisory
432
465
  // lock guards a concurrent domain rename, not ordinal uniqueness.
433
466
  async fn append_member_state(
434
- client: &Mutex<Client>,
467
+ store: &Store,
435
468
  config: &LineageConfig,
436
469
  domain_ir: &Value,
437
470
  id: &str,
438
471
  state: &Value,
439
472
  ) -> anyhow::Result<()> {
440
473
  let (aggregate_name, _) = membership_aggregate(domain_ir)?;
441
- let mut guard = client.lock().await;
442
- let txn = guard.transaction().await?;
443
-
444
- txn.execute(
445
- "SELECT pg_advisory_xact_lock(hashtext('hecks_ordinal:' || $1))",
446
- &[&config.domain],
447
- )
448
- .await?;
449
-
450
- journal::append_lineage_mutation(
451
- &txn,
452
- config,
453
- &journal::Mutation {
454
- aggregate: &aggregate_name,
455
- id,
456
- operation: "save",
457
- state,
458
- },
459
- )
460
- .await?;
461
-
462
- txn.commit().await?;
474
+ store
475
+ .put_head(
476
+ config,
477
+ &journal::Mutation {
478
+ aggregate: &aggregate_name,
479
+ id,
480
+ operation: "save",
481
+ state,
482
+ },
483
+ )
484
+ .await?;
463
485
  Ok(())
464
486
  }
465
487
 
466
488
  pub async fn session_for_member_by_identity(
467
- client: &Mutex<Client>,
489
+ store: &Store,
468
490
  domain_ir: &Value,
469
491
  identity_id: &str,
470
492
  ) -> anyhow::Result<Option<Session>> {
471
493
  let (_, storage_name) = membership_aggregate(domain_ir)?;
472
494
  // docs/decisions/0059 — same domain-qualification as above.
473
495
  let domain = domain_ir.get("name").and_then(|v| v.as_str()).unwrap_or("");
474
- let guard = client.lock().await;
475
- let row = guard
476
- .query_opt(
477
- &format!(
478
- "SELECT state FROM {} WHERE state->'identity_id'->>'value' = $1",
479
- journal::quote_ident(&journal::head_view(domain, &storage_name))
480
- ),
481
- &[&identity_id],
482
- )
496
+ let row = store
497
+ .head_find(&[domain], &storage_name, &[("identity_id", identity_id)])
483
498
  .await?;
484
- drop(guard);
485
- Ok(row.and_then(|r| {
486
- let state: Value = r.get(0);
487
- if is_disabled(&state) {
499
+ Ok(row.and_then(|state| {
500
+ if is_lapsed(&state) {
488
501
  return None;
489
502
  }
490
503
  Some(Session {
@@ -504,7 +517,7 @@ pub async fn session_for_member_by_identity(
504
517
  /// never creates one; Admit is a separate, real cap-table event.
505
518
  #[allow(clippy::too_many_arguments)]
506
519
  pub async fn provision(
507
- client: &Mutex<Client>,
520
+ store: &Store,
508
521
  wasm_path: &Path,
509
522
  config: &LineageConfig,
510
523
  domain_ir: &Value,
@@ -513,10 +526,10 @@ pub async fn provision(
513
526
  subject: &str,
514
527
  invoker: &dyn LambdaInvoker,
515
528
  ) -> anyhow::Result<Option<Session>> {
516
- let Some(member) = member_row_by_email(client, domain_ir, email).await? else {
529
+ let Some(member) = member_row_by_email(store, domain_ir, email).await? else {
517
530
  return Ok(None);
518
531
  };
519
- if is_disabled(&member) {
532
+ if is_lapsed(&member) {
520
533
  return Ok(None);
521
534
  }
522
535
  let role = member
@@ -532,10 +545,10 @@ pub async fn provision(
532
545
  };
533
546
 
534
547
  // Uses this domain's declared identity provider (`ir::identity_provider`),
535
- // never a hard-coded chapter. Link's reference field is `identity`, not
548
+ // never a hard-coded bluebook. Link's reference field is `identity`, not
536
549
  // `identity_id` -- changing this silently breaks linking.
537
550
  let Some(identity) = crate::ir::identity_provider(domain_ir) else {
538
- anyhow::bail!("this domain attaches no chapter that provides \"identity\" — cannot register or link an identity");
551
+ anyhow::bail!("this domain attaches no bluebook that provides \"identity\" — cannot register or link an identity");
539
552
  };
540
553
 
541
554
  // Reuse an Identity already registered+linked for this (issuer, subject)
@@ -551,7 +564,7 @@ pub async fn provision(
551
564
  // first makes a retry converge onto the earlier attempt's identity
552
565
  // instead of accumulating another orphan.
553
566
  let identity_id = if let Some(existing) =
554
- resolve_identity_from_head(client, domain_ir, issuer, subject).await?
567
+ resolve_identity_from_head(store, domain_ir, issuer, subject).await?
555
568
  {
556
569
  existing
557
570
  } else {
@@ -561,7 +574,7 @@ pub async fn provision(
561
574
  // provisioning, not a caller-submitted command, so there is no
562
575
  // caller role to assert.
563
576
  let register = dispatch::handle(
564
- Host::new(client, wasm_path, config, invoker),
577
+ Host::new(store, wasm_path, config, invoker),
565
578
  &identity.register,
566
579
  json!({"identity_id": {"value": identity_id}}),
567
580
  None,
@@ -572,7 +585,7 @@ pub async fn provision(
572
585
  }
573
586
 
574
587
  let link_external = dispatch::handle(
575
- Host::new(client, wasm_path, config, invoker),
588
+ Host::new(store, wasm_path, config, invoker),
576
589
  &identity.link,
577
590
  json!({
578
591
  "identity": identity_id, "key": {"value": format!("{issuer}:{subject}")},
@@ -589,15 +602,15 @@ pub async fn provision(
589
602
  };
590
603
 
591
604
  // The grant verb this domain's declared authorization provider names
592
- // (`ir::authorization_provider`), never a hard-coded chapter.
605
+ // (`ir::authorization_provider`), never a hard-coded bluebook.
593
606
  let Some(provider) = crate::ir::authorization_provider(domain_ir) else {
594
607
  anyhow::bail!(
595
- "this domain attaches no chapter that provides \"authorization\" — cannot grant a role"
608
+ "this domain attaches no bluebook that provides \"authorization\" — cannot grant a role"
596
609
  );
597
610
  };
598
- let assign_role = dispatch::handle(Host::new(client, wasm_path, config, invoker), &provider.grant, json!({
611
+ let assign_role = dispatch::handle(Host::new(store, wasm_path, config, invoker), &provider.grant, json!({
599
612
  "actor_id": {"value": identity_id}, "role_name": {"value": role}, "scope": {"value": config.domain.clone()},
600
- "starts_at": {"value": httpdate_now()},
613
+ "starts_at": {"value": crate::clock::iso8601_now()},
601
614
  }), None).await?;
602
615
  if !assign_role.accepted {
603
616
  anyhow::bail!("{} refused: {}", provider.grant, assign_role.result);
@@ -608,7 +621,7 @@ pub async fn provision(
608
621
  // refuse. Writes through the same journal+snapshot append instead.
609
622
  let mut new_state = member.clone();
610
623
  new_state["identity_id"] = json!({"value": identity_id});
611
- append_member_state(client, config, domain_ir, email, &new_state).await?;
624
+ append_member_state(store, config, domain_ir, email, &new_state).await?;
612
625
 
613
626
  let name = member
614
627
  .get("name")
@@ -625,37 +638,87 @@ pub async fn provision(
625
638
  }
626
639
 
627
640
  pub async fn grant_access(
628
- client: &Mutex<Client>,
641
+ store: &Store,
629
642
  wasm_path: &Path,
630
643
  config: &LineageConfig,
631
644
  domain_ir: &Value,
632
645
  email: &str,
633
646
  role: &str,
634
647
  ) -> anyhow::Result<bool> {
635
- let _ = wasm_path;
636
- let Some(member) = member_row_by_email(client, domain_ir, email).await? else {
637
- return Ok(false);
638
- };
648
+ let outcome =
649
+ grant_access_until(store, wasm_path, config, domain_ir, email, role, None).await?;
650
+ Ok(outcome == GrantOutcome::Done)
651
+ }
639
652
 
640
- // Same append protocol `provision` uses. GrantAccess's own bluebook
641
- // command sets only `:role`, no other invariant to preserve here.
642
- let mut new_state = member;
643
- new_state["role"] = json!({"value": role});
644
- append_member_state(client, config, domain_ir, email, &new_state).await?;
645
- Ok(true)
653
+ /// What `grant_access_until` decided.
654
+ #[derive(Debug, PartialEq, Eq)]
655
+ pub enum GrantOutcome {
656
+ /// The role (and window, if any) is now on the person's row.
657
+ Done,
658
+ /// No person with that email exists.
659
+ UnknownPerson,
660
+ /// The change would leave no active admin without an end date.
661
+ LastOpenAdmin,
662
+ }
663
+
664
+ /// Grants `role` like `grant_access`, for a window ending at `expires_at`
665
+ /// (an ISO 8601 UTC instant) when one is given. Granting again renews: the
666
+ /// new window replaces the old, and `None` clears it, so a plain grant makes
667
+ /// the access open-ended. Refused when it would take the site's last
668
+ /// open-ended admin off that footing, locked with other membership writes.
669
+ pub async fn grant_access_until(
670
+ store: &Store,
671
+ wasm_path: &Path,
672
+ config: &LineageConfig,
673
+ domain_ir: &Value,
674
+ email: &str,
675
+ role: &str,
676
+ expires_at: Option<&str>,
677
+ ) -> anyhow::Result<GrantOutcome> {
678
+ let _ = wasm_path;
679
+ let (aggregate_name, storage_name) = membership_aggregate(domain_ir)?;
680
+ let domain = domain_ir.get("name").and_then(|v| v.as_str()).unwrap_or("");
681
+ let target = email.trim().to_lowercase();
682
+
683
+ let outcome = store
684
+ .amend_head(config, domain, &aggregate_name, &storage_name, |rows| {
685
+ let Some((id, member)) = rows.iter().find(|(id, _)| id.to_lowercase() == target) else {
686
+ return (GrantOutcome::UnknownPerson, None);
687
+ };
688
+
689
+ // Same append protocol `provision` uses. GrantAccess's own bluebook
690
+ // command sets only `:role`; the window is a host-side field beside it,
691
+ // like `disabled`, so it needs no bluebook attribute.
692
+ let mut new_state = member.clone();
693
+ new_state["role"] = json!({"value": role});
694
+ match expires_at {
695
+ Some(instant) => new_state[EXPIRY_FIELD] = json!(instant),
696
+ None => {
697
+ if let Some(fields) = new_state.as_object_mut() {
698
+ fields.remove(EXPIRY_FIELD);
699
+ }
700
+ }
701
+ }
702
+ if strands_open_admin(rows, id, &new_state) {
703
+ return (GrantOutcome::LastOpenAdmin, None);
704
+ }
705
+ (GrantOutcome::Done, Some((id.clone(), new_state)))
706
+ })
707
+ .await?;
708
+ Ok(outcome)
646
709
  }
647
710
 
648
711
  /// Admits a new person (name + email), the same state `Admit` sets.
649
712
  /// Returns `false` without writing when that email already exists.
650
713
  pub async fn admit_person(
651
- client: &Mutex<Client>,
714
+ store: &Store,
652
715
  config: &LineageConfig,
653
716
  domain_ir: &Value,
654
717
  email: &str,
655
718
  name: &str,
656
719
  ) -> anyhow::Result<bool> {
657
720
  let email = email.to_lowercase();
658
- let exists = member_rows(client, domain_ir)
721
+ let exists = member_rows(store, domain_ir)
659
722
  .await?
660
723
  .into_iter()
661
724
  .any(|(id, _)| id.to_lowercase() == email);
@@ -663,7 +726,7 @@ pub async fn admit_person(
663
726
  return Ok(false);
664
727
  }
665
728
  let state = json!({"name": {"value": name}, "email": {"value": email}});
666
- append_member_state(client, config, domain_ir, &email, &state).await?;
729
+ append_member_state(store, config, domain_ir, &email, &state).await?;
667
730
  Ok(true)
668
731
  }
669
732
 
@@ -676,6 +739,77 @@ fn is_disabled(state: &Value) -> bool {
676
739
  .unwrap_or(false)
677
740
  }
678
741
 
742
+ // The row field holding the end of a time-boxed access window, an ISO 8601
743
+ // UTC instant. It sits beside `disabled` in the row state, so a grant that
744
+ // carries no window leaves the person with open-ended access as before.
745
+ const EXPIRY_FIELD: &str = "access_expires_at";
746
+
747
+ // The window's end as written on the row: a bare string, or a `{"value": ..}`
748
+ // wrapper as other timestamp attributes carry.
749
+ enum Window {
750
+ // No window is set (absent or null).
751
+ Open,
752
+ // A window ending at this many epoch seconds.
753
+ Ends(i64),
754
+ // A window is set but is not a readable instant; it counts as ended.
755
+ Unreadable,
756
+ }
757
+
758
+ fn window_of(state: &Value) -> Window {
759
+ let Some(raw) = state.get(EXPIRY_FIELD).filter(|v| !v.is_null()) else {
760
+ return Window::Open;
761
+ };
762
+ let text = raw.get("value").unwrap_or(raw).as_str();
763
+ text.and_then(crate::clock::parse_iso_utc)
764
+ .map_or(Window::Unreadable, Window::Ends)
765
+ }
766
+
767
+ // The window's end for display: the stored text, exactly as written.
768
+ fn expiry_text(state: &Value) -> Option<String> {
769
+ let raw = state.get(EXPIRY_FIELD).filter(|v| !v.is_null())?;
770
+ let text = raw.get("value").unwrap_or(raw);
771
+ Some(
772
+ text.as_str()
773
+ .map_or_else(|| text.to_string(), str::to_string),
774
+ )
775
+ }
776
+
777
+ // Whether the row's access window has ended at `now` (epoch seconds). An
778
+ // unreadable value counts as ended: access fails closed.
779
+ fn is_expired_at(state: &Value, now: i64) -> bool {
780
+ match window_of(state) {
781
+ Window::Open => false,
782
+ Window::Unreadable => true,
783
+ Window::Ends(ends) => ends <= now,
784
+ }
785
+ }
786
+
787
+ // Whether the row grants no access right now: disabled, or its window ended.
788
+ // Every check that lets a person in or counts an admin asks this, never
789
+ // `is_disabled` alone; disabling and enabling keep the raw flag.
790
+ fn is_lapsed(state: &Value) -> bool {
791
+ is_lapsed_at(state, ProcessClock.now_secs())
792
+ }
793
+
794
+ fn is_lapsed_at(state: &Value, now: i64) -> bool {
795
+ is_disabled(state) || is_expired_at(state, now)
796
+ }
797
+
798
+ /// The ISO 8601 UTC instant `days` days after `now` (epoch seconds), the
799
+ /// value `grant_access_until` stores as the end of a window.
800
+ pub fn expiry_after_days_from(days: u32, now: i64) -> String {
801
+ crate::clock::iso_utc((now + i64::from(days) * 86_400).max(0) as u64)
802
+ }
803
+
804
+ /// `expiry_after_days_from` counted from the process clock, which a spec
805
+ /// can fix (`needs::set_fixed_clock`).
806
+ pub fn expiry_after_days(days: u32) -> String {
807
+ expiry_after_days_from(days, ProcessClock.now_secs())
808
+ }
809
+
810
+ /// The longest window a grant may carry, in days.
811
+ pub const MAX_EXPIRY_DAYS: u32 = 365;
812
+
679
813
  // A deleted person's row is never erased (this is an append-only journal,
680
814
  // same as every other aggregate here) — "deleted" only means all_people
681
815
  // stops listing it. There is deliberately no undelete route: re-admitting
@@ -708,9 +842,10 @@ pub fn is_admin_role(role: &str) -> bool {
708
842
  ADMIN_ROLES.contains(&role)
709
843
  }
710
844
 
711
- // Whether the row holds an admin role ("Admin" or "Owner") and is not disabled.
845
+ // Whether the row holds an admin role ("Admin" or "Owner") and is not disabled
846
+ // or past its access window.
712
847
  fn is_active_admin(state: &Value) -> bool {
713
- role_of(state).is_some_and(is_admin_role) && !is_disabled(state)
848
+ role_of(state).is_some_and(is_admin_role) && !is_lapsed(state)
714
849
  }
715
850
 
716
851
  // Whether `state` is the only active admin (Admin or Owner) among `rows`.
@@ -718,60 +853,82 @@ fn is_last_active_admin(rows: &[(String, Value)], state: &Value) -> bool {
718
853
  is_active_admin(state) && rows.iter().filter(|(_, s)| is_active_admin(s)).count() <= 1
719
854
  }
720
855
 
856
+ // Whether the row is an active admin with no end date: the kind of access a
857
+ // site keeps at least one of, because a window's lapse cannot be prevented.
858
+ fn is_open_admin(state: &Value) -> bool {
859
+ is_active_admin(state) && matches!(window_of(state), Window::Open)
860
+ }
861
+
862
+ // Whether replacing the row `id` with `after` would leave the site with no
863
+ // open-ended admin where it had one, or would make the site's first admin one
864
+ // with an end date. A site that has admins but none open-ended (it was never
865
+ // given one) is not made worse by a change, so this stays false there.
866
+ fn strands_open_admin(rows: &[(String, Value)], id: &str, after: &Value) -> bool {
867
+ let had = rows.iter().any(|(_, s)| is_open_admin(s));
868
+ let keeps = is_open_admin(after)
869
+ || rows
870
+ .iter()
871
+ .any(|(other, s)| other.to_lowercase() != id.to_lowercase() && is_open_admin(s));
872
+ let first_admin = !rows
873
+ .iter()
874
+ .any(|(_, s)| role_of(s).is_some_and(is_admin_role));
875
+ let windowed_first = first_admin && role_of(after).is_some_and(is_admin_role) && !keeps;
876
+ (had && !keeps) || windowed_first
877
+ }
878
+
721
879
  /// Whether the person with `email` (compared case-insensitively) is an
722
- /// active admin: granted the "Admin" or "Owner" role and not disabled.
880
+ /// active admin: granted the "Admin" or "Owner" role, not disabled, and not
881
+ /// past its access window.
723
882
  pub async fn caller_is_admin(
724
- client: &Mutex<Client>,
883
+ store: &Store,
725
884
  domain_ir: &Value,
726
885
  email: &str,
727
886
  ) -> anyhow::Result<bool> {
728
887
  let email = email.to_lowercase();
729
- Ok(member_rows(client, domain_ir)
888
+ Ok(member_rows(store, domain_ir)
730
889
  .await?
731
890
  .iter()
732
891
  .any(|(id, state)| id.to_lowercase() == email && is_active_admin(state)))
733
892
  }
734
893
 
735
894
  /// Whether `email` may hold a session right now: admitted, granted a role,
736
- /// and not disabled — a signature alone can't tell, since a cookie outlives a later disable.
737
- pub async fn has_access(
738
- client: &Mutex<Client>,
739
- domain_ir: &Value,
740
- email: &str,
741
- ) -> anyhow::Result<bool> {
895
+ /// and neither disabled nor past its access window — a signature alone can't
896
+ /// tell, since a cookie outlives a later disable or the end of the window.
897
+ pub async fn has_access(store: &Store, domain_ir: &Value, email: &str) -> anyhow::Result<bool> {
742
898
  let email = email.to_lowercase();
743
- Ok(member_rows(client, domain_ir)
899
+ Ok(member_rows(store, domain_ir)
744
900
  .await?
745
901
  .iter()
746
902
  .any(|(id, state)| {
747
- id.to_lowercase() == email && role_of(state).is_some() && !is_disabled(state)
903
+ id.to_lowercase() == email && role_of(state).is_some() && !is_lapsed(state)
748
904
  }))
749
905
  }
750
906
 
751
907
  /// The role `email` holds right now, or `None` if unknown, roleless, or
752
- /// disabled — reads the live membership head, never the session cookie.
908
+ /// disabled or past its window — reads the live membership head, never the
909
+ /// session cookie.
753
910
  pub async fn active_role(
754
- client: &Mutex<Client>,
911
+ store: &Store,
755
912
  domain_ir: &Value,
756
913
  email: &str,
757
914
  ) -> anyhow::Result<Option<String>> {
758
915
  let email = email.to_lowercase();
759
- Ok(member_rows(client, domain_ir)
916
+ Ok(member_rows(store, domain_ir)
760
917
  .await?
761
918
  .iter()
762
- .find(|(id, state)| id.to_lowercase() == email && !is_disabled(state))
919
+ .find(|(id, state)| id.to_lowercase() == email && !is_lapsed(state))
763
920
  .and_then(|(_, state)| role_of(state).map(String::from)))
764
921
  }
765
922
 
766
923
  /// The Governance identity id linked to the member with `email`, read from
767
924
  /// the live membership head. `None` when the member has not signed in yet.
768
925
  pub async fn member_identity_id(
769
- client: &Mutex<Client>,
926
+ store: &Store,
770
927
  domain_ir: &Value,
771
928
  email: &str,
772
929
  ) -> anyhow::Result<Option<String>> {
773
930
  let email = email.to_lowercase();
774
- Ok(member_rows(client, domain_ir)
931
+ Ok(member_rows(store, domain_ir)
775
932
  .await?
776
933
  .iter()
777
934
  .find(|(id, state)| id.to_lowercase() == email && !is_deleted(state))
@@ -788,10 +945,13 @@ fn linked_identity_id(state: &Value) -> Option<String> {
788
945
  }
789
946
 
790
947
  /// Every admitted person as a JSON row: name, email, role, `linked`
791
- /// (signed in), `granted` (has access), `disabled`, and `identity_id`
948
+ /// (signed in), `granted` (has access right now), `disabled` (the raw flag),
949
+ /// `access_expires_at` (the end of a time-boxed window, `null` when open-ended),
950
+ /// `expired` (the window has ended or is unreadable), and `identity_id`
792
951
  /// (the Governance identity id, `null` until the person has signed in).
793
- pub async fn all_people(client: &Mutex<Client>, domain_ir: &Value) -> anyhow::Result<Vec<Value>> {
794
- Ok(member_rows(client, domain_ir)
952
+ pub async fn all_people(store: &Store, domain_ir: &Value) -> anyhow::Result<Vec<Value>> {
953
+ let now = ProcessClock.now_secs();
954
+ Ok(member_rows(store, domain_ir)
795
955
  .await?
796
956
  .into_iter()
797
957
  .filter(|(_, state)| !is_deleted(state))
@@ -802,13 +962,16 @@ pub async fn all_people(client: &Mutex<Client>, domain_ir: &Value) -> anyhow::Re
802
962
  .and_then(|v| v.get("value"))
803
963
  .is_some();
804
964
  let disabled = is_disabled(&state);
965
+ let expired = is_expired_at(&state, now);
805
966
  json!({
806
967
  "name": state.get("name").and_then(|v| v.get("value")),
807
968
  "email": state.get("email").and_then(|v| v.get("value")),
808
969
  "role": role,
809
970
  "linked": linked,
810
- "granted": role.is_some() && !disabled,
971
+ "granted": role.is_some() && !disabled && !expired,
811
972
  "disabled": disabled,
973
+ "access_expires_at": expiry_text(&state),
974
+ "expired": expired,
812
975
  "identity_id": linked_identity_id(&state),
813
976
  })
814
977
  })
@@ -828,12 +991,14 @@ pub enum DisableOutcome {
828
991
  UnknownPerson,
829
992
  /// Disabling would leave no active Admin.
830
993
  LastAdmin,
994
+ /// Disabling would leave no active Admin without an end date.
995
+ LastOpenAdmin,
831
996
  }
832
997
 
833
998
  /// Disables or enables `target` for `caller`, keeping role and identity
834
999
  /// link — locked with other membership writes so concurrent disables can't both win.
835
1000
  pub async fn set_person_disabled(
836
- client: &Mutex<Client>,
1001
+ store: &Store,
837
1002
  config: &LineageConfig,
838
1003
  domain_ir: &Value,
839
1004
  caller: &str,
@@ -844,53 +1009,40 @@ pub async fn set_person_disabled(
844
1009
  let domain = domain_ir.get("name").and_then(|v| v.as_str()).unwrap_or("");
845
1010
  let (caller, target) = (caller.trim().to_lowercase(), target.trim().to_lowercase());
846
1011
 
847
- let mut guard = client.lock().await;
848
- let txn = guard.transaction().await?;
849
- txn.execute(
850
- "SELECT pg_advisory_xact_lock(hashtext('hecks_ordinal:' || $1))",
851
- &[&config.domain],
852
- )
853
- .await?;
854
- let rows = journal::read_lineage_head_all(&txn, domain, &storage_name).await?;
855
-
856
- if !rows
857
- .iter()
858
- .any(|(id, state)| id.to_lowercase() == caller && is_active_admin(state))
859
- {
860
- return Ok(DisableOutcome::CallerNotAdmin);
861
- }
862
- if disable && caller == target {
863
- return Ok(DisableOutcome::SelfDisable);
864
- }
865
- let Some((id, state)) = rows.iter().find(|(id, _)| id.to_lowercase() == target) else {
866
- return Ok(DisableOutcome::UnknownPerson);
867
- };
868
- if is_disabled(state) == disable {
869
- return Ok(DisableOutcome::Done);
870
- }
871
- if disable && is_last_active_admin(&rows, state) {
872
- return Ok(DisableOutcome::LastAdmin);
873
- }
1012
+ let outcome = store
1013
+ .amend_head(config, domain, &aggregate_name, &storage_name, |rows| {
1014
+ if !rows
1015
+ .iter()
1016
+ .any(|(id, state)| id.to_lowercase() == caller && is_active_admin(state))
1017
+ {
1018
+ return (DisableOutcome::CallerNotAdmin, None);
1019
+ }
1020
+ if disable && caller == target {
1021
+ return (DisableOutcome::SelfDisable, None);
1022
+ }
1023
+ let Some((id, state)) = rows.iter().find(|(id, _)| id.to_lowercase() == target) else {
1024
+ return (DisableOutcome::UnknownPerson, None);
1025
+ };
1026
+ if is_disabled(state) == disable {
1027
+ return (DisableOutcome::Done, None);
1028
+ }
1029
+ if disable && is_last_active_admin(rows, state) {
1030
+ return (DisableOutcome::LastAdmin, None);
1031
+ }
874
1032
 
875
- let mut new_state = state.clone();
876
- if disable {
877
- new_state["disabled"] = json!(true);
878
- } else if let Some(fields) = new_state.as_object_mut() {
879
- fields.remove("disabled");
880
- }
881
- journal::append_lineage_mutation(
882
- &txn,
883
- config,
884
- &journal::Mutation {
885
- aggregate: &aggregate_name,
886
- id,
887
- operation: "save",
888
- state: &new_state,
889
- },
890
- )
891
- .await?;
892
- txn.commit().await?;
893
- Ok(DisableOutcome::Done)
1033
+ let mut new_state = state.clone();
1034
+ if disable {
1035
+ new_state["disabled"] = json!(true);
1036
+ } else if let Some(fields) = new_state.as_object_mut() {
1037
+ fields.remove("disabled");
1038
+ }
1039
+ if strands_open_admin(rows, id, &new_state) {
1040
+ return (DisableOutcome::LastOpenAdmin, None);
1041
+ }
1042
+ (DisableOutcome::Done, Some((id.clone(), new_state)))
1043
+ })
1044
+ .await?;
1045
+ Ok(outcome)
894
1046
  }
895
1047
 
896
1048
  /// What `set_person_deleted` decided.
@@ -911,7 +1063,7 @@ pub enum DeleteOutcome {
911
1063
  /// stays in the journal (nothing here is ever erased) but `all_people` stops
912
1064
  /// listing it. Locked with other membership writes the same as disable/role.
913
1065
  pub async fn set_person_deleted(
914
- client: &Mutex<Client>,
1066
+ store: &Store,
915
1067
  config: &LineageConfig,
916
1068
  domain_ir: &Value,
917
1069
  caller: &str,
@@ -921,46 +1073,30 @@ pub async fn set_person_deleted(
921
1073
  let domain = domain_ir.get("name").and_then(|v| v.as_str()).unwrap_or("");
922
1074
  let (caller, target) = (caller.trim().to_lowercase(), target.trim().to_lowercase());
923
1075
 
924
- let mut guard = client.lock().await;
925
- let txn = guard.transaction().await?;
926
- txn.execute(
927
- "SELECT pg_advisory_xact_lock(hashtext('hecks_ordinal:' || $1))",
928
- &[&config.domain],
929
- )
930
- .await?;
931
- let rows = journal::read_lineage_head_all(&txn, domain, &storage_name).await?;
932
-
933
- if !rows
934
- .iter()
935
- .any(|(id, state)| id.to_lowercase() == caller && is_active_admin(state))
936
- {
937
- return Ok(DeleteOutcome::CallerNotAdmin);
938
- }
939
- let Some((id, state)) = rows.iter().find(|(id, _)| id.to_lowercase() == target) else {
940
- return Ok(DeleteOutcome::UnknownPerson);
941
- };
942
- if is_deleted(state) {
943
- return Ok(DeleteOutcome::Done);
944
- }
945
- if !is_disabled(state) {
946
- return Ok(DeleteOutcome::NotDisabled);
947
- }
1076
+ let outcome = store
1077
+ .amend_head(config, domain, &aggregate_name, &storage_name, |rows| {
1078
+ if !rows
1079
+ .iter()
1080
+ .any(|(id, state)| id.to_lowercase() == caller && is_active_admin(state))
1081
+ {
1082
+ return (DeleteOutcome::CallerNotAdmin, None);
1083
+ }
1084
+ let Some((id, state)) = rows.iter().find(|(id, _)| id.to_lowercase() == target) else {
1085
+ return (DeleteOutcome::UnknownPerson, None);
1086
+ };
1087
+ if is_deleted(state) {
1088
+ return (DeleteOutcome::Done, None);
1089
+ }
1090
+ if !is_disabled(state) {
1091
+ return (DeleteOutcome::NotDisabled, None);
1092
+ }
948
1093
 
949
- let mut new_state = state.clone();
950
- new_state["deleted"] = json!(true);
951
- journal::append_lineage_mutation(
952
- &txn,
953
- config,
954
- &journal::Mutation {
955
- aggregate: &aggregate_name,
956
- id,
957
- operation: "save",
958
- state: &new_state,
959
- },
960
- )
961
- .await?;
962
- txn.commit().await?;
963
- Ok(DeleteOutcome::Done)
1094
+ let mut new_state = state.clone();
1095
+ new_state["deleted"] = json!(true);
1096
+ (DeleteOutcome::Done, Some((id.clone(), new_state)))
1097
+ })
1098
+ .await?;
1099
+ Ok(outcome)
964
1100
  }
965
1101
 
966
1102
  /// What `set_person_role` decided.
@@ -974,12 +1110,14 @@ pub enum RoleOutcome {
974
1110
  UnknownPerson,
975
1111
  /// The change would leave no active Admin or Owner.
976
1112
  LastAdmin,
1113
+ /// The change would leave no active Admin or Owner without an end date.
1114
+ LastOpenAdmin,
977
1115
  }
978
1116
 
979
1117
  /// Sets `target`'s role on `caller`'s behalf; any active admin may
980
1118
  /// grant any role, Owner included, locked against concurrent demotions.
981
1119
  pub async fn set_person_role(
982
- client: &Mutex<Client>,
1120
+ store: &Store,
983
1121
  config: &LineageConfig,
984
1122
  domain_ir: &Value,
985
1123
  caller: &str,
@@ -990,77 +1128,59 @@ pub async fn set_person_role(
990
1128
  let domain = domain_ir.get("name").and_then(|v| v.as_str()).unwrap_or("");
991
1129
  let (caller, target) = (caller.trim().to_lowercase(), target.trim().to_lowercase());
992
1130
 
993
- let mut guard = client.lock().await;
994
- let txn = guard.transaction().await?;
995
- txn.execute(
996
- "SELECT pg_advisory_xact_lock(hashtext('hecks_ordinal:' || $1))",
997
- &[&config.domain],
998
- )
999
- .await?;
1000
- let rows = journal::read_lineage_head_all(&txn, domain, &storage_name).await?;
1001
-
1002
- if !rows
1003
- .iter()
1004
- .any(|(id, state)| id.to_lowercase() == caller && is_active_admin(state))
1005
- {
1006
- return Ok(RoleOutcome::CallerNotAdmin);
1007
- }
1008
- let Some((id, state)) = rows.iter().find(|(id, _)| id.to_lowercase() == target) else {
1009
- return Ok(RoleOutcome::UnknownPerson);
1010
- };
1011
- if role_of(state) == Some(role) {
1012
- return Ok(RoleOutcome::Done);
1013
- }
1014
- if !is_admin_role(role) && is_last_active_admin(&rows, state) {
1015
- return Ok(RoleOutcome::LastAdmin);
1016
- }
1131
+ let outcome = store
1132
+ .amend_head(config, domain, &aggregate_name, &storage_name, |rows| {
1133
+ if !rows
1134
+ .iter()
1135
+ .any(|(id, state)| id.to_lowercase() == caller && is_active_admin(state))
1136
+ {
1137
+ return (RoleOutcome::CallerNotAdmin, None);
1138
+ }
1139
+ let Some((id, state)) = rows.iter().find(|(id, _)| id.to_lowercase() == target) else {
1140
+ return (RoleOutcome::UnknownPerson, None);
1141
+ };
1142
+ if role_of(state) == Some(role) {
1143
+ return (RoleOutcome::Done, None);
1144
+ }
1145
+ if !is_admin_role(role) && is_last_active_admin(rows, state) {
1146
+ return (RoleOutcome::LastAdmin, None);
1147
+ }
1017
1148
 
1018
- let mut new_state = state.clone();
1019
- new_state["role"] = json!({"value": role});
1020
- journal::append_lineage_mutation(
1021
- &txn,
1022
- config,
1023
- &journal::Mutation {
1024
- aggregate: &aggregate_name,
1025
- id,
1026
- operation: "save",
1027
- state: &new_state,
1028
- },
1029
- )
1030
- .await?;
1031
- txn.commit().await?;
1032
- Ok(RoleOutcome::Done)
1149
+ let mut new_state = state.clone();
1150
+ new_state["role"] = json!({"value": role});
1151
+ if strands_open_admin(rows, id, &new_state) {
1152
+ return (RoleOutcome::LastOpenAdmin, None);
1153
+ }
1154
+ (RoleOutcome::Done, Some((id.clone(), new_state)))
1155
+ })
1156
+ .await?;
1157
+ Ok(outcome)
1033
1158
  }
1034
1159
 
1035
- /// Whether `identity_id` holds a live Admin/Owner assignment in this
1036
- /// domain's authorization provider; `None` provider means never admin.
1037
- pub fn holds_admin(
1038
- instances: &Value,
1039
- identity_id: &str,
1040
- provider: Option<&crate::ir::AuthorizationProvider>,
1041
- ) -> bool {
1042
- let Some(provider) = provider else {
1043
- return false;
1044
- };
1045
- let prefix = format!("{}#", provider.assignment_aggregate);
1046
- instances.as_object().is_some_and(|obj| {
1047
- obj.iter().any(|(key, state)| {
1048
- key.starts_with(&prefix)
1049
- && state
1050
- .get("actor_id")
1051
- .and_then(|v| v.get("value"))
1052
- .and_then(|v| v.as_str())
1053
- == Some(identity_id)
1054
- && state
1055
- .get("role_name")
1056
- .and_then(|v| v.get("value"))
1057
- .and_then(|v| v.as_str())
1058
- .is_some_and(is_admin_role)
1059
- && state.get("ends_at").is_none_or(serde_json::Value::is_null)
1060
- })
1160
+ /// Whether any of `rows` — the provider's `live_assignments` answer for one actor — grants an
1161
+ /// Admin or Owner role. The bluebook's query decides which assignments are live, so this reads
1162
+ /// no `ends_at`.
1163
+ pub fn holds_admin(rows: &[Value]) -> bool {
1164
+ rows.iter().any(|row| {
1165
+ row.get("role_name")
1166
+ .and_then(|v| v.get("value"))
1167
+ .and_then(|v| v.as_str())
1168
+ .is_some_and(is_admin_role)
1061
1169
  })
1062
1170
  }
1063
1171
 
1172
+ /// The rows of a kernel query answer (`{"queries":[{"rows":[...]}]}`).
1173
+ pub fn answer_rows(answer: &Value) -> Vec<Value> {
1174
+ answer
1175
+ .get("queries")
1176
+ .and_then(|q| q.as_array())
1177
+ .and_then(|q| q.first())
1178
+ .and_then(|q| q.get("rows"))
1179
+ .and_then(|r| r.as_array())
1180
+ .cloned()
1181
+ .unwrap_or_default()
1182
+ }
1183
+
1064
1184
  fn now_secs() -> u64 {
1065
1185
  SystemTime::now()
1066
1186
  .duration_since(UNIX_EPOCH)
@@ -1068,36 +1188,6 @@ fn now_secs() -> u64 {
1068
1188
  .as_secs()
1069
1189
  }
1070
1190
 
1071
- // `dispatch.rs`'s `occurred_at` stamping reuses this exact rendering
1072
- // rather than re-deriving it — see the body comment for the format
1073
- // guarantee both call sites depend on.
1074
- pub(crate) fn httpdate_now() -> String {
1075
- // ISO 8601 UTC, matching Ruby's Time.now.utc.iso8601 -- Governance::
1076
- // RoleAssignment.starts_at only needs to parse as a real instant,
1077
- // never re-derived from or compared against wall-clock time here.
1078
- let secs = now_secs();
1079
- let days = secs / 86_400;
1080
- let rem = secs % 86_400;
1081
- let (h, m, s) = (rem / 3600, (rem % 3600) / 60, rem % 60);
1082
- let (y, mo, d) = civil_from_days(days as i64);
1083
- format!("{y:04}-{mo:02}-{d:02}T{h:02}:{m:02}:{s:02}Z")
1084
- }
1085
-
1086
- // Howard Hinnant's days-from-civil algorithm, inverted -- avoids
1087
- // pulling in a full date/time crate for one timestamp format.
1088
- const fn civil_from_days(z: i64) -> (i64, u32, u32) {
1089
- let z = z + 719_468;
1090
- let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
1091
- let doe = (z - era * 146_097) as u64;
1092
- let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
1093
- let y = yoe as i64 + era * 400;
1094
- let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
1095
- let mp = (5 * doy + 2) / 153;
1096
- let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
1097
- let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
1098
- (if m <= 2 { y + 1 } else { y }, m, d)
1099
- }
1100
-
1101
1191
  #[allow(clippy::expect_used)] // reason: HMAC accepts a key of any length, so construction cannot fail
1102
1192
  fn sign(secret: &str, payload: &str) -> String {
1103
1193
  use hmac::{Hmac, Mac};
@@ -1136,6 +1226,8 @@ pub(crate) fn urlencode(s: &str) -> String {
1136
1226
  #[cfg(test)]
1137
1227
  mod tests {
1138
1228
  use super::*;
1229
+ #[allow(unused_imports)]
1230
+ use crate::store::StoreFixtures;
1139
1231
 
1140
1232
  #[test]
1141
1233
  fn signed_token_verifies_with_the_right_secret_and_rejects_tampering() {
@@ -1287,15 +1379,42 @@ mod tests {
1287
1379
  }
1288
1380
 
1289
1381
  #[test]
1290
- fn oauth_state_verifies_fresh_and_rejects_forged_or_expired() {
1291
- let fresh = now_secs().to_string();
1292
- let state = format!("{fresh}.{}", sign("s3cret", &fresh));
1293
- assert!(verify_state(&state, "s3cret").is_ok());
1294
- assert!(verify_state(&state, "wrong-secret").is_err());
1295
-
1296
- let stale = (now_secs() - STATE_TTL_SECS - 1).to_string();
1297
- let expired = format!("{stale}.{}", sign("s3cret", &stale));
1298
- assert!(verify_state(&expired, "s3cret").is_err());
1382
+ fn an_id_token_is_accepted_only_with_the_sign_ins_own_nonce() {
1383
+ assert!(check_nonce(&json!({"nonce": "n-123"}), "n-123").is_ok());
1384
+ assert!(check_nonce(&json!({"nonce": "n-456"}), "n-123").is_err());
1385
+ assert!(check_nonce(&json!({}), "n-123").is_err());
1386
+ assert!(check_nonce(&json!({"nonce": 5}), "n-123").is_err());
1387
+ }
1388
+
1389
+ #[test]
1390
+ fn a_handoff_token_is_redeemed_once_for_the_account_token() {
1391
+ let spent = crate::platform_signin::jti::JtiStore::new();
1392
+ let handoff = handoff_token("s3cret", "zed@example.com");
1393
+ assert_eq!(verify_account_token("s3cret", &handoff), None);
1394
+ let account = redeem_handoff_token("s3cret", &handoff, &spent).unwrap();
1395
+ assert_eq!(
1396
+ verify_account_token("s3cret", &account).as_deref(),
1397
+ Some("zed@example.com")
1398
+ );
1399
+ assert_eq!(redeem_handoff_token("s3cret", &handoff, &spent), None);
1400
+ }
1401
+
1402
+ #[test]
1403
+ fn an_account_or_foreign_token_is_not_a_handoff_token() {
1404
+ let spent = crate::platform_signin::jti::JtiStore::new();
1405
+ let account = account_token("s3cret", "zed@example.com", 60);
1406
+ assert_eq!(redeem_handoff_token("s3cret", &account, &spent), None);
1407
+ let handoff = handoff_token("other-secret", "zed@example.com");
1408
+ assert_eq!(redeem_handoff_token("s3cret", &handoff, &spent), None);
1409
+ }
1410
+
1411
+ #[test]
1412
+ fn the_consent_url_carries_the_sign_ins_state_and_nonce() {
1413
+ crate::secrets::provide("GOOGLE_CLIENT_ID", "client-1".to_string());
1414
+ let signin = GoogleSignin::fresh();
1415
+ let url = authorization_url("https://host.example/auth/google/callback", &signin).unwrap();
1416
+ assert!(url.contains(&format!("&state={}", signin.state)), "{url}");
1417
+ assert!(url.contains(&format!("&nonce={}", signin.nonce)), "{url}");
1299
1418
  }
1300
1419
 
1301
1420
  #[test]
@@ -1312,7 +1431,7 @@ mod tests {
1312
1431
  #[test]
1313
1432
  fn resolve_identity_finds_a_link_that_only_the_flat_journal_holds() {
1314
1433
  // The shape `ExternalIdentifier.Link` leaves in the flat snapshot of a domain whose Identity
1315
- // chapter has no head of its own; the key carries the issuer URL and the subject.
1434
+ // bluebook has no head of its own; the key carries the issuer URL and the subject.
1316
1435
  let instances = json!({
1317
1436
  "Identity::ExternalIdentifier#https://accounts.google.com:111": {
1318
1437
  "identity": "471ef402-8609-4fa4-9288-6372fc74650d",
@@ -1356,21 +1475,61 @@ mod tests {
1356
1475
  }
1357
1476
 
1358
1477
  #[test]
1359
- fn holds_admin_reads_straight_off_instances() {
1360
- let instances = json!({
1361
- "Governance::RoleAssignment#id-1:Admin": {
1362
- "actor_id": {"value": "id-1"}, "role_name": {"value": "Admin"}, "ends_at": null,
1363
- },
1364
- });
1478
+ fn holds_admin_reads_the_live_assignment_rows() {
1479
+ let live = [json!({"actor_id": {"value": "id-1"}, "role_name": {"value": "Admin"}})];
1480
+ assert!(holds_admin(&live));
1481
+ // No live assignment answered: no role is held, whatever was granted before.
1482
+ assert!(!holds_admin(&[]));
1483
+ }
1484
+
1485
+ #[test]
1486
+ fn answer_rows_reads_the_first_query_of_a_kernel_answer() {
1487
+ let answer = json!({"queries": [{"query": "G.Live", "rows": [{"id": "a"}]}]});
1488
+ assert_eq!(answer_rows(&answer), vec![json!({"id": "a"})]);
1489
+ assert!(answer_rows(&json!({"queries": []})).is_empty());
1490
+ }
1491
+
1492
+ #[test]
1493
+ fn a_window_lapses_exactly_at_its_end_and_a_missing_one_never_does() {
1494
+ let ends = crate::clock::parse_iso_utc("2026-10-22T12:00:00Z").unwrap();
1495
+ let row = json!({"role": {"value": "Member"}, "access_expires_at": "2026-10-22T12:00:00Z"});
1496
+ assert!(!is_lapsed_at(&row, ends - 1), "one second before the end");
1497
+ assert!(is_lapsed_at(&row, ends), "the end instant itself is over");
1498
+ assert!(is_lapsed_at(&row, ends + 1));
1499
+ let open = json!({"role": {"value": "Member"}});
1500
+ assert!(!is_lapsed_at(&open, i64::MAX));
1501
+ let unreadable = json!({"access_expires_at": "next week"});
1502
+ assert!(is_lapsed_at(&unreadable, 0), "unreadable fails closed");
1503
+ let disabled = json!({"disabled": true, "access_expires_at": "2099-01-01T00:00:00Z"});
1504
+ assert!(
1505
+ is_lapsed_at(&disabled, 0),
1506
+ "an open window does not undo a disable"
1507
+ );
1508
+ }
1509
+
1510
+ #[test]
1511
+ fn expiry_after_days_counts_whole_days_from_the_given_instant() {
1512
+ let start = crate::clock::parse_iso_utc("2026-10-22T08:30:15Z").unwrap();
1513
+ assert_eq!(expiry_after_days_from(1, start), "2026-10-23T08:30:15Z");
1514
+ assert_eq!(
1515
+ expiry_after_days_from(MAX_EXPIRY_DAYS, start),
1516
+ "2027-10-22T08:30:15Z"
1517
+ );
1518
+ }
1365
1519
 
1366
- let provider = crate::ir::AuthorizationProvider {
1367
- grant: "Governance::RoleAssignment.Assign".to_string(),
1368
- assignment_aggregate: "Governance::RoleAssignment".to_string(),
1520
+ #[test]
1521
+ fn is_last_active_admin_ignores_an_admin_whose_window_ended() {
1522
+ let admin = json!({"role": {"value": "Admin"}});
1523
+ let expired =
1524
+ json!({"role": {"value": "Admin"}, "access_expires_at": "2020-01-01T00:00:00Z"});
1525
+ let rows = |states: &[&Value]| -> Vec<(String, Value)> {
1526
+ states
1527
+ .iter()
1528
+ .map(|s| (String::new(), (*s).clone()))
1529
+ .collect()
1369
1530
  };
1370
- assert!(holds_admin(&instances, "id-1", Some(&provider)));
1371
- assert!(!holds_admin(&instances, "id-2", Some(&provider)));
1372
- // No declared provider: no assignment can exist, so never admin.
1373
- assert!(!holds_admin(&instances, "id-1", None));
1531
+ assert!(is_last_active_admin(&rows(&[&admin, &expired]), &admin));
1532
+ assert!(!is_last_active_admin(&rows(&[&admin, &expired]), &expired));
1374
1533
  }
1375
1534
 
1376
1535
  #[test]
@@ -1417,15 +1576,12 @@ mod tests {
1417
1576
 
1418
1577
  #[test]
1419
1578
  fn holds_admin_accepts_an_owner_assignment_and_refuses_other_roles() {
1420
- let provider = crate::ir::AuthorizationProvider {
1421
- grant: "G".to_string(),
1422
- assignment_aggregate: "Governance::RoleAssignment".to_string(),
1423
- };
1424
- let assignment = |role: &str| json!({"Governance::RoleAssignment#a": {"actor_id": {"value": "id-1"}, "role_name": {"value": role}, "ends_at": null}});
1425
- assert!(holds_admin(&assignment("Admin"), "id-1", Some(&provider)));
1426
- assert!(holds_admin(&assignment("Owner"), "id-1", Some(&provider)));
1427
- assert!(!holds_admin(&assignment("Member"), "id-1", Some(&provider)));
1428
- assert!(!holds_admin(&assignment("owner"), "id-1", Some(&provider)));
1579
+ let rows =
1580
+ |role: &str| [json!({"actor_id": {"value": "id-1"}, "role_name": {"value": role}})];
1581
+ assert!(holds_admin(&rows("Admin")));
1582
+ assert!(holds_admin(&rows("Owner")));
1583
+ assert!(!holds_admin(&rows("Member")));
1584
+ assert!(!holds_admin(&rows("owner")));
1429
1585
  }
1430
1586
 
1431
1587
  #[test]
@@ -1456,7 +1612,7 @@ mod tests {
1456
1612
  // A throwaway Postgres database per test, matching the real
1457
1613
  // `head_view` naming (`qualified_name(domain, "#{storage_name}_head")`,
1458
1614
  // docs/decisions/0059) that the functions under test query directly.
1459
- async fn scratch_member_db(name: &str) -> Mutex<tokio_postgres::Client> {
1615
+ async fn scratch_member_db(name: &str) -> Store {
1460
1616
  use tokio_postgres::NoTls;
1461
1617
  let (admin, conn) = tokio_postgres::connect(&crate::test_pg::conninfo("postgres"), NoTls)
1462
1618
  .await
@@ -1493,7 +1649,7 @@ mod tests {
1493
1649
  )
1494
1650
  .await
1495
1651
  .unwrap();
1496
- Mutex::new(client)
1652
+ crate::store::postgres_store(client)
1497
1653
  }
1498
1654
 
1499
1655
  // A minimal `ir.json`-shaped fixture: one lineage-capable aggregate
@@ -1592,6 +1748,7 @@ mod tests {
1592
1748
  domain: "Acme".to_string(),
1593
1749
  era: Some(1),
1594
1750
  mirrored: None,
1751
+ seal_every: None,
1595
1752
  };
1596
1753
  {
1597
1754
  let guard = db.lock().await;