hecks 3.10.0 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/bluebook/pizzas.bluebook +29 -0
  3. data/examples/pizzas/bluebook/pizzas.hecksagon +7 -0
  4. data/exe/hecks +4 -4
  5. data/lib/hecks/adapters/driven/d1.rb +2 -10
  6. data/lib/hecks/adapters/driven/heki.adapter +1 -1
  7. data/lib/hecks/adapters/driven/heki.rb +2 -12
  8. data/lib/hecks/adapters/driven/lambda.adapter +1 -1
  9. data/lib/hecks/adapters/driven/lambda.rb +3 -18
  10. data/lib/hecks/adapters/driven/postgres.rb +4 -11
  11. data/lib/hecks/adapters/driven/sqlite.adapter +1 -1
  12. data/lib/hecks/adapters/driven/sqlite.rb +2 -10
  13. data/lib/hecks/adapters/driving/admission.rb +46 -0
  14. data/lib/hecks/adapters/driving/cli/nesting.rb +36 -0
  15. data/lib/hecks/adapters/driving/cli/shorthand.rb +88 -0
  16. data/lib/hecks/adapters/driving/cli.rb +127 -0
  17. data/lib/hecks/adapters/driving/cli_runner/answers.rb +160 -0
  18. data/lib/hecks/adapters/driving/cli_runner/resolution.rb +155 -0
  19. data/lib/hecks/adapters/driving/cli_runner/settling.rb +120 -0
  20. data/lib/hecks/adapters/driving/cli_runner/streaming.rb +93 -0
  21. data/lib/hecks/adapters/driving/cli_runner/suggestions.rb +47 -0
  22. data/lib/hecks/adapters/driving/cli_runner.rb +69 -0
  23. data/lib/hecks/adapters/driving/command_request.rb +121 -0
  24. data/lib/hecks/adapters/driving/handle.rb +214 -0
  25. data/lib/hecks/adapters/driving/json.rb +136 -0
  26. data/lib/hecks/adapters/driving/launcher_options.rb +267 -0
  27. data/lib/hecks/adapters/driving/mcp/properties.rb +49 -0
  28. data/lib/hecks/adapters/driving/mcp/tool_calls.rb +116 -0
  29. data/lib/hecks/adapters/driving/mcp/tool_definitions/auditing.rb +53 -0
  30. data/lib/hecks/adapters/driving/mcp/tool_definitions/calling.rb +71 -0
  31. data/lib/hecks/adapters/driving/mcp/tool_definitions/reading.rb +99 -0
  32. data/lib/hecks/adapters/driving/mcp/tool_definitions.rb +18 -0
  33. data/lib/hecks/adapters/driving/mcp.rb +159 -0
  34. data/lib/hecks/adapters/driving/mcp_guide_cache.rb +78 -0
  35. data/lib/hecks/adapters/driving/mcp_scope/argument_policy.rb +97 -0
  36. data/lib/hecks/adapters/driving/mcp_scope/env_problems.rb +69 -0
  37. data/lib/hecks/adapters/driving/mcp_scope/presenter.rb +83 -0
  38. data/lib/hecks/adapters/driving/mcp_scope.rb +213 -0
  39. data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +171 -0
  40. data/lib/hecks/adapters/driving/ruby/chapter.rb +91 -0
  41. data/lib/hecks/adapters/driving/ruby.rb +83 -0
  42. data/lib/hecks/adapters/driving/usage_cache.rb +144 -0
  43. data/lib/hecks/{doors.rb → adapters/driving.rb} +10 -6
  44. data/lib/hecks/behaviors/expectations/boot.rb +1 -1
  45. data/lib/hecks/bench/ruby_runner.rb +1 -1
  46. data/lib/hecks/bluebook/ask_resolution.rb +130 -0
  47. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +8 -2
  48. data/lib/hecks/bluebook/assembly/contracts/rules.rb +4 -2
  49. data/lib/hecks/bluebook/assembly/contracts/structure.rb +3 -1
  50. data/lib/hecks/bluebook/assembly/marks.rb +1 -0
  51. data/lib/hecks/bluebook/attribute.rb +1 -1
  52. data/lib/hecks/bluebook/behaviour/command.rb +13 -8
  53. data/lib/hecks/bluebook/behaviour/policy.rb +30 -0
  54. data/lib/hecks/bluebook/capabilities/attribute_values.rb +72 -0
  55. data/lib/hecks/bluebook/capabilities.rb +91 -9
  56. data/lib/hecks/bluebook/domain_port.rb +16 -4
  57. data/lib/hecks/bluebook/dsl/adapter_builder.rb +18 -6
  58. data/lib/hecks/bluebook/dsl/binding_proxy.rb +13 -0
  59. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/computed_sources.rb +98 -0
  60. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/fan_out_rows.rb +42 -0
  61. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/projected_fields.rb +14 -2
  62. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/provisions.rb +69 -3
  63. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/with_projections.rb +17 -10
  64. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +5 -0
  65. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
  66. data/lib/hecks/bluebook/dsl/command_builder/mutations.rb +8 -1
  67. data/lib/hecks/bluebook/dsl/command_builder/operands.rb +121 -0
  68. data/lib/hecks/bluebook/dsl/command_builder.rb +2 -0
  69. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +10 -1
  70. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +45 -2
  71. data/lib/hecks/bluebook/dsl/policy_builder.rb +47 -10
  72. data/lib/hecks/bluebook/dsl/rule_reference.rb +7 -4
  73. data/lib/hecks/bluebook/dsl/world_builder.rb +1 -1
  74. data/lib/hecks/bluebook/expression/ast_json/emitters.rb +6 -0
  75. data/lib/hecks/bluebook/expression/ast_json.rb +9 -6
  76. data/lib/hecks/bluebook/expression/ast_reader.rb +14 -9
  77. data/lib/hecks/bluebook/expression/projection.json +36 -0
  78. data/lib/hecks/bluebook/expression/resolver/operations.rb +31 -0
  79. data/lib/hecks/bluebook/expression/resolver/parsing.rb +38 -5
  80. data/lib/hecks/bluebook/expression/resolver/scanning.rb +22 -5
  81. data/lib/hecks/bluebook/expression/resolver/values.rb +10 -0
  82. data/lib/hecks/bluebook/expression/resolver.rb +20 -1
  83. data/lib/hecks/bluebook/hecksagon.rb +23 -3
  84. data/lib/hecks/bluebook/lifecycle.rb +22 -3
  85. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +13 -1
  86. data/lib/hecks/bluebook/meta_validator/modes.rb +4 -1
  87. data/lib/hecks/bluebook/meta_validator/readings.rb +18 -1
  88. data/lib/hecks/bluebook/meta_validator/reconstruction/heads.rb +2 -1
  89. data/lib/hecks/bluebook/meta_validator/shapes.rb +5 -0
  90. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +1 -1
  91. data/lib/hecks/bluebook/meta_validator.rb +12 -12
  92. data/lib/hecks/bluebook/model_check/ask_checks.rb +71 -0
  93. data/lib/hecks/bluebook/model_check/policy_checks.rb +2 -1
  94. data/lib/hecks/bluebook/model_check/reference_checks.rb +70 -0
  95. data/lib/hecks/bluebook/model_check.rb +8 -2
  96. data/lib/hecks/bluebook/policy.rb +4 -2
  97. data/lib/hecks/bluebook/smoke_test.rb +1 -1
  98. data/lib/hecks/bluebook/synthesizer.rb +2 -0
  99. data/lib/hecks/bluebook.rb +1 -0
  100. data/lib/hecks/cli/command_table.rb +1 -1
  101. data/lib/hecks/cli/console.rb +2 -1
  102. data/lib/hecks/cli/document.rb +1 -1
  103. data/lib/hecks/cli/launcher_words.rb +4 -4
  104. data/lib/hecks/cli/mcp.rb +4 -4
  105. data/lib/hecks/cli/project_cli/launcher_source.rb +2 -2
  106. data/lib/hecks/cli/project_cli/templates.rb +5 -5
  107. data/lib/hecks/cli/project_cli.rb +9 -6
  108. data/lib/hecks/cli/run.rb +2 -2
  109. data/lib/hecks/cli/smoke_http/checks.rb +4 -1
  110. data/lib/hecks/cli/smoke_http/transport.rb +3 -3
  111. data/lib/hecks/cli/smoke_http.rb +3 -0
  112. data/lib/hecks/codemod.rb +5 -2
  113. data/lib/hecks/corpus/routes.rb +7 -0
  114. data/lib/hecks/corpus/rust.rb +3 -2
  115. data/lib/hecks/deploy/bluebook/deploy.bluebook +18 -2
  116. data/lib/hecks/doc/reference/rendering.rb +6 -3
  117. data/lib/hecks/forms/app/commands.rb +8 -7
  118. data/lib/hecks/forms/app/queries.rb +2 -1
  119. data/lib/hecks/forms/app.rb +1 -1
  120. data/lib/hecks/forms/record_renderer.rb +1 -1
  121. data/lib/hecks/framework/bluebook/privacy.bluebook +11 -0
  122. data/lib/hecks/fuzzing/concurrent_dispatch.rb +4 -1
  123. data/lib/hecks/fuzzing/failure_triage.rb +2 -1
  124. data/lib/hecks/fuzzing/mutation/run.rb +11 -4
  125. data/lib/hecks/fuzzing/properties/catalog.rb +6 -4
  126. data/lib/hecks/fuzzing/properties/guaranteed_by_construction.rb +18 -11
  127. data/lib/hecks/fuzzing/properties/guards.rb +1 -1
  128. data/lib/hecks/fuzzing/self_consistency/saga_redelivery.rb +1 -1
  129. data/lib/hecks/fuzzing/self_consistency.rb +6 -2
  130. data/lib/hecks/fuzzing/shrinker.rb +3 -1
  131. data/lib/hecks/fuzzing/value_generator.rb +19 -5
  132. data/lib/hecks/gate/stages.yml +4 -10
  133. data/lib/hecks/grammar/evolve.rb +1 -1
  134. data/lib/hecks/grammar/expression_operators.json +372 -0
  135. data/lib/hecks/grammar/operators.rb +3 -1
  136. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +10 -10
  137. data/lib/hecks/hecks/adapters/codebase/inspection/rubycritic.rb +35 -0
  138. data/lib/hecks/hecks/adapters/codebase/inspection.rb +104 -0
  139. data/lib/hecks/hecks/adapters/codebase/publishing.rb +2 -1
  140. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +12 -2
  141. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +2 -1
  142. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +1 -1
  143. data/lib/hecks/hecks/adapters/cost_explorer.rb +8 -5
  144. data/lib/hecks/hecks/adapters/deploy_toolchain/statuses.rb +2 -1
  145. data/lib/hecks/hecks/adapters/finding_github.rb +13 -6
  146. data/lib/hecks/hecks/adapters/first_admin.rb +2 -2
  147. data/lib/hecks/hecks/adapters/github_rulesets.rb +6 -3
  148. data/lib/hecks/hecks/adapters/in_process_boot.rb +1 -1
  149. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +1 -1
  150. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +10 -20
  151. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +28 -5
  152. data/lib/hecks/hecks/adapters/local_files.rb +2 -2
  153. data/lib/hecks/hecks/adapters/rehearsal/audit_report.rb +107 -0
  154. data/lib/hecks/hecks/adapters/rehearsal/census.rb +71 -0
  155. data/lib/hecks/hecks/adapters/rehearsal/declaration.rb +133 -0
  156. data/lib/hecks/hecks/adapters/rehearsal/environment.rb +83 -0
  157. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +142 -0
  158. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +96 -0
  159. data/lib/hecks/hecks/adapters/rehearsal/postgres_census.rb +100 -0
  160. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +69 -0
  161. data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +129 -0
  162. data/lib/hecks/hecks/adapters/rehearsal/rows.rb +41 -0
  163. data/lib/hecks/hecks/adapters/rehearsal/rust_audit.rb +83 -0
  164. data/lib/hecks/hecks/adapters/rehearsal/subject.rb +104 -0
  165. data/lib/hecks/hecks/adapters/rehearsal.rb +54 -0
  166. data/lib/hecks/hecks/adapters/rehearsal_store.adapter +7 -0
  167. data/lib/hecks/hecks/adapters/rehearsal_store.rb +81 -0
  168. data/lib/hecks/hecks/adapters/terminal.rb +13 -11
  169. data/lib/hecks/hecks/codebase.behaviors +13 -1
  170. data/lib/hecks/hecks/codebase.bluebook +239 -5
  171. data/lib/hecks/hecks/custodian.behaviors +24 -6
  172. data/lib/hecks/hecks/custodian.bluebook +6 -1227
  173. data/lib/hecks/hecks/deploy.behaviors +1 -1
  174. data/lib/hecks/hecks/deploy.world +1 -3
  175. data/lib/hecks/hecks/hecks.behaviors +1 -1
  176. data/lib/hecks/hecks/hecks.hecksagon +53 -18
  177. data/lib/hecks/hecks/hecks.world +10 -10
  178. data/lib/hecks/hecks/rehearsal.bluebook +477 -0
  179. data/lib/hecks/hecks/site.behaviors +1 -1
  180. data/lib/hecks/hecks/site.world +0 -1
  181. data/lib/hecks/hecks/tooling.bluebook +1238 -0
  182. data/lib/hecks/indifferent_key.rb +20 -0
  183. data/lib/hecks/language/adapter.bluebook +13 -3
  184. data/lib/hecks/language/bluebook/aggregate.bluebook +23 -0
  185. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -0
  186. data/lib/hecks/language/bluebook/entity.bluebook +18 -0
  187. data/lib/hecks/language/bluebook/policy.bluebook +9 -1
  188. data/lib/hecks/language/bluebook/query.bluebook +1 -1
  189. data/lib/hecks/language/bluebook/vocabulary.bluebook +11 -4
  190. data/lib/hecks/language/hecksagon/domain_port.bluebook +1 -1
  191. data/lib/hecks/language/hecksagon/hecksagon.bluebook +22 -0
  192. data/lib/hecks/language/oidc.json +10 -0
  193. data/lib/hecks/language/port.bluebook +4 -4
  194. data/lib/hecks/literal.rb +12 -3
  195. data/lib/hecks/ports/key_vault.rb +7 -2
  196. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +21 -10
  197. data/lib/hecks/ports/persistence/plugins/era/postgres_era/boot.rb +4 -2
  198. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +2 -1
  199. data/lib/hecks/ports/persistence/repository_factory.rb +6 -3
  200. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +29 -0
  201. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +1 -1
  202. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  203. data/lib/hecks/projections/deploy/shared.rb +5 -2
  204. data/lib/hecks/projections/model/deviations.rb +2 -2
  205. data/lib/hecks/projections/model.rb +2 -2
  206. data/lib/hecks/projections/oidc.rb +13 -1
  207. data/lib/hecks/projections/shape.rb +5 -2
  208. data/lib/hecks/projections/site/cms_editor/attributes.rb +26 -6
  209. data/lib/hecks/projections/site/cms_editor/blocks.rb +157 -0
  210. data/lib/hecks/projections/site/cms_editor/checks.rb +14 -1
  211. data/lib/hecks/projections/site/cms_editor/clearing.rb +70 -0
  212. data/lib/hecks/projections/site/cms_editor/color.rb +124 -0
  213. data/lib/hecks/projections/site/cms_editor/composition.rb +42 -0
  214. data/lib/hecks/projections/site/cms_editor/destructive.rb +61 -0
  215. data/lib/hecks/projections/site/cms_editor/drafts.rb +102 -0
  216. data/lib/hecks/projections/site/cms_editor/hints.rb +74 -0
  217. data/lib/hecks/projections/site/cms_editor/icons.rb +86 -0
  218. data/lib/hecks/projections/site/cms_editor/key_kinds.rb +31 -0
  219. data/lib/hecks/projections/site/cms_editor/limits.rb +72 -0
  220. data/lib/hecks/projections/site/cms_editor/media.rb +1 -1
  221. data/lib/hecks/projections/site/cms_editor/moment.rb +34 -0
  222. data/lib/hecks/projections/site/cms_editor/palette.rb +115 -0
  223. data/lib/hecks/projections/site/cms_editor/pickers.rb +85 -0
  224. data/lib/hecks/projections/site/cms_editor/preview.rb +91 -0
  225. data/lib/hecks/projections/site/cms_editor/relations.rb +92 -0
  226. data/lib/hecks/projections/site/cms_editor/scale_checks.rb +66 -0
  227. data/lib/hecks/projections/site/cms_editor/scheduling.rb +150 -0
  228. data/lib/hecks/projections/site/cms_editor/schema.rb +96 -19
  229. data/lib/hecks/projections/site/cms_editor/setting.rb +52 -9
  230. data/lib/hecks/projections/site/cms_editor/targets.rb +86 -0
  231. data/lib/hecks/projections/site/cms_editor/templates/package.json.tmpl +18 -1
  232. data/lib/hecks/projections/site/cms_editor/templates/src/app.ts.tmpl +251 -57
  233. data/lib/hecks/projections/site/cms_editor/templates/src/assets.ts.tmpl +98 -0
  234. data/lib/hecks/projections/site/cms_editor/templates/src/auth/membership.ts.tmpl +32 -24
  235. data/lib/hecks/projections/site/cms_editor/templates/src/browser/app.css.tmpl +412 -0
  236. data/lib/hecks/projections/site/cms_editor/templates/src/browser/autosave.js.tmpl +144 -0
  237. data/lib/hecks/projections/site/cms_editor/templates/src/browser/blocks.js.tmpl +500 -0
  238. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_doc.js.tmpl +175 -0
  239. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_editor.js.tmpl +418 -0
  240. data/lib/hecks/projections/site/cms_editor/templates/src/browser/epoch.js.tmpl +61 -0
  241. data/lib/hecks/projections/site/cms_editor/templates/src/browser/forms.js.tmpl +210 -0
  242. data/lib/hecks/projections/site/cms_editor/templates/src/browser/lists.js.tmpl +40 -0
  243. data/lib/hecks/projections/site/cms_editor/templates/src/browser/main.js.tmpl +26 -0
  244. data/lib/hecks/projections/site/cms_editor/templates/src/browser/media_picker.js.tmpl +232 -0
  245. data/lib/hecks/projections/site/cms_editor/templates/src/browser/moments.js.tmpl +73 -0
  246. data/lib/hecks/projections/site/cms_editor/templates/src/browser/pickers.js.tmpl +152 -0
  247. data/lib/hecks/projections/site/cms_editor/templates/src/browser/preview.js.tmpl +98 -0
  248. data/lib/hecks/projections/site/cms_editor/templates/src/browser/shell.js.tmpl +140 -0
  249. data/lib/hecks/projections/site/cms_editor/templates/src/browser/toast.js.tmpl +66 -0
  250. data/lib/hecks/projections/site/cms_editor/templates/src/choices.ts.tmpl +99 -0
  251. data/lib/hecks/projections/site/cms_editor/templates/src/commands.ts.tmpl +78 -24
  252. data/lib/hecks/projections/site/cms_editor/templates/src/config.ts.tmpl +6 -0
  253. data/lib/hecks/projections/site/cms_editor/templates/src/flash.ts.tmpl +52 -0
  254. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +19 -2
  255. data/lib/hecks/projections/site/cms_editor/templates/src/media/handler.ts.tmpl +31 -8
  256. data/lib/hecks/projections/site/cms_editor/templates/src/names.ts.tmpl +36 -0
  257. data/lib/hecks/projections/site/cms_editor/templates/src/schema.ts.tmpl +131 -2
  258. data/lib/hecks/projections/site/cms_editor/templates/src/ui/availability.ts.tmpl +15 -0
  259. data/lib/hecks/projections/site/cms_editor/templates/src/ui/block_list.js.tmpl +213 -0
  260. data/lib/hecks/projections/site/cms_editor/templates/src/ui/blocks.ts.tmpl +275 -0
  261. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_model.js.tmpl +8 -5
  262. data/lib/hecks/projections/site/cms_editor/templates/src/ui/detail.ts.tmpl +88 -0
  263. data/lib/hecks/projections/site/cms_editor/templates/src/ui/dialogs.ts.tmpl +43 -0
  264. data/lib/hecks/projections/site/cms_editor/templates/src/ui/fields.ts.tmpl +229 -34
  265. data/lib/hecks/projections/site/cms_editor/templates/src/ui/html.ts.tmpl +40 -81
  266. data/lib/hecks/projections/site/cms_editor/templates/src/ui/icons.ts.tmpl +15 -0
  267. data/lib/hecks/projections/site/cms_editor/templates/src/ui/input.ts.tmpl +29 -12
  268. data/lib/hecks/projections/site/cms_editor/templates/src/ui/links.ts.tmpl +100 -0
  269. data/lib/hecks/projections/site/cms_editor/templates/src/ui/moments.ts.tmpl +44 -0
  270. data/lib/hecks/projections/site/cms_editor/templates/src/ui/outline.ts.tmpl +53 -17
  271. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pages.ts.tmpl +178 -88
  272. data/lib/hecks/projections/site/cms_editor/templates/src/ui/paging.ts.tmpl +82 -0
  273. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pickers.ts.tmpl +110 -0
  274. data/lib/hecks/projections/site/cms_editor/templates/src/ui/preview.ts.tmpl +87 -0
  275. data/lib/hecks/projections/site/cms_editor/templates/src/ui/related.ts.tmpl +97 -0
  276. data/lib/hecks/projections/site/cms_editor/templates/src/ui/schedule.ts.tmpl +160 -0
  277. data/lib/hecks/projections/site/cms_editor/templates/src/ui/shell.ts.tmpl +211 -0
  278. data/lib/hecks/projections/site/cms_editor/templates/src/ui/table.ts.tmpl +151 -0
  279. data/lib/hecks/projections/site/cms_editor/templates/src/ui/theme.js.tmpl +16 -0
  280. data/lib/hecks/projections/site/cms_editor/templates/src/ui/values.ts.tmpl +69 -0
  281. data/lib/hecks/projections/site/cms_editor/templates/src/ui/words.ts.tmpl +97 -0
  282. data/lib/hecks/projections/site/cms_editor/templates/src/ui/world.ts.tmpl +35 -0
  283. data/lib/hecks/projections/site/cms_editor/templates/tsconfig.json.tmpl +2 -1
  284. data/lib/hecks/projections/site/cms_editor/theme.rb +29 -0
  285. data/lib/hecks/projections/site/cms_editor/theme_checks.rb +57 -0
  286. data/lib/hecks/projections/site/cms_editor.rb +49 -23
  287. data/lib/hecks/projections/site/role_probe.rb +2 -0
  288. data/lib/hecks/projector/cli_audience.rb +2 -1
  289. data/lib/hecks/projector/cli_projector/listing.rb +1 -1
  290. data/lib/hecks/projector/cli_projector/option_specs.rb +1 -1
  291. data/lib/hecks/projector/docs_projector/closing.rb +1 -1
  292. data/lib/hecks/projector/docs_projector.rb +1 -1
  293. data/lib/hecks/projector/exporter/bindings.rb +40 -10
  294. data/lib/hecks/projector/exporter/capability_values.rb +55 -0
  295. data/lib/hecks/projector/narrate_projector/reactions.rb +1 -1
  296. data/lib/hecks/projector/target.rb +11 -0
  297. data/lib/hecks/projector.rb +32 -5
  298. data/lib/hecks/quality_control/adapters/github_checks.rb +12 -8
  299. data/lib/hecks/quality_control/adapters/github_issues.rb +8 -4
  300. data/lib/hecks/quality_control/cli/qa_open_pr.rb +2 -2
  301. data/lib/hecks/quality_control/cli/qa_sweep/checks/shrinking.rb +1 -1
  302. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +1 -1
  303. data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
  304. data/lib/hecks/quality_control/quality_control.bluebook +6 -2
  305. data/lib/hecks/release/approval.rb +64 -0
  306. data/lib/hecks/router/{door.rb → aggregate_reader.rb} +5 -4
  307. data/lib/hecks/router/namespace_installer.rb +16 -16
  308. data/lib/hecks/router.rb +1 -1
  309. data/lib/hecks/runtime/best_effort.rb +22 -0
  310. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -2
  311. data/lib/hecks/runtime/command_interpreter.rb +3 -2
  312. data/lib/hecks/runtime/command_rules/emission.rb +1 -1
  313. data/lib/hecks/runtime/dispatcher.rb +4 -4
  314. data/lib/hecks/runtime/entity_element/location.rb +1 -1
  315. data/lib/hecks/runtime/event.rb +5 -0
  316. data/lib/hecks/runtime/identity.rb +7 -6
  317. data/lib/hecks/runtime/interpreting.rb +2 -1
  318. data/lib/hecks/runtime/invocation_builder.rb +1 -1
  319. data/lib/hecks/runtime/loader/boot_steps.rb +8 -5
  320. data/lib/hecks/runtime/loader.rb +10 -10
  321. data/lib/hecks/runtime/policy_interpreter/arguments.rb +7 -4
  322. data/lib/hecks/runtime/policy_interpreter/fan_out.rb +3 -3
  323. data/lib/hecks/runtime/policy_interpreter.rb +6 -6
  324. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -1
  325. data/lib/hecks/runtime/reaction_invocation.rb +1 -1
  326. data/lib/hecks/runtime/rebuild_sweep.rb +11 -1
  327. data/lib/hecks/runtime/registry/ask_verification.rb +35 -0
  328. data/lib/hecks/runtime/registry/hecksagon_rules.rb +10 -0
  329. data/lib/hecks/runtime/registry/merging.rb +1 -1
  330. data/lib/hecks/runtime/registry/providers.rb +16 -0
  331. data/lib/hecks/runtime/registry/read_repository.rb +6 -4
  332. data/lib/hecks/runtime/registry/verification.rb +4 -1
  333. data/lib/hecks/runtime/registry/world_defaults.rb +29 -0
  334. data/lib/hecks/runtime/registry.rb +1 -1
  335. data/lib/hecks/runtime/saga_interpreter/compensation.rb +1 -1
  336. data/lib/hecks/runtime/saga_interpreter/delivery.rb +4 -4
  337. data/lib/hecks/runtime/saga_interpreter/transitions.rb +1 -1
  338. data/lib/hecks/runtime/saga_interpreter.rb +3 -3
  339. data/lib/hecks/runtime/tenant_check.rb +5 -4
  340. data/lib/hecks/runtime/value/coercion.rb +2 -2
  341. data/lib/hecks/runtime/value/validation.rb +16 -4
  342. data/lib/hecks/runtime.rb +9 -9
  343. data/lib/hecks/rust_build/project_rust/target_ir.rb +1 -1
  344. data/lib/hecks/rust_build/wasm.rb +1 -1
  345. data/lib/hecks/rust_build/wasm_browser.rb +1 -1
  346. data/lib/hecks/rust_build.rb +11 -0
  347. data/lib/hecks/served_options.rb +53 -0
  348. data/lib/hecks/site/bluebook/site.bluebook +1 -0
  349. data/lib/hecks/storehouse/audit.rb +1 -1
  350. data/lib/hecks/storehouse/dispatching.rb +8 -6
  351. data/lib/hecks/storehouse/introspection.rb +2 -2
  352. data/lib/hecks/storehouse/querying.rb +6 -4
  353. data/lib/hecks/storehouse.rb +4 -4
  354. data/lib/hecks/tools/comment_style/source_file.rb +4 -1
  355. data/lib/hecks/tools/deploy_recipe_lint/fixtures.rb +4 -1
  356. data/lib/hecks/tools/lanes/rows.rb +6 -0
  357. data/lib/hecks/tools/oidc_manifests.rb +4 -5
  358. data/lib/hecks/tools/regeneration_run.rb +11 -4
  359. data/lib/hecks/tools/site_routes/editor_files.rb +12 -5
  360. data/lib/hecks/tools/tenant_provisioning.rb +2 -2
  361. data/lib/hecks/tools/tools_doc.rb +4 -4
  362. data/lib/hecks/version.rb +1 -1
  363. data/lib/hecks/vocabulary.rb +4 -4
  364. data/lib/hecks.rb +15 -15
  365. data/lib/rubocop/cop/hecks/env_dump.rb +69 -0
  366. data/lib/rubocop/cop/hecks/monotonic_duration.rb +53 -0
  367. data/lib/rubocop/cop/hecks/no_wall_clock_in_runtime.rb +34 -0
  368. data/lib/rubocop/cop/hecks/raw_shell_interpolation.rb +79 -0
  369. data/lib/rubocop/cop/hecks/sleep_in_spec.rb +34 -0
  370. data/lib/rubocop/cop/hecks/swallowed_rescue.rb +37 -0
  371. data/lib/rubocop/cop/hecks/symbol_string_key_mix.rb +50 -0
  372. data/lib/rubocop/cop/hecks/untyped_raise.rb +37 -0
  373. data/prebuilt/05c358d38d661a3774f813d49c66f5b68939af94432d66257a56f49ee2598994.marshal +0 -0
  374. data/prebuilt/26c40c61f09b09b11651c64668eda3ceb180f83cf2a3802d3033d2fd99ac841b.marshal +0 -0
  375. data/prebuilt/verdicts-230ac18c25abfd3d92ede032bee2661a4c2cd2773ab0cf0e2ae52546ba2f7af9.json +1 -0
  376. data/rust/.gitignore +6 -0
  377. data/rust/Cargo.toml +6 -0
  378. data/rust/build/src/binding_pass.rs +103 -0
  379. data/rust/build/src/cargo_sync.rs +6 -0
  380. data/rust/build/src/lineage_pass.rs +3 -3
  381. data/rust/build/src/main.rs +17 -5
  382. data/rust/build/src/pipeline.rs +48 -5
  383. data/rust/build/src/seams_pass.rs +123 -0
  384. data/rust/build/src/translation_pass.rs +167 -0
  385. data/rust/codegen/src/asks.rs +103 -0
  386. data/rust/codegen/src/commands.rs +13 -8
  387. data/rust/codegen/src/constraints.rs +25 -0
  388. data/rust/codegen/src/domain_generator.rs +2 -0
  389. data/rust/codegen/src/expr_emitter.rs +13 -0
  390. data/rust/codegen/src/json_codec.rs +54 -15
  391. data/rust/codegen/src/main.rs +1 -0
  392. data/rust/codegen/src/mutations.rs +128 -3
  393. data/rust/codegen/src/queries.rs +11 -2
  394. data/rust/codegen/src/reactions.rs +1 -1
  395. data/rust/codegen/src/types.rs +166 -8
  396. data/rust/host/HECKS_RELEASE +1 -1
  397. data/rust/host/src/api.rs +4 -4
  398. data/rust/host/src/bin/validate_harness.rs +43 -0
  399. data/rust/host/src/boundary_fuzz/expr_json.rs +2 -2
  400. data/rust/host/src/boundary_fuzz/ir.rs +1 -0
  401. data/rust/host/src/boundary_fuzz/server.rs +1 -1
  402. data/rust/host/src/checkout.rs +35 -19
  403. data/rust/host/src/commerce_ir.rs +408 -2
  404. data/rust/host/src/expr_json.rs +118 -0
  405. data/rust/host/src/mint.rs +1 -1
  406. data/rust/host/src/payments/tests.rs +27 -1
  407. data/rust/host/src/payments.rs +12 -4
  408. data/rust/host/src/query_step.rs +22 -4
  409. data/rust/host/src/reference_validate.rs +653 -108
  410. data/rust/host/src/resend.rs +2 -0
  411. data/rust/host/src/server.rs +19 -6
  412. data/rust/host/src/ui_schema.rs +132 -5
  413. data/rust/host/src/web/newsletter.rs +7 -8
  414. data/rust/host/src/web/newsletter_send.rs +38 -13
  415. data/rust/host/src/web/registrations.rs +229 -26
  416. data/rust/host/src/web.rs +3 -3
  417. data/rust/parser/src/emit.rs +38 -6
  418. data/rust/parser/src/expr/ast_json.rs +7 -1
  419. data/rust/parser/src/expr/resolver.rs +171 -6
  420. data/rust/parser/src/ir.rs +7 -0
  421. data/rust/parser/src/keywords.rs +24 -0
  422. data/rust/parser/src/main.rs +3 -0
  423. data/rust/parser/src/parse/aggregate.rs +2 -1
  424. data/rust/parser/src/parse/chapter.rs +2 -2
  425. data/rust/parser/src/parse/command.rs +2 -0
  426. data/rust/parser/src/parse/hecksagon.rs +5 -2
  427. data/rust/parser/src/parse/lifecycle.rs +81 -0
  428. data/rust/parser/src/parse/mod.rs +15 -1
  429. data/rust/parser/src/parse/policy.rs +13 -2
  430. data/rust/parser/src/parse/value_object.rs +13 -8
  431. data/rust/parser/src/ruby_value.rs +1 -1
  432. data/rust/src/exemplar/commands.rs +4 -4
  433. data/rust/src/exemplar/constraints.rs +1 -1
  434. data/rust/src/exemplar/json.rs +43 -1
  435. data/rust/src/kernel/cli.rs +7 -5
  436. data/rust/src/kernel/dispatch.rs +2 -2
  437. data/rust/src/kernel/dispatch_guard_tests.rs +1 -1
  438. data/rust/src/kernel/expr.rs +66 -3
  439. data/rust/src/kernel/expression_operators/arithmetic.rs +130 -5
  440. data/rust/src/kernel/expression_operators/text.rs +26 -2
  441. data/rust/src/kernel/json.rs +15 -0
  442. data/rust/src/kernel/mod.rs +1 -1
  443. data/rust/src/kernel/orchestrate.rs +54 -5
  444. data/rust/src/kernel/orchestrate_guard_tests.rs +67 -0
  445. data/rust/src/kernel/pattern.rs +60 -0
  446. data/rust/src/kernel/reference_lookup.rs +90 -7
  447. metadata +140 -38
  448. data/lib/hecks/doors/cli_door/nesting.rb +0 -34
  449. data/lib/hecks/doors/cli_door/shorthand.rb +0 -85
  450. data/lib/hecks/doors/cli_door.rb +0 -124
  451. data/lib/hecks/doors/cli_runner/answers.rb +0 -138
  452. data/lib/hecks/doors/cli_runner/resolution.rb +0 -146
  453. data/lib/hecks/doors/cli_runner/settling.rb +0 -115
  454. data/lib/hecks/doors/cli_runner/streaming.rb +0 -90
  455. data/lib/hecks/doors/cli_runner/suggestions.rb +0 -43
  456. data/lib/hecks/doors/cli_runner.rb +0 -66
  457. data/lib/hecks/doors/command_request.rb +0 -119
  458. data/lib/hecks/doors/handle.rb +0 -208
  459. data/lib/hecks/doors/json_door.rb +0 -132
  460. data/lib/hecks/doors/launcher_options.rb +0 -247
  461. data/lib/hecks/doors/mcp_door/properties.rb +0 -47
  462. data/lib/hecks/doors/mcp_door/tool_calls.rb +0 -113
  463. data/lib/hecks/doors/mcp_door/tool_definitions/auditing.rb +0 -51
  464. data/lib/hecks/doors/mcp_door/tool_definitions/calling.rb +0 -69
  465. data/lib/hecks/doors/mcp_door/tool_definitions/reading.rb +0 -97
  466. data/lib/hecks/doors/mcp_door/tool_definitions.rb +0 -15
  467. data/lib/hecks/doors/mcp_door.rb +0 -156
  468. data/lib/hecks/doors/mcp_door_scope/argument_policy.rb +0 -92
  469. data/lib/hecks/doors/mcp_door_scope/env_problems.rb +0 -55
  470. data/lib/hecks/doors/mcp_door_scope/presenter.rb +0 -78
  471. data/lib/hecks/doors/mcp_door_scope.rb +0 -201
  472. data/lib/hecks/doors/mcp_guide_cache.rb +0 -75
  473. data/lib/hecks/doors/ruby_door/aggregate_door.rb +0 -162
  474. data/lib/hecks/doors/ruby_door/chapter.rb +0 -89
  475. data/lib/hecks/doors/ruby_door.rb +0 -80
  476. data/lib/hecks/doors/usage_cache.rb +0 -139
  477. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_widget.js.tmpl +0 -282
  478. data/lib/hecks/projections/site/cms_editor/templates/src/ui/media_picker.js.tmpl +0 -109
  479. data/prebuilt/c28d6d9db14eea3e17161e9d9c262efee30b256cf97693a7cfd552bce24b2f7f.marshal +0 -0
  480. data/prebuilt/cb375cba61ca0dbe8ddb9739c306510fe05e5f4b7a9b6955caff8224c63ea0e0.marshal +0 -0
  481. data/prebuilt/verdicts-d6d584ad43b5f3cebf88aafbbdbf024c38574acf18e06adffbc555f7786b1c2f.json +0 -1
@@ -2,16 +2,21 @@ __BANNER__
2
2
 
3
3
  // Running one command and judging what came of it. The host answers HTTP 200 whether or not the
4
4
  // domain refused, and its `refusals` list can carry entries replayed from history, so the outcome is
5
- // judged by the state that comes back: the instance has to show the change. Only when the state did
6
- // not change at all (a repeated command, a lifecycle move to the state it is already in) does the
7
- // answer's last refusal decide, and only if it names this verb.
5
+ // judged by the state that comes back, never by what was sent: a command may set fields from
6
+ // arguments that are not fields (a clearing argument), so the arguments are no evidence. The command
7
+ // took effect when the instance's state changed, and for a lifecycle move when it reached the state
8
+ // the move names. When the state did not change at all (a repeated command, a move to the state it
9
+ // is already in) the answer's last refusal decides, and only if it names this verb.
8
10
  import type { Answer, HostClient } from "@hecks/client";
9
11
  import { DomainUnavailable } from "@hecks/client";
10
12
 
13
+ import { qualified } from "./names.ts";
11
14
  import type { Aggregate, Command } from "./schema.ts";
15
+ import { problemsOf } from "./ui/block_list.js";
16
+ import type { Problem } from "./ui/blocks.ts";
12
17
 
13
18
  export type State = Record<string, unknown>;
14
- export type Outcome = { ok: true; id: string } | { ok: false; message: string };
19
+ export type Outcome = { ok: true; id: string } | { ok: false; message: string; field?: string; problems?: Problem[] };
15
20
 
16
21
  /** The text an identity value holds: the part of a value object, or the plain value. */
17
22
  export function leaf(value: unknown): string | null {
@@ -24,40 +29,86 @@ export const idOf = (agg: Aggregate, state: State | undefined): string | null =>
24
29
 
25
30
  /** The state of one instance in an answer, or undefined when the answer holds none by that id. */
26
31
  export function instanceIn(client: HostClient, answer: Answer, agg: Aggregate, id: string): State | undefined {
27
- return client.instancesOf(answer, agg.name).find(([key]) => key === id)?.[1];
32
+ return client.instancesOf(answer, qualified(agg)).find(([key]) => key === id)?.[1];
28
33
  }
29
34
 
30
- /** True when everything `sent` carries is in `held`; numbers and their text compare equal. */
31
- export function covers(sent: unknown, held: unknown): boolean {
32
- if (Array.isArray(sent)) return Array.isArray(held) && sent.length === held.length && sent.every((item, at) => covers(item, held[at]));
33
- if (sent && typeof sent === "object") {
34
- const keys = Object.keys(sent);
35
- if (!held || typeof held !== "object") return keys.length === 1 && covers((sent as State)[keys[0]], held);
36
- return keys.every((key) => covers((sent as State)[key], (held as State)[key]));
37
- }
38
- return String(sent) === String(held);
35
+ /**
36
+ * A client that asks the host to read once and remembers the answer, for a request that only reads:
37
+ * the page and the navigation's counts then come from one read of the host.
38
+ */
39
+ export function reading(client: HostClient): HostClient {
40
+ let held: Promise<Answer> | undefined;
41
+ return new Proxy(client, {
42
+ get(target, key) {
43
+ if (key === "read") return () => (held ??= target.read());
44
+ const member = Reflect.get(target, key, target);
45
+ return typeof member === "function" ? member.bind(target) : member;
46
+ },
47
+ });
39
48
  }
40
49
 
41
- const sameState = (a: State | undefined, b: State | undefined): boolean => JSON.stringify(a) === JSON.stringify(b);
50
+ /**
51
+ * A value as it is compared: the keys of an object in order and each null left out, since one answer
52
+ * of a host may echo a slot that was never set as null where another leaves it out, in an order of its own.
53
+ */
54
+ function canonical(value: unknown): unknown {
55
+ if (Array.isArray(value)) return value.map(canonical);
56
+ if (value === null || typeof value !== "object") return value;
57
+ const held = Object.entries(value as object).filter(([, part]) => part !== null);
58
+ return Object.fromEntries(held.sort(([a], [b]) => (a < b ? -1 : 1)).map(([key, part]) => [key, canonical(part)]));
59
+ }
60
+
61
+ const sameState = (a: State | undefined, b: State | undefined): boolean => JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));
42
62
 
63
+ /** Whether the host's refusal names this aggregate's verb: by its chapter too when the editor spans several. */
43
64
  function refusedLast(answer: Answer, agg: Aggregate, command: Command): boolean {
44
65
  const last = (answer.refusals ?? []).at(-1);
45
- return last !== undefined && String(last.verb ?? "").replace(/^.*::/, "") === `${agg.name}.${command.name}`;
66
+ if (last === undefined) return false;
67
+ const verb = String(last.verb ?? "");
68
+ return verb.replace(/^.*::/, "") === `${agg.name}.${command.name}` && (!agg.chapter || !verb.includes("::") || verb.startsWith(`${agg.chapter}::`));
46
69
  }
47
70
 
48
71
  /** Whether `after` shows the command took effect, given the instance as it was `before`. */
49
- export function applied(agg: Aggregate, command: Command, args: State, before: State | undefined, after: State | undefined, answer: Answer): boolean {
72
+ export function applied(agg: Aggregate, command: Command, before: State | undefined, after: State | undefined, answer: Answer): boolean {
50
73
  if (!after) return false;
51
74
  if (sameState(before, after)) return !refusedLast(answer, agg, command);
52
75
  const move = agg.lifecycle?.transitions.find((transition) => transition.verb === command.name);
53
- if (move && agg.lifecycle) return after[agg.lifecycle.field] === move.to;
54
- return covers(args, after);
76
+ return move && agg.lifecycle ? after[agg.lifecycle.field] === move.to : true;
77
+ }
78
+
79
+ const label = (name: string): string => name.replace(/_/g, " ");
80
+
81
+ /** The command's attribute a value object's name stands for, to name the offending field. */
82
+ function fieldOf(command: Command, type: string): string {
83
+ const held = command.attributes.find((attr) => attr.type === type);
84
+ return held ? label(held.name) : type;
85
+ }
86
+
87
+ /**
88
+ * The words for a refusal. The host words a given as `<Command> refused — <condition>`, which reads
89
+ * as though the condition were the reason, so it is shown as the condition that was not met; an
90
+ * invariant is shown as the field and the rule, without the value that offended (raw JSON).
91
+ */
92
+ export function refusalText(refusal: { kind?: string; error?: string } | undefined, command: Command): string {
93
+ const error = refusal?.error ?? "";
94
+ const refused = /^.+? refused — (.+)$/s.exec(error);
95
+ if (refusal?.kind === "GivenNotMet" && refused) return `Not allowed unless ${refused[1]}.`;
96
+ if (refusal?.kind === "EnsuresNotMet" && refused) return `Not allowed: the result would break this rule: ${refused[1]}.`;
97
+ const invariant = /^(.+?) invariant violated — (.+?)(?: \(given .*\))?$/s.exec(error);
98
+ if (invariant) return `${fieldOf(command, invariant[1])}: ${invariant[2]}.`;
99
+ if (refusal?.kind === "InvariantViolation" && refused) return `${refused[1]}.`;
100
+ return error || `The domain did not apply ${command.name}.`;
101
+ }
102
+
103
+ /** The name of the command's attribute a refused invariant is about, so its input can be marked; undefined when none. */
104
+ export function refusedField(refusal: { kind?: string; error?: string } | undefined, command: Command): string | undefined {
105
+ const invariant = /^(.+?) invariant violated — /s.exec(refusal?.error ?? "");
106
+ return invariant ? command.attributes.find((attr) => attr.type === invariant[1])?.name : undefined;
55
107
  }
56
108
 
57
109
  /** The words for a command that changed nothing: the host's last refusal, or a plain statement. */
58
110
  export function refusalMessage(answer: Answer, command: Command): string {
59
- const last = (answer.refusals ?? []).at(-1);
60
- return last?.error ?? `The domain did not apply ${command.name}.`;
111
+ return refusalText((answer.refusals ?? []).at(-1), command);
61
112
  }
62
113
 
63
114
  /**
@@ -68,10 +119,13 @@ export async function runCommand(client: HostClient, agg: Aggregate, command: Co
68
119
  try {
69
120
  const id = to ?? idOf(agg, args);
70
121
  const before = id ? instanceIn(client, await client.read(), agg, id) : undefined;
71
- const answer = await client.dispatch(`${agg.name}.${command.name}`, args, to, command.role ?? undefined);
122
+ const answer = await client.dispatch(`${qualified(agg)}.${command.name}`, args, to, command.role ?? undefined);
72
123
  const after = id ? instanceIn(client, answer, agg, id) : undefined;
73
- if (id && applied(agg, command, args, before, after, answer)) return { ok: true, id };
74
- return { ok: false, message: refusalMessage(answer, command) };
124
+ if (id && applied(agg, command, before, after, answer)) return { ok: true, id };
125
+ const last = (answer.refusals ?? []).at(-1);
126
+ const problems: Problem[] = problemsOf(last?.error, command.attributes, args, agg.valueObjects);
127
+ const field = refusedField(last, command);
128
+ return { ok: false, message: refusalMessage(answer, command), field, problems };
75
129
  } catch (err) {
76
130
  if (err instanceof DomainUnavailable) return { ok: false, message: `The domain could not be reached: ${err.message}` };
77
131
  throw err;
@@ -5,17 +5,23 @@ __BANNER__
5
5
 
6
6
  export const EDITOR = {
7
7
  title: __TITLE__,
8
+ brand: __BRAND__,
9
+ logo: __LOGO__,
8
10
  basePath: __BASE_PATH__,
9
11
  ssoPath: __SSO_PATH__,
10
12
  login: __LOGIN__,
11
13
  sessionCookie: __SESSION_COOKIE__,
12
14
  sessionMaxAge: 28800,
15
+ flashMaxAge: 60,
13
16
  hostCookie: __HOST_COOKIE__,
14
17
  hostEnv: __HOST_ENV__,
15
18
  hostDefault: __HOST_DEFAULT__,
16
19
  roles: __ROLES__,
17
20
  mediaDir: __MEDIA_DIR__,
18
21
  mediaMaxBytes: __MEDIA_MAX_BYTES__,
22
+ pageSize: __PAGE_SIZE__,
23
+ /** Where a record is shown on the site, with `{key}`, `{kind}`, `{slug}` and `{id}`; null for no preview. */
24
+ preview: __PREVIEW__ as string | null,
19
25
  accountPath: "/accounts/me",
20
26
  membersPath: "/members",
21
27
  rememberMs: 60000,
@@ -0,0 +1,52 @@
1
+ __BANNER__
2
+
3
+ // A notice that survives one redirect: "Published." after a command succeeds and the person is sent
4
+ // to the page that shows the result. It is carried in a short-lived cookie that is signed (so a
5
+ // visitor cannot make the editor say something), holds only a kind and a sentence, and is read
6
+ // once: the page that shows it clears it in the same response. There is no server-side state.
7
+ // The sentence is escaped wherever it is shown; it is encoded as base64url so no character of it can
8
+ // break out of the cookie.
9
+ import { createHmac, timingSafeEqual } from "node:crypto";
10
+
11
+ import { EDITOR } from "./config.ts";
12
+
13
+ export type FlashKind = "success" | "error" | "info";
14
+
15
+ export interface Flash {
16
+ kind: FlashKind;
17
+ message: string;
18
+ }
19
+
20
+ const SUFFIX = ":editor-flash";
21
+ const NAME = `${EDITOR.sessionCookie}_flash`;
22
+ const ATTRIBUTES = `Path=${EDITOR.basePath}; HttpOnly; SameSite=Lax`;
23
+ const KINDS: readonly string[] = ["success", "error", "info"];
24
+
25
+ const sign = (secret: string, payload: string): string => createHmac("sha256", secret + SUFFIX).update(payload).digest("base64url");
26
+
27
+ /** The `Set-Cookie` value that carries `flash` to the next page. */
28
+ export function flashCookie(flash: Flash, secret: string, now: () => number = Date.now): string {
29
+ const payload = Buffer.from(JSON.stringify({ k: flash.kind, m: flash.message.slice(0, 300), t: Math.floor(now() / 1000) })).toString("base64url");
30
+ const secure = process.env.NODE_ENV === "production" ? "; Secure" : "";
31
+ return `${NAME}=${payload}.${sign(secret, payload)}; ${ATTRIBUTES}; Max-Age=${EDITOR.flashMaxAge}${secure}`;
32
+ }
33
+
34
+ /** The `Set-Cookie` value that spends the notice. */
35
+ export const spentFlash = (): string => `${NAME}=; ${ATTRIBUTES}; Max-Age=0`;
36
+
37
+ /** The notice a `Cookie` header carries, or null when it has none, it was forged or it has expired. */
38
+ export function readFlash(header: string | null, secret: string, now: () => number = Date.now): Flash | null {
39
+ const raw = header?.split(";").map((part) => part.trim()).find((part) => part.startsWith(`${NAME}=`))?.slice(NAME.length + 1);
40
+ const [payload, signature] = raw?.split(".") ?? [];
41
+ if (!payload || !signature || !secret) return null;
42
+ const expected = Buffer.from(sign(secret, payload));
43
+ const given = Buffer.from(signature);
44
+ if (expected.length !== given.length || !timingSafeEqual(expected, given)) return null;
45
+ try {
46
+ const found = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
47
+ const fresh = typeof found.t === "number" && now() / 1000 - found.t <= EDITOR.flashMaxAge && now() / 1000 >= found.t - 5;
48
+ return fresh && KINDS.includes(found.k) && typeof found.m === "string" ? { kind: found.k, message: found.m } : null;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
@@ -4,7 +4,8 @@ __BANNER__
4
4
  // posting to /dispatch from this machine. The host accepts that protocol from a loopback peer only,
5
5
  // which is why this server, not the browser, talks to it; it authenticates each person itself
6
6
  // (auth/) before any of these calls is made on their behalf.
7
- import { HostClient } from "@hecks/client";
7
+ import { DomainRefusal, HostClient, rowsOf } from "@hecks/client";
8
+ import type { Answer } from "@hecks/client";
8
9
 
9
10
  import { EDITOR } from "./config.ts";
10
11
  import { SCHEMA } from "./schema.ts";
@@ -13,6 +14,8 @@ export interface HostOptions {
13
14
  /** The host's address, when not read from the environment variable the Editor row names. */
14
15
  url?: string;
15
16
  fetch?: typeof fetch;
17
+ /** The domain to address, when it is not the chapter the editor edits (pictures kept in another chapter). */
18
+ domain?: string;
16
19
  }
17
20
 
18
21
  /** The host's base URL, without a trailing slash. */
@@ -22,5 +25,19 @@ export function hostUrl(options: HostOptions = {}): string {
22
25
 
23
26
  /** A client for the domain this editor edits. */
24
27
  export function hostClient(options: HostOptions = {}): HostClient {
25
- return new HostClient({ domain: SCHEMA.domain, url: hostUrl(options), fetch: options.fetch, timeoutMs: EDITOR.timeoutMs });
28
+ return new HostClient({ domain: options.domain ?? SCHEMA.domain, url: hostUrl(options), fetch: options.fetch, timeoutMs: EDITOR.timeoutMs });
29
+ }
30
+
31
+ /**
32
+ * The rows a query answered. A host that has no such query for its running domain (a declared
33
+ * query the Rust host does not generate) answers with no entry at all, which `rowsOf` reads as
34
+ * null; that is a refusal here, not an empty list, or a list page, a picker or the picture
35
+ * listing would show nothing while the records are there.
36
+ *
37
+ * @throws {DomainRefusal} when the host refused the question or did not answer it
38
+ */
39
+ export function rowsAnswered(answer: Answer, name: string): Record<string, unknown>[] {
40
+ const rows = rowsOf(answer, name);
41
+ if (rows === null) throw new DomainRefusal("QueryNotAnswered", `The host did not answer ${name}.`);
42
+ return rows as Record<string, unknown>[];
26
43
  }
@@ -11,12 +11,15 @@ __BANNER__
11
11
  // the bytes, and a client-supplied filename is never used.
12
12
  import { createHash } from "node:crypto";
13
13
 
14
- import { DomainRefusal, rowsOf } from "@hecks/client";
14
+ import { DomainRefusal } from "@hecks/client";
15
15
  import type { HostClient } from "@hecks/client";
16
16
 
17
17
  import { instanceIn, leaf, runCommand } from "../commands.ts";
18
18
  import type { State } from "../commands.ts";
19
19
  import { EDITOR } from "../config.ts";
20
+ import { hostClient, rowsAnswered } from "../host.ts";
21
+ import type { HostOptions } from "../host.ts";
22
+ import { mayEdit, qualified } from "../names.ts";
20
23
  import { SCHEMA } from "../schema.ts";
21
24
  import type { Aggregate, Media, MediaField } from "../schema.ts";
22
25
  import { boundaryOf, parts } from "./multipart.ts";
@@ -76,7 +79,14 @@ function picture(storage: MediaStorage, media: Media, state: State) {
76
79
  }
77
80
 
78
81
  function aggregateOf(media: Media): Aggregate {
79
- return SCHEMA.aggregates.find((candidate) => candidate.name === media.aggregate) as Aggregate;
82
+ const own = (candidate: Aggregate): boolean => candidate.name === media.aggregate && (candidate.chapter ?? SCHEMA.domain) === (media.domain ?? SCHEMA.domain);
83
+ return media.definition ?? (SCHEMA.aggregates.find(own) as Aggregate);
84
+ }
85
+
86
+ /** Whether a person in `role` may use the pictures: not when the chapter that holds them limits its roles to others. */
87
+ export function mediaOpen(role: string | null): boolean {
88
+ const media = SCHEMA.media as Media;
89
+ return mayEdit(aggregateOf(media), role);
80
90
  }
81
91
 
82
92
  /** What was uploaded, or the refusal to answer with. */
@@ -113,9 +123,16 @@ async function upload(client: HostClient, storage: MediaStorage, request: Reques
113
123
 
114
124
  async function stateOf(client: HostClient, media: Media): Promise<State[]> {
115
125
  const agg = aggregateOf(media);
116
- if (!media.listing) return client.instancesOf(await client.read(), agg.name).map(([, state]) => state);
117
- const name = `${agg.name}.${media.listing}`;
118
- return (rowsOf(await client.query(name, {}), name) ?? []) as State[];
126
+ const everything = async () => client.instancesOf(await client.read(), qualified(agg)).map(([, state]) => state);
127
+ if (!media.listing) return everything();
128
+ const name = `${qualified(agg)}.${media.listing}`;
129
+ try {
130
+ return rowsAnswered(await client.query(name, {}), name) as State[];
131
+ } catch (err) {
132
+ // The listing only narrows what is offered; a host that does not answer it still holds the pictures.
133
+ if (err instanceof DomainRefusal) return everything();
134
+ throw err;
135
+ }
119
136
  }
120
137
 
121
138
  async function listing(client: HostClient, storage: MediaStorage, media: Media): Promise<Response> {
@@ -142,10 +159,16 @@ async function serve(storage: MediaStorage, key: string): Promise<Response> {
142
159
  });
143
160
  }
144
161
 
145
- /** The handler of everything under `<base>/media`, given the path segments after `media`. */
146
- export function mediaHandler(storage: MediaStorage = diskStorage()) {
162
+ /**
163
+ * The handler of everything under `<base>/media`, given the path segments after `media`. When the
164
+ * pictures are kept in another chapter (`media.domain`), they are read and registered through a
165
+ * client for that chapter, not the editor's own.
166
+ */
167
+ export function mediaHandler(storage: MediaStorage = diskStorage(), options: HostOptions = {}) {
147
168
  const media = SCHEMA.media as Media;
148
- return (client: HostClient, request: Request, rest: string[]): Promise<Response> | Response => {
169
+ const elsewhere = media.domain ? hostClient({ ...options, domain: media.domain }) : null;
170
+ return (editors: HostClient, request: Request, rest: string[]): Promise<Response> | Response => {
171
+ const client = elsewhere ?? editors;
149
172
  if (rest.length === 0 && request.method === "POST") return upload(client, storage, request, media);
150
173
  if (rest.length === 0 && request.method === "GET") return listing(client, storage, media);
151
174
  if (rest.length === 1 && request.method === "GET") return serve(storage, rest[0]);
@@ -0,0 +1,36 @@
1
+ __BANNER__
2
+
3
+ // How an aggregate is named in the three places it has a name: its address in the editor, the name
4
+ // the host knows it by, and the key the navigation and the counts hold it under. An editor of one
5
+ // chapter names an aggregate by its own name in all three. An editor of several chapters names it by
6
+ // its chapter as well, so two aggregates of one name in different chapters stay two.
7
+ import { SCHEMA } from "./schema.ts";
8
+ import type { Aggregate } from "./schema.ts";
9
+
10
+ /** The path segments that address an aggregate under the editor's base path. */
11
+ export const at = (agg: Aggregate): string[] => (agg.chapter ? [agg.chapter, agg.name] : [agg.name]);
12
+
13
+ /** The name the host knows an aggregate by: `Chapter::Aggregate` when the editor spans chapters. */
14
+ export const qualified = (agg: Aggregate): string => (agg.chapter ? `${agg.chapter}::${agg.name}` : agg.name);
15
+
16
+ /** What the navigation and the counts hold an aggregate under. */
17
+ export const keyOf = (agg: Aggregate): string => (agg.chapter ? `${agg.chapter}.${agg.name}` : agg.name);
18
+
19
+ /** The host's name for an aggregate of `chapter` (this editor's first chapter when none is named). */
20
+ export const hostName = (name: string, chapter?: string): string => `${chapter ?? SCHEMA.domain}::${name}`;
21
+
22
+ /**
23
+ * The aggregate a path names, and the segments after it. With several chapters the path is
24
+ * `<Chapter>/<Aggregate>/...`, and with one it is `<Aggregate>/...`.
25
+ */
26
+ export function locate(segments: string[]): { agg: Aggregate | undefined; rest: string[] } {
27
+ if (SCHEMA.chapters) {
28
+ const [chapter, name, ...rest] = segments;
29
+ return { agg: SCHEMA.aggregates.find((candidate) => candidate.chapter === chapter && candidate.name === name), rest };
30
+ }
31
+ const [name, ...rest] = segments;
32
+ return { agg: SCHEMA.aggregates.find((candidate) => candidate.name === name), rest };
33
+ }
34
+
35
+ /** Whether a person in `role` may edit `agg`: every role may, unless its chapter limits them. */
36
+ export const mayEdit = (agg: Aggregate, role: string | null): boolean => !agg.roles || (role !== null && agg.roles.includes(role));
@@ -7,6 +7,44 @@ __BANNER__
7
7
  /** How an attribute's value is entered: `object` is a value object, the rest are single values. */
8
8
  export type Kind = "text" | "integer" | "number" | "boolean" | "object" | "reference";
9
9
 
10
+ /**
11
+ * What an attribute names: an instance of another aggregate, offered as a choice. `strict` pickers
12
+ * accept only a key that exists; the others suggest and accept any text. `listing` is the query of
13
+ * the target that lists what is on offer, or none to offer every instance. With `kinds`, the value is
14
+ * `<kind>:<key>`: the kind is chosen first, then the record of the aggregate that kind names.
15
+ */
16
+ export interface Picker {
17
+ target?: string;
18
+ /** The target's chapter, when the editor spans several or the target is in another. */
19
+ chapter?: string;
20
+ /** The target's identity attribute. */
21
+ key?: string;
22
+ /** The attribute that names an instance to a person (its title, name or label, else its identity). */
23
+ label?: string;
24
+ listing?: string;
25
+ strict?: boolean;
26
+ kinds?: PickerKind[];
27
+ }
28
+
29
+ /** One kind of a `<kind>:<key>` value, and the aggregate it names when it names one. */
30
+ export type PickerKind = Omit<Picker, "kinds"> & { kind: string };
31
+
32
+ /**
33
+ * A list of mixed blocks: a list of one value object whose `discriminator` part is one of `kinds`. With
34
+ * `rules`, each kind's `requires` (parts it must have) and `uses` (parts it may have) come from a
35
+ * declared table; a part no row names belongs to every kind. With none, every part is shown and
36
+ * `mentions` says which parts a kind's rules read. `limits` are the most each list part of the block
37
+ * may hold; `pictures` says, for a value object, which of its parts are a picture's key, alt text and caption.
38
+ */
39
+ export interface BlockList {
40
+ discriminator: string;
41
+ kinds: string[];
42
+ rules?: Record<string, { requires: string[]; uses: string[] }>;
43
+ mentions?: Record<string, string[]>;
44
+ limits?: Record<string, number>;
45
+ pictures?: Record<string, { key: string; alt: string; caption?: string }>;
46
+ }
47
+
10
48
  export interface Attr {
11
49
  name: string;
12
50
  /** The type as the bluebook spells it ("String", "Byline"). */
@@ -15,10 +53,29 @@ export interface Attr {
15
53
  optional: boolean;
16
54
  /** True for `list_of`: the attribute holds a list of `type`. */
17
55
  list: boolean;
18
- /** `body` for an attribute whose value object has the shape of a structured document (blocks of spans). */
19
- widget?: "body";
56
+ /**
57
+ * `body` for an attribute whose value object has the shape of a structured document (blocks of
58
+ * spans); `date` or `datetime` for whole seconds since 1970 that the names say are a moment.
59
+ */
60
+ widget?: "body" | "date" | "datetime";
61
+ /** Set when the attribute names an instance of another aggregate of the editor. */
62
+ picker?: Picker;
63
+ /** The members of the closed set the attribute is one of, when its value object declares one. */
64
+ options?: string[];
20
65
  /** The aggregate a `reference` points at. */
21
66
  target?: string;
67
+ /** Set when the attribute is a list of mixed blocks (see `BlockList`). */
68
+ blockList?: BlockList;
69
+ /** The most rows the list may hold, when a rule of its holder (an invariant or a command's `given`) says. */
70
+ max?: number;
71
+ /** The fewest rows the list may hold, when a rule of its holder says. */
72
+ min?: number;
73
+ /** The most characters a text may hold, when a rule of its holder says. */
74
+ maxLength?: number;
75
+ /** True when `maxLength` is long enough for the text to be written in a box of several lines. */
76
+ multiline?: boolean;
77
+ /** Set when the value object is a picture: the names of its parts that hold the file, its description and its caption. */
78
+ picture?: { key: string; alt: string; caption?: string };
22
79
  }
23
80
 
24
81
  export interface Transition {
@@ -31,6 +88,8 @@ export interface Lifecycle {
31
88
  field: string;
32
89
  default: string;
33
90
  transitions: Transition[];
91
+ /** Each state's badge tone: `neutral`, `ok`, `info`, `warn` or `danger`. */
92
+ tones: Record<string, string>;
34
93
  }
35
94
 
36
95
  export interface Command {
@@ -41,7 +100,11 @@ export interface Command {
41
100
  creates: boolean;
42
101
  /** The aggregate the command acts on, when it is not a creating command. */
43
102
  on: string | null;
103
+ /** True when the editor asks for confirmation before running it (see the generator's `Destructive`). */
104
+ destructive: boolean;
44
105
  attributes: Attr[];
106
+ /** The arguments that only clear a field and are not offered as inputs: sent as empty lists. */
107
+ empty?: string[];
45
108
  }
46
109
 
47
110
  export interface Query {
@@ -50,8 +113,37 @@ export interface Query {
50
113
  attributes: Attr[];
51
114
  }
52
115
 
116
+ /**
117
+ * A record of another aggregate that goes with this one's records, because its identity is a
118
+ * `<kind>:<slug>` key of this aggregate. `shape` says how to show it; `create` and `edit` name the
119
+ * commands that make and change it, when it has them.
120
+ */
121
+ export interface Related {
122
+ aggregate: string;
123
+ chapter?: string;
124
+ /** The related aggregate's identity attribute, which the key fills. */
125
+ field: string;
126
+ shape: "document" | "gallery" | "metadata";
127
+ create?: string;
128
+ edit?: string;
129
+ }
130
+
131
+ /** An aggregate that keeps a draft of its content beside the live one, and the commands that handle it. */
132
+ export interface Drafts {
133
+ /** The draft attribute (`draft_body`, `draft_content`) and the live one it stands for (`body`, `content`). */
134
+ attribute: string;
135
+ live: string;
136
+ save: string;
137
+ publish?: string;
138
+ discard?: string;
139
+ }
140
+
53
141
  export interface Aggregate {
54
142
  name: string;
143
+ /** The chapter that declares the aggregate; set when the editor spans several. */
144
+ chapter?: string;
145
+ /** The roles that may edit it, when its chapter limits them; any admitted role otherwise. */
146
+ roles?: string[];
55
147
  description: string | null;
56
148
  identity: string;
57
149
  lifecycle: Lifecycle | null;
@@ -59,6 +151,10 @@ export interface Aggregate {
59
151
  valueObjects: Record<string, Attr[]>;
60
152
  commands: Command[];
61
153
  queries: Query[];
154
+ /** The aggregates keyed by this one's records, shown as panels on a record's page. */
155
+ related?: Related[];
156
+ /** The draft the aggregate keeps beside its live content, when it keeps one. */
157
+ drafts?: Drafts;
62
158
  }
63
159
 
64
160
  /** How one part of a picture's record is sent: `wrap` is the part of a value object that holds the value. */
@@ -74,12 +170,45 @@ export interface Media {
74
170
  fields: { key: MediaField; alt: MediaField; mime: MediaField; width?: MediaField; height?: MediaField };
75
171
  /** The query that lists the pictures, or null when every instance is read. */
76
172
  listing: string | null;
173
+ /** The chapter that holds the picture aggregate, when it is not this editor's own chapter. */
174
+ domain?: string;
175
+ /** That aggregate, which is then not among the editor's own; see `domain`. */
176
+ definition?: Aggregate;
177
+ }
178
+
179
+ /**
180
+ * The aggregate whose records are actions to run later, found by shape: the attribute that names the
181
+ * record an action is for, the closed set of actions, the due time, and the commands. The editor only
182
+ * makes and changes these records; something else runs them.
183
+ */
184
+ export interface Scheduling {
185
+ aggregate: string;
186
+ chapter?: string;
187
+ subject: string;
188
+ action: string;
189
+ actions: string[];
190
+ due: string;
191
+ /** The first state of a record: an action not yet run. */
192
+ pending: string;
193
+ schedule: string;
194
+ reschedule?: string;
195
+ cancel?: string;
196
+ /** The query that lists the actions of one subject, when there is one. */
197
+ query?: string;
198
+ /** The optional text a record may carry to say why it ended as it did. */
199
+ reason?: string;
200
+ /** True when the editor makes the record's identity, since it is not the subject. */
201
+ generated?: boolean;
77
202
  }
78
203
 
79
204
  export interface Schema {
205
+ /** The first chapter, which names the domain the host is asked for. */
80
206
  domain: string;
207
+ /** Every chapter, in the order the navigation groups them; set when the editor spans several. */
208
+ chapters?: string[];
81
209
  aggregates: Aggregate[];
82
210
  media?: Media;
211
+ scheduling?: Scheduling;
83
212
  }
84
213
 
85
214
  export const SCHEMA: Schema = __SCHEMA__;
@@ -0,0 +1,15 @@
1
+ __BANNER__
2
+
3
+ // Which commands apply to a record now, and which of them need a page of their own.
4
+ import type { Aggregate, Command } from "../schema.ts";
5
+
6
+ type State = Record<string, unknown>;
7
+
8
+ /** Whether a command applies to an instance in its present state: a lifecycle move only from its states. */
9
+ export function available(agg: Aggregate, command: Command, state: State): boolean {
10
+ const move = agg.lifecycle?.transitions.find((transition) => transition.verb === command.name);
11
+ return !move || !agg.lifecycle || move.from.includes(String(state[agg.lifecycle.field]));
12
+ }
13
+
14
+ /** What a command needs asked first: it takes input, or it is destructive. */
15
+ export const needsPage = (command: Command): boolean => command.attributes.length > 0 || command.destructive;