hecks 3.10.0 → 3.12.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 (542) 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 +158 -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 +70 -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/newcomer.rb +50 -0
  40. data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +171 -0
  41. data/lib/hecks/adapters/driving/ruby/chapter.rb +91 -0
  42. data/lib/hecks/adapters/driving/ruby.rb +83 -0
  43. data/lib/hecks/adapters/driving/usage_cache.rb +144 -0
  44. data/lib/hecks/{doors.rb → adapters/driving.rb} +10 -6
  45. data/lib/hecks/behaviors/expectations/boot.rb +1 -1
  46. data/lib/hecks/bench/ruby_runner.rb +1 -1
  47. data/lib/hecks/bluebook/ask_resolution.rb +130 -0
  48. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +8 -2
  49. data/lib/hecks/bluebook/assembly/contracts/read_models.rb +1 -1
  50. data/lib/hecks/bluebook/assembly/contracts/rules.rb +4 -2
  51. data/lib/hecks/bluebook/assembly/contracts/structure.rb +3 -1
  52. data/lib/hecks/bluebook/assembly/marks.rb +4 -15
  53. data/lib/hecks/bluebook/assembly/scalars.rb +39 -0
  54. data/lib/hecks/bluebook/attribute.rb +1 -1
  55. data/lib/hecks/bluebook/behaviour/command.rb +13 -8
  56. data/lib/hecks/bluebook/behaviour/policy.rb +30 -0
  57. data/lib/hecks/bluebook/capabilities/attribute_values.rb +72 -0
  58. data/lib/hecks/bluebook/capabilities.rb +91 -9
  59. data/lib/hecks/bluebook/domain_port.rb +16 -4
  60. data/lib/hecks/bluebook/dsl/adapter_builder.rb +18 -6
  61. data/lib/hecks/bluebook/dsl/binding_proxy.rb +13 -0
  62. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/computed_sources.rb +98 -0
  63. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/fan_out_rows.rb +42 -0
  64. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/projected_fields.rb +14 -2
  65. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/provisions.rb +69 -3
  66. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/with_projections.rb +17 -10
  67. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +5 -0
  68. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
  69. data/lib/hecks/bluebook/dsl/command_builder/mutations.rb +8 -1
  70. data/lib/hecks/bluebook/dsl/command_builder/operands.rb +121 -0
  71. data/lib/hecks/bluebook/dsl/command_builder.rb +2 -0
  72. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +10 -1
  73. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +45 -2
  74. data/lib/hecks/bluebook/dsl/policy_builder.rb +47 -10
  75. data/lib/hecks/bluebook/dsl/rule_reference.rb +7 -4
  76. data/lib/hecks/bluebook/dsl/world_builder.rb +1 -1
  77. data/lib/hecks/bluebook/expression/ast_json/emitters.rb +6 -0
  78. data/lib/hecks/bluebook/expression/ast_json.rb +9 -6
  79. data/lib/hecks/bluebook/expression/ast_reader.rb +14 -9
  80. data/lib/hecks/bluebook/expression/projection.json +36 -0
  81. data/lib/hecks/bluebook/expression/resolver/operations.rb +31 -0
  82. data/lib/hecks/bluebook/expression/resolver/parsing.rb +38 -5
  83. data/lib/hecks/bluebook/expression/resolver/scanning.rb +22 -5
  84. data/lib/hecks/bluebook/expression/resolver/values.rb +10 -0
  85. data/lib/hecks/bluebook/expression/resolver.rb +20 -1
  86. data/lib/hecks/bluebook/hecksagon.rb +23 -3
  87. data/lib/hecks/bluebook/lifecycle.rb +22 -3
  88. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +13 -1
  89. data/lib/hecks/bluebook/meta_validator/modes.rb +4 -1
  90. data/lib/hecks/bluebook/meta_validator/readings.rb +24 -1
  91. data/lib/hecks/bluebook/meta_validator/reconstruction/heads.rb +2 -1
  92. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +6 -3
  93. data/lib/hecks/bluebook/meta_validator/shapes.rb +5 -0
  94. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +1 -1
  95. data/lib/hecks/bluebook/meta_validator.rb +12 -12
  96. data/lib/hecks/bluebook/model_check/ask_checks.rb +71 -0
  97. data/lib/hecks/bluebook/model_check/policy_checks.rb +2 -1
  98. data/lib/hecks/bluebook/model_check/reference_checks.rb +70 -0
  99. data/lib/hecks/bluebook/model_check.rb +8 -2
  100. data/lib/hecks/bluebook/policy.rb +4 -2
  101. data/lib/hecks/bluebook/smoke_test.rb +1 -1
  102. data/lib/hecks/bluebook/synthesizer.rb +2 -0
  103. data/lib/hecks/bluebook.rb +2 -0
  104. data/lib/hecks/cli/command_table.rb +1 -1
  105. data/lib/hecks/cli/console.rb +2 -1
  106. data/lib/hecks/cli/document.rb +1 -1
  107. data/lib/hecks/cli/launcher_words.rb +4 -4
  108. data/lib/hecks/cli/mcp.rb +4 -4
  109. data/lib/hecks/cli/project_cli/launcher_source.rb +2 -2
  110. data/lib/hecks/cli/project_cli/templates.rb +5 -5
  111. data/lib/hecks/cli/project_cli.rb +9 -6
  112. data/lib/hecks/cli/run.rb +2 -2
  113. data/lib/hecks/cli/smoke_http/checks.rb +4 -1
  114. data/lib/hecks/cli/smoke_http/transport.rb +3 -3
  115. data/lib/hecks/cli/smoke_http.rb +3 -0
  116. data/lib/hecks/codemod.rb +5 -2
  117. data/lib/hecks/corpus/routes.rb +7 -0
  118. data/lib/hecks/corpus/rust.rb +3 -2
  119. data/lib/hecks/deploy/bluebook/deploy.bluebook +34 -4
  120. data/lib/hecks/doc/reference/rendering.rb +6 -3
  121. data/lib/hecks/forms/app/commands.rb +8 -7
  122. data/lib/hecks/forms/app/queries.rb +2 -1
  123. data/lib/hecks/forms/app.rb +1 -1
  124. data/lib/hecks/forms/record_renderer.rb +1 -1
  125. data/lib/hecks/framework/bluebook/privacy.bluebook +11 -0
  126. data/lib/hecks/fuzzing/concurrent_dispatch.rb +4 -1
  127. data/lib/hecks/fuzzing/failure_triage.rb +2 -1
  128. data/lib/hecks/fuzzing/mutation/run.rb +11 -4
  129. data/lib/hecks/fuzzing/properties/catalog.rb +6 -4
  130. data/lib/hecks/fuzzing/properties/guaranteed_by_construction.rb +18 -11
  131. data/lib/hecks/fuzzing/properties/guards.rb +1 -1
  132. data/lib/hecks/fuzzing/self_consistency/saga_redelivery.rb +1 -1
  133. data/lib/hecks/fuzzing/self_consistency.rb +6 -2
  134. data/lib/hecks/fuzzing/shrinker.rb +3 -1
  135. data/lib/hecks/fuzzing/value_generator.rb +19 -5
  136. data/lib/hecks/gate/stages.yml +33 -7
  137. data/lib/hecks/grammar/evolve.rb +1 -1
  138. data/lib/hecks/grammar/expression_operators.json +372 -0
  139. data/lib/hecks/grammar/operators.rb +3 -1
  140. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +10 -10
  141. data/lib/hecks/hecks/adapters/codebase/inspection/rubycritic.rb +35 -0
  142. data/lib/hecks/hecks/adapters/codebase/inspection.rb +104 -0
  143. data/lib/hecks/hecks/adapters/codebase/publishing.rb +2 -1
  144. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +12 -2
  145. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +2 -1
  146. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +1 -1
  147. data/lib/hecks/hecks/adapters/cost_explorer.rb +8 -5
  148. data/lib/hecks/hecks/adapters/deploy_toolchain/statuses.rb +2 -1
  149. data/lib/hecks/hecks/adapters/finding_github.rb +13 -6
  150. data/lib/hecks/hecks/adapters/first_admin.rb +2 -2
  151. data/lib/hecks/hecks/adapters/github_rulesets.rb +6 -3
  152. data/lib/hecks/hecks/adapters/in_process_boot.rb +1 -1
  153. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +1 -1
  154. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +10 -20
  155. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +28 -5
  156. data/lib/hecks/hecks/adapters/local_files.rb +2 -2
  157. data/lib/hecks/hecks/adapters/rehearsal/audit_report.rb +107 -0
  158. data/lib/hecks/hecks/adapters/rehearsal/census.rb +71 -0
  159. data/lib/hecks/hecks/adapters/rehearsal/declaration.rb +133 -0
  160. data/lib/hecks/hecks/adapters/rehearsal/environment.rb +83 -0
  161. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +142 -0
  162. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +96 -0
  163. data/lib/hecks/hecks/adapters/rehearsal/postgres_census.rb +100 -0
  164. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +69 -0
  165. data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +133 -0
  166. data/lib/hecks/hecks/adapters/rehearsal/rows.rb +41 -0
  167. data/lib/hecks/hecks/adapters/rehearsal/rust_audit.rb +83 -0
  168. data/lib/hecks/hecks/adapters/rehearsal/subject.rb +104 -0
  169. data/lib/hecks/hecks/adapters/rehearsal.rb +54 -0
  170. data/lib/hecks/hecks/adapters/rehearsal_store.adapter +7 -0
  171. data/lib/hecks/hecks/adapters/rehearsal_store.rb +81 -0
  172. data/lib/hecks/hecks/adapters/terminal.rb +13 -11
  173. data/lib/hecks/hecks/codebase.behaviors +13 -1
  174. data/lib/hecks/hecks/codebase.bluebook +239 -5
  175. data/lib/hecks/hecks/custodian.behaviors +24 -6
  176. data/lib/hecks/hecks/custodian.bluebook +6 -1227
  177. data/lib/hecks/hecks/deploy.behaviors +1 -1
  178. data/lib/hecks/hecks/deploy.world +1 -3
  179. data/lib/hecks/hecks/hecks.behaviors +1 -1
  180. data/lib/hecks/hecks/hecks.hecksagon +53 -18
  181. data/lib/hecks/hecks/hecks.world +13 -10
  182. data/lib/hecks/hecks/rehearsal.bluebook +477 -0
  183. data/lib/hecks/hecks/site.behaviors +1 -1
  184. data/lib/hecks/hecks/site.world +0 -1
  185. data/lib/hecks/hecks/tooling.bluebook +1238 -0
  186. data/lib/hecks/indifferent_key.rb +20 -0
  187. data/lib/hecks/language/adapter.bluebook +13 -3
  188. data/lib/hecks/language/bluebook/aggregate.bluebook +23 -0
  189. data/lib/hecks/language/bluebook/bluebook.behaviors +23 -0
  190. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -0
  191. data/lib/hecks/language/bluebook/entity.bluebook +18 -0
  192. data/lib/hecks/language/bluebook/policy.behaviors +19 -0
  193. data/lib/hecks/language/bluebook/policy.bluebook +9 -1
  194. data/lib/hecks/language/bluebook/query.bluebook +1 -1
  195. data/lib/hecks/language/bluebook/vocabulary.bluebook +63 -4
  196. data/lib/hecks/language/hecksagon/domain_port.bluebook +1 -1
  197. data/lib/hecks/language/hecksagon/hecksagon.bluebook +22 -0
  198. data/lib/hecks/language/oidc.json +10 -0
  199. data/lib/hecks/language/port.bluebook +4 -4
  200. data/lib/hecks/literal.rb +12 -3
  201. data/lib/hecks/ports/key_vault.rb +7 -2
  202. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +21 -10
  203. data/lib/hecks/ports/persistence/plugins/era/postgres_era/boot.rb +4 -2
  204. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +2 -1
  205. data/lib/hecks/ports/persistence/repository_factory.rb +6 -3
  206. data/lib/hecks/projection_files/expression_tables.rb +7 -2
  207. data/lib/hecks/projections/deploy/box/compose.rb +6 -4
  208. data/lib/hecks/projections/deploy/box/edge.rb +62 -0
  209. data/lib/hecks/projections/deploy/box/logging.rb +99 -0
  210. data/lib/hecks/projections/deploy/box/roll_scripts.rb +16 -8
  211. data/lib/hecks/projections/deploy/box/settings/cdn.rb +40 -0
  212. data/lib/hecks/projections/deploy/box/settings/logs.rb +40 -0
  213. data/lib/hecks/projections/deploy/box/settings/patterns.rb +11 -0
  214. data/lib/hecks/projections/deploy/box/settings/types.rb +16 -1
  215. data/lib/hecks/projections/deploy/box/settings.rb +6 -2
  216. data/lib/hecks/projections/deploy/box/stack_yaml.rb +5 -2
  217. data/lib/hecks/projections/deploy/box/templates/Makefile.tmpl +16 -0
  218. data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +8 -4
  219. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +32 -0
  220. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +1 -1
  221. data/lib/hecks/projections/deploy/box/templates/edge.mk.tmpl +17 -0
  222. data/lib/hecks/projections/deploy/box/templates/edge.yaml.tmpl +65 -0
  223. data/lib/hecks/projections/deploy/box/templates/log-group.yaml.tmpl +12 -0
  224. data/lib/hecks/projections/deploy/box/templates/log-policy.yaml.tmpl +7 -0
  225. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +5 -4
  226. data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +5 -4
  227. data/lib/hecks/projections/deploy/box.rb +15 -3
  228. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  229. data/lib/hecks/projections/deploy/shared.rb +5 -2
  230. data/lib/hecks/projections/model/deviations.rb +2 -2
  231. data/lib/hecks/projections/model.rb +2 -2
  232. data/lib/hecks/projections/oidc.rb +13 -1
  233. data/lib/hecks/projections/shape.rb +5 -2
  234. data/lib/hecks/projections/site/cms_editor/attributes.rb +26 -6
  235. data/lib/hecks/projections/site/cms_editor/blocks.rb +157 -0
  236. data/lib/hecks/projections/site/cms_editor/checks.rb +14 -1
  237. data/lib/hecks/projections/site/cms_editor/clearing.rb +70 -0
  238. data/lib/hecks/projections/site/cms_editor/color.rb +124 -0
  239. data/lib/hecks/projections/site/cms_editor/composition.rb +42 -0
  240. data/lib/hecks/projections/site/cms_editor/destructive.rb +61 -0
  241. data/lib/hecks/projections/site/cms_editor/drafts.rb +102 -0
  242. data/lib/hecks/projections/site/cms_editor/hints.rb +74 -0
  243. data/lib/hecks/projections/site/cms_editor/icons.rb +86 -0
  244. data/lib/hecks/projections/site/cms_editor/key_kinds.rb +31 -0
  245. data/lib/hecks/projections/site/cms_editor/limits.rb +72 -0
  246. data/lib/hecks/projections/site/cms_editor/media.rb +1 -1
  247. data/lib/hecks/projections/site/cms_editor/moment.rb +34 -0
  248. data/lib/hecks/projections/site/cms_editor/palette.rb +115 -0
  249. data/lib/hecks/projections/site/cms_editor/pickers.rb +85 -0
  250. data/lib/hecks/projections/site/cms_editor/preview.rb +91 -0
  251. data/lib/hecks/projections/site/cms_editor/relations.rb +92 -0
  252. data/lib/hecks/projections/site/cms_editor/scale_checks.rb +66 -0
  253. data/lib/hecks/projections/site/cms_editor/scheduling.rb +150 -0
  254. data/lib/hecks/projections/site/cms_editor/schema.rb +96 -19
  255. data/lib/hecks/projections/site/cms_editor/setting.rb +52 -9
  256. data/lib/hecks/projections/site/cms_editor/targets.rb +86 -0
  257. data/lib/hecks/projections/site/cms_editor/templates/package.json.tmpl +18 -1
  258. data/lib/hecks/projections/site/cms_editor/templates/src/app.ts.tmpl +251 -57
  259. data/lib/hecks/projections/site/cms_editor/templates/src/assets.ts.tmpl +98 -0
  260. data/lib/hecks/projections/site/cms_editor/templates/src/auth/membership.ts.tmpl +32 -24
  261. data/lib/hecks/projections/site/cms_editor/templates/src/browser/app.css.tmpl +412 -0
  262. data/lib/hecks/projections/site/cms_editor/templates/src/browser/autosave.js.tmpl +144 -0
  263. data/lib/hecks/projections/site/cms_editor/templates/src/browser/blocks.js.tmpl +500 -0
  264. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_doc.js.tmpl +175 -0
  265. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_editor.js.tmpl +418 -0
  266. data/lib/hecks/projections/site/cms_editor/templates/src/browser/epoch.js.tmpl +61 -0
  267. data/lib/hecks/projections/site/cms_editor/templates/src/browser/forms.js.tmpl +210 -0
  268. data/lib/hecks/projections/site/cms_editor/templates/src/browser/lists.js.tmpl +40 -0
  269. data/lib/hecks/projections/site/cms_editor/templates/src/browser/main.js.tmpl +26 -0
  270. data/lib/hecks/projections/site/cms_editor/templates/src/browser/media_picker.js.tmpl +232 -0
  271. data/lib/hecks/projections/site/cms_editor/templates/src/browser/moments.js.tmpl +73 -0
  272. data/lib/hecks/projections/site/cms_editor/templates/src/browser/pickers.js.tmpl +152 -0
  273. data/lib/hecks/projections/site/cms_editor/templates/src/browser/preview.js.tmpl +98 -0
  274. data/lib/hecks/projections/site/cms_editor/templates/src/browser/shell.js.tmpl +140 -0
  275. data/lib/hecks/projections/site/cms_editor/templates/src/browser/toast.js.tmpl +66 -0
  276. data/lib/hecks/projections/site/cms_editor/templates/src/choices.ts.tmpl +99 -0
  277. data/lib/hecks/projections/site/cms_editor/templates/src/commands.ts.tmpl +78 -24
  278. data/lib/hecks/projections/site/cms_editor/templates/src/config.ts.tmpl +6 -0
  279. data/lib/hecks/projections/site/cms_editor/templates/src/flash.ts.tmpl +52 -0
  280. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +19 -2
  281. data/lib/hecks/projections/site/cms_editor/templates/src/media/handler.ts.tmpl +31 -8
  282. data/lib/hecks/projections/site/cms_editor/templates/src/names.ts.tmpl +36 -0
  283. data/lib/hecks/projections/site/cms_editor/templates/src/schema.ts.tmpl +131 -2
  284. data/lib/hecks/projections/site/cms_editor/templates/src/ui/availability.ts.tmpl +15 -0
  285. data/lib/hecks/projections/site/cms_editor/templates/src/ui/block_list.js.tmpl +213 -0
  286. data/lib/hecks/projections/site/cms_editor/templates/src/ui/blocks.ts.tmpl +275 -0
  287. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_model.js.tmpl +8 -5
  288. data/lib/hecks/projections/site/cms_editor/templates/src/ui/detail.ts.tmpl +88 -0
  289. data/lib/hecks/projections/site/cms_editor/templates/src/ui/dialogs.ts.tmpl +43 -0
  290. data/lib/hecks/projections/site/cms_editor/templates/src/ui/fields.ts.tmpl +229 -34
  291. data/lib/hecks/projections/site/cms_editor/templates/src/ui/html.ts.tmpl +40 -81
  292. data/lib/hecks/projections/site/cms_editor/templates/src/ui/icons.ts.tmpl +15 -0
  293. data/lib/hecks/projections/site/cms_editor/templates/src/ui/input.ts.tmpl +29 -12
  294. data/lib/hecks/projections/site/cms_editor/templates/src/ui/links.ts.tmpl +100 -0
  295. data/lib/hecks/projections/site/cms_editor/templates/src/ui/moments.ts.tmpl +44 -0
  296. data/lib/hecks/projections/site/cms_editor/templates/src/ui/outline.ts.tmpl +53 -17
  297. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pages.ts.tmpl +178 -88
  298. data/lib/hecks/projections/site/cms_editor/templates/src/ui/paging.ts.tmpl +82 -0
  299. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pickers.ts.tmpl +110 -0
  300. data/lib/hecks/projections/site/cms_editor/templates/src/ui/preview.ts.tmpl +87 -0
  301. data/lib/hecks/projections/site/cms_editor/templates/src/ui/related.ts.tmpl +97 -0
  302. data/lib/hecks/projections/site/cms_editor/templates/src/ui/schedule.ts.tmpl +160 -0
  303. data/lib/hecks/projections/site/cms_editor/templates/src/ui/shell.ts.tmpl +211 -0
  304. data/lib/hecks/projections/site/cms_editor/templates/src/ui/table.ts.tmpl +151 -0
  305. data/lib/hecks/projections/site/cms_editor/templates/src/ui/theme.js.tmpl +16 -0
  306. data/lib/hecks/projections/site/cms_editor/templates/src/ui/values.ts.tmpl +69 -0
  307. data/lib/hecks/projections/site/cms_editor/templates/src/ui/words.ts.tmpl +97 -0
  308. data/lib/hecks/projections/site/cms_editor/templates/src/ui/world.ts.tmpl +35 -0
  309. data/lib/hecks/projections/site/cms_editor/templates/tsconfig.json.tmpl +2 -1
  310. data/lib/hecks/projections/site/cms_editor/theme.rb +29 -0
  311. data/lib/hecks/projections/site/cms_editor/theme_checks.rb +57 -0
  312. data/lib/hecks/projections/site/cms_editor.rb +49 -23
  313. data/lib/hecks/projections/site/role_probe.rb +2 -0
  314. data/lib/hecks/projections/site/site_root/workflow.rb +5 -0
  315. data/lib/hecks/projector/cli_audience.rb +22 -1
  316. data/lib/hecks/projector/cli_projector/listing.rb +1 -1
  317. data/lib/hecks/projector/cli_projector/option_specs.rb +1 -1
  318. data/lib/hecks/projector/cli_projector/usage.rb +15 -4
  319. data/lib/hecks/projector/docs_projector/closing.rb +1 -1
  320. data/lib/hecks/projector/docs_projector.rb +1 -1
  321. data/lib/hecks/projector/exporter/bindings.rb +40 -10
  322. data/lib/hecks/projector/exporter/capability_values.rb +55 -0
  323. data/lib/hecks/projector/narrate_projector/reactions.rb +1 -1
  324. data/lib/hecks/projector/target.rb +11 -0
  325. data/lib/hecks/projector.rb +32 -5
  326. data/lib/hecks/quality_control/adapters/github_checks.rb +12 -8
  327. data/lib/hecks/quality_control/adapters/github_issues.rb +8 -4
  328. data/lib/hecks/quality_control/cli/qa_open_pr.rb +2 -2
  329. data/lib/hecks/quality_control/cli/qa_sweep/checks/shrinking.rb +1 -1
  330. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +1 -1
  331. data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
  332. data/lib/hecks/quality_control/quality_control.bluebook +6 -2
  333. data/lib/hecks/release/approval.rb +64 -0
  334. data/lib/hecks/router/{door.rb → aggregate_reader.rb} +5 -4
  335. data/lib/hecks/router/namespace_installer.rb +16 -16
  336. data/lib/hecks/router.rb +1 -1
  337. data/lib/hecks/runtime/best_effort.rb +22 -0
  338. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -2
  339. data/lib/hecks/runtime/command_interpreter.rb +3 -2
  340. data/lib/hecks/runtime/command_rules/emission.rb +1 -1
  341. data/lib/hecks/runtime/dispatcher.rb +4 -4
  342. data/lib/hecks/runtime/entity_element/location.rb +1 -1
  343. data/lib/hecks/runtime/event.rb +5 -0
  344. data/lib/hecks/runtime/identity.rb +7 -6
  345. data/lib/hecks/runtime/interpreting.rb +2 -1
  346. data/lib/hecks/runtime/invocation_builder.rb +1 -1
  347. data/lib/hecks/runtime/loader/boot_steps.rb +8 -5
  348. data/lib/hecks/runtime/loader.rb +10 -10
  349. data/lib/hecks/runtime/policy_interpreter/arguments.rb +7 -4
  350. data/lib/hecks/runtime/policy_interpreter/fan_out.rb +3 -3
  351. data/lib/hecks/runtime/policy_interpreter.rb +6 -6
  352. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -1
  353. data/lib/hecks/runtime/reaction_invocation.rb +1 -1
  354. data/lib/hecks/runtime/rebuild_sweep.rb +11 -1
  355. data/lib/hecks/runtime/registry/ask_verification.rb +35 -0
  356. data/lib/hecks/runtime/registry/hecksagon_rules.rb +10 -0
  357. data/lib/hecks/runtime/registry/merging.rb +1 -1
  358. data/lib/hecks/runtime/registry/providers.rb +16 -0
  359. data/lib/hecks/runtime/registry/read_repository.rb +6 -4
  360. data/lib/hecks/runtime/registry/verification.rb +4 -1
  361. data/lib/hecks/runtime/registry/world_defaults.rb +29 -0
  362. data/lib/hecks/runtime/registry.rb +1 -1
  363. data/lib/hecks/runtime/saga_interpreter/compensation.rb +1 -1
  364. data/lib/hecks/runtime/saga_interpreter/delivery.rb +4 -4
  365. data/lib/hecks/runtime/saga_interpreter/transitions.rb +1 -1
  366. data/lib/hecks/runtime/saga_interpreter.rb +3 -3
  367. data/lib/hecks/runtime/tenant_check.rb +5 -4
  368. data/lib/hecks/runtime/value/coercion.rb +2 -2
  369. data/lib/hecks/runtime/value/validation.rb +16 -4
  370. data/lib/hecks/runtime.rb +9 -9
  371. data/lib/hecks/rust_build/host.rb +11 -4
  372. data/lib/hecks/rust_build/prebuilt_binary.rb +101 -0
  373. data/lib/hecks/rust_build/project_rust/target_ir.rb +1 -1
  374. data/lib/hecks/rust_build/wasm.rb +1 -1
  375. data/lib/hecks/rust_build/wasm_browser.rb +1 -1
  376. data/lib/hecks/rust_build.rb +11 -0
  377. data/lib/hecks/served_options.rb +53 -0
  378. data/lib/hecks/site/bluebook/site.bluebook +1 -0
  379. data/lib/hecks/storehouse/audit.rb +1 -1
  380. data/lib/hecks/storehouse/dispatching.rb +8 -6
  381. data/lib/hecks/storehouse/introspection.rb +2 -2
  382. data/lib/hecks/storehouse/querying.rb +6 -4
  383. data/lib/hecks/storehouse.rb +4 -4
  384. data/lib/hecks/tools/ci_gate_decision.rb +35 -0
  385. data/lib/hecks/tools/ci_gates.rb +20 -1
  386. data/lib/hecks/tools/comment_style/source_file.rb +4 -1
  387. data/lib/hecks/tools/deploy_recipe_lint/fixtures.rb +4 -1
  388. data/lib/hecks/tools/lanes/files.rb +8 -0
  389. data/lib/hecks/tools/lanes/prebuilt_workflow.rb +81 -0
  390. data/lib/hecks/tools/lanes/rows.rb +6 -0
  391. data/lib/hecks/tools/lanes.rb +1 -0
  392. data/lib/hecks/tools/oidc_manifests.rb +4 -5
  393. data/lib/hecks/tools/regeneration_run.rb +11 -4
  394. data/lib/hecks/tools/release_only.rb +111 -0
  395. data/lib/hecks/tools/site_routes/editor_files.rb +12 -5
  396. data/lib/hecks/tools/tenant_provisioning.rb +2 -2
  397. data/lib/hecks/tools/tools_doc.rb +4 -4
  398. data/lib/hecks/version.rb +1 -1
  399. data/lib/hecks/vocabulary.rb +19 -5
  400. data/lib/hecks.rb +15 -15
  401. data/lib/rubocop/cop/hecks/env_dump.rb +69 -0
  402. data/lib/rubocop/cop/hecks/monotonic_duration.rb +53 -0
  403. data/lib/rubocop/cop/hecks/no_wall_clock_in_runtime.rb +34 -0
  404. data/lib/rubocop/cop/hecks/raw_shell_interpolation.rb +79 -0
  405. data/lib/rubocop/cop/hecks/sleep_in_spec.rb +34 -0
  406. data/lib/rubocop/cop/hecks/swallowed_rescue.rb +37 -0
  407. data/lib/rubocop/cop/hecks/symbol_string_key_mix.rb +50 -0
  408. data/lib/rubocop/cop/hecks/untyped_raise.rb +37 -0
  409. data/prebuilt/9ee54e2f0d4d6d3fe046827779b221e02de665fba911a048ae5dd8b8fbfb7d90.marshal +0 -0
  410. data/prebuilt/d74a00e996222c1d8b676d5959092effc46b4dc59e0555f3bf9554ac58e3094a.marshal +0 -0
  411. data/prebuilt/verdicts-df19f35868d3284a70f55e42dcef87ef4e65e6a678e5135f72e1f6cff4c1357b.json +1 -0
  412. data/rust/.gitignore +6 -0
  413. data/rust/Cargo.toml +6 -0
  414. data/rust/build/src/binding_pass.rs +103 -0
  415. data/rust/build/src/cargo_sync.rs +6 -0
  416. data/rust/build/src/lineage_pass.rs +3 -3
  417. data/rust/build/src/main.rs +17 -5
  418. data/rust/build/src/pipeline.rs +48 -5
  419. data/rust/build/src/seams_pass.rs +123 -0
  420. data/rust/build/src/translation_pass.rs +167 -0
  421. data/rust/codegen/src/asks.rs +103 -0
  422. data/rust/codegen/src/commands.rs +13 -8
  423. data/rust/codegen/src/constraints.rs +25 -0
  424. data/rust/codegen/src/domain_generator.rs +2 -0
  425. data/rust/codegen/src/expr_emitter.rs +13 -0
  426. data/rust/codegen/src/json_codec.rs +54 -15
  427. data/rust/codegen/src/main.rs +1 -0
  428. data/rust/codegen/src/mutations.rs +128 -3
  429. data/rust/codegen/src/queries.rs +11 -2
  430. data/rust/codegen/src/reactions.rs +1 -1
  431. data/rust/codegen/src/types.rs +166 -8
  432. data/rust/host/HECKS_RELEASE +1 -1
  433. data/rust/host/src/api.rs +4 -4
  434. data/rust/host/src/bin/validate_harness.rs +43 -0
  435. data/rust/host/src/boundary_fuzz/expr_json.rs +2 -2
  436. data/rust/host/src/boundary_fuzz/ir.rs +1 -0
  437. data/rust/host/src/boundary_fuzz/server.rs +1 -1
  438. data/rust/host/src/checkout.rs +35 -19
  439. data/rust/host/src/commerce_ir.rs +408 -2
  440. data/rust/host/src/expr_json.rs +118 -0
  441. data/rust/host/src/extension.rs +13 -1
  442. data/rust/host/src/lib.rs +1 -0
  443. data/rust/host/src/mint.rs +1 -1
  444. data/rust/host/src/payments/tests.rs +27 -1
  445. data/rust/host/src/payments.rs +12 -4
  446. data/rust/host/src/platform_signin/attack_tests.rs +1100 -0
  447. data/rust/host/src/platform_signin/config.rs +512 -0
  448. data/rust/host/src/platform_signin/cookie.rs +237 -0
  449. data/rust/host/src/platform_signin/extension.rs +190 -0
  450. data/rust/host/src/platform_signin/fetcher.rs +322 -0
  451. data/rust/host/src/platform_signin/handler.rs +251 -0
  452. data/rust/host/src/platform_signin/handler_tests.rs +554 -0
  453. data/rust/host/src/platform_signin/jti.rs +196 -0
  454. data/rust/host/src/platform_signin/jwks.rs +542 -0
  455. data/rust/host/src/platform_signin/jws.rs +249 -0
  456. data/rust/host/src/platform_signin/known_answer_tests.rs +237 -0
  457. data/rust/host/src/platform_signin/mapping_tests.rs +254 -0
  458. data/rust/host/src/platform_signin/mod.rs +75 -0
  459. data/rust/host/src/platform_signin/mode_tests.rs +382 -0
  460. data/rust/host/src/platform_signin/pages.rs +204 -0
  461. data/rust/host/src/platform_signin/query.rs +233 -0
  462. data/rust/host/src/platform_signin/session.rs +151 -0
  463. data/rust/host/src/platform_signin/settings.rs +385 -0
  464. data/rust/host/src/platform_signin/strict_json.rs +351 -0
  465. data/rust/host/src/platform_signin/test_support.rs +235 -0
  466. data/rust/host/src/platform_signin/ttl_tests.rs +94 -0
  467. data/rust/host/src/platform_signin/verify.rs +604 -0
  468. data/rust/host/src/platform_signin/wiring_support.rs +319 -0
  469. data/rust/host/src/query_step.rs +22 -4
  470. data/rust/host/src/reference_validate.rs +653 -108
  471. data/rust/host/src/resend.rs +2 -0
  472. data/rust/host/src/server.rs +19 -6
  473. data/rust/host/src/ui_schema.rs +132 -5
  474. data/rust/host/src/web/newsletter.rs +7 -8
  475. data/rust/host/src/web/newsletter_send.rs +38 -13
  476. data/rust/host/src/web/registrations.rs +229 -26
  477. data/rust/host/src/web.rs +53 -9
  478. data/rust/parser/src/emit.rs +38 -6
  479. data/rust/parser/src/expr/ast_json.rs +7 -1
  480. data/rust/parser/src/expr/resolver.rs +171 -6
  481. data/rust/parser/src/ir.rs +7 -0
  482. data/rust/parser/src/keywords.rs +24 -0
  483. data/rust/parser/src/main.rs +3 -0
  484. data/rust/parser/src/parse/aggregate.rs +2 -1
  485. data/rust/parser/src/parse/chapter.rs +2 -2
  486. data/rust/parser/src/parse/command.rs +2 -0
  487. data/rust/parser/src/parse/hecksagon.rs +5 -2
  488. data/rust/parser/src/parse/lifecycle.rs +81 -0
  489. data/rust/parser/src/parse/mod.rs +15 -1
  490. data/rust/parser/src/parse/policy.rs +13 -2
  491. data/rust/parser/src/parse/value_object.rs +34 -17
  492. data/rust/parser/src/ruby_value.rs +1 -1
  493. data/rust/src/exemplar/commands.rs +4 -4
  494. data/rust/src/exemplar/constraints.rs +1 -1
  495. data/rust/src/exemplar/json.rs +43 -1
  496. data/rust/src/kernel/cli.rs +7 -5
  497. data/rust/src/kernel/dispatch.rs +2 -2
  498. data/rust/src/kernel/dispatch_guard_tests.rs +1 -1
  499. data/rust/src/kernel/expr.rs +66 -3
  500. data/rust/src/kernel/expression_operators/arithmetic.rs +130 -5
  501. data/rust/src/kernel/expression_operators/text.rs +26 -2
  502. data/rust/src/kernel/json.rs +15 -0
  503. data/rust/src/kernel/mod.rs +1 -1
  504. data/rust/src/kernel/orchestrate.rs +54 -5
  505. data/rust/src/kernel/orchestrate_guard_tests.rs +67 -0
  506. data/rust/src/kernel/pattern.rs +60 -0
  507. data/rust/src/kernel/reference_lookup.rs +90 -7
  508. metadata +178 -38
  509. data/lib/hecks/doors/cli_door/nesting.rb +0 -34
  510. data/lib/hecks/doors/cli_door/shorthand.rb +0 -85
  511. data/lib/hecks/doors/cli_door.rb +0 -124
  512. data/lib/hecks/doors/cli_runner/answers.rb +0 -138
  513. data/lib/hecks/doors/cli_runner/resolution.rb +0 -146
  514. data/lib/hecks/doors/cli_runner/settling.rb +0 -115
  515. data/lib/hecks/doors/cli_runner/streaming.rb +0 -90
  516. data/lib/hecks/doors/cli_runner/suggestions.rb +0 -43
  517. data/lib/hecks/doors/cli_runner.rb +0 -66
  518. data/lib/hecks/doors/command_request.rb +0 -119
  519. data/lib/hecks/doors/handle.rb +0 -208
  520. data/lib/hecks/doors/json_door.rb +0 -132
  521. data/lib/hecks/doors/launcher_options.rb +0 -247
  522. data/lib/hecks/doors/mcp_door/properties.rb +0 -47
  523. data/lib/hecks/doors/mcp_door/tool_calls.rb +0 -113
  524. data/lib/hecks/doors/mcp_door/tool_definitions/auditing.rb +0 -51
  525. data/lib/hecks/doors/mcp_door/tool_definitions/calling.rb +0 -69
  526. data/lib/hecks/doors/mcp_door/tool_definitions/reading.rb +0 -97
  527. data/lib/hecks/doors/mcp_door/tool_definitions.rb +0 -15
  528. data/lib/hecks/doors/mcp_door.rb +0 -156
  529. data/lib/hecks/doors/mcp_door_scope/argument_policy.rb +0 -92
  530. data/lib/hecks/doors/mcp_door_scope/env_problems.rb +0 -55
  531. data/lib/hecks/doors/mcp_door_scope/presenter.rb +0 -78
  532. data/lib/hecks/doors/mcp_door_scope.rb +0 -201
  533. data/lib/hecks/doors/mcp_guide_cache.rb +0 -75
  534. data/lib/hecks/doors/ruby_door/aggregate_door.rb +0 -162
  535. data/lib/hecks/doors/ruby_door/chapter.rb +0 -89
  536. data/lib/hecks/doors/ruby_door.rb +0 -80
  537. data/lib/hecks/doors/usage_cache.rb +0 -139
  538. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_widget.js.tmpl +0 -282
  539. data/lib/hecks/projections/site/cms_editor/templates/src/ui/media_picker.js.tmpl +0 -109
  540. data/prebuilt/c28d6d9db14eea3e17161e9d9c262efee30b256cf97693a7cfd552bce24b2f7f.marshal +0 -0
  541. data/prebuilt/cb375cba61ca0dbe8ddb9739c306510fe05e5f4b7a9b6955caff8224c63ea0e0.marshal +0 -0
  542. data/prebuilt/verdicts-d6d584ad43b5f3cebf88aafbbdbf024c38574acf18e06adffbc555f7786b1c2f.json +0 -1
@@ -0,0 +1,82 @@
1
+ __BANNER__
2
+
3
+ // A list at scale: which instances a page of a list shows. The address holds all of it, so a page of
4
+ // a list is a link anyone can follow: `_page` (the page, from 1), `_status` (a lifecycle state) and
5
+ // `_q` (text), beside the `query` (and its arguments) the view of the domain's own queries uses. The
6
+ // names start with an underscore so they never collide with a query's own argument. The server
7
+ // narrows and cuts the rows; the browser only orders the page it was given.
8
+ import { at } from "../names.ts";
9
+ import type { Aggregate } from "../schema.ts";
10
+ import { summaryText } from "./outline.ts";
11
+ import { href } from "./html.ts";
12
+
13
+ type State = Record<string, unknown>;
14
+ type Row = [string, State];
15
+
16
+ /** The attribute names that name an instance to a person, in the order they are tried. */
17
+ const TITLES = ["title", "name", "label"];
18
+
19
+ /** What a list is showing: the filters in force, which page, and how many there are. */
20
+ export interface View {
21
+ q: string;
22
+ status: string;
23
+ page: number;
24
+ pages: number;
25
+ size: number;
26
+ /** How many rows match the filters. */
27
+ total: number;
28
+ /** How many rows there are before any filter. */
29
+ unfiltered: number;
30
+ /** The first and last row shown, counted from 1; both 0 when none. */
31
+ from: number;
32
+ to: number;
33
+ /** The other pairs of the address (the view's query and its arguments), kept on every link. */
34
+ params: [string, string][];
35
+ }
36
+
37
+ /** The states a lifecycle can be in, the one it starts in first. */
38
+ export function statesOf(agg: Aggregate): string[] {
39
+ const lifecycle = agg.lifecycle;
40
+ if (!lifecycle) return [];
41
+ return [...new Set([lifecycle.default, ...lifecycle.transitions.flatMap((move) => [...move.from, move.to])])];
42
+ }
43
+
44
+ /** The attribute that names an instance to a person: its title, name or label, when it has a plain one. */
45
+ export function titleOf(agg: Aggregate): string | undefined {
46
+ const plain = agg.attributes.filter((attr) => !attr.list && attr.widget !== "body").map((attr) => attr.name);
47
+ return TITLES.find((name) => plain.includes(name));
48
+ }
49
+
50
+ /** The text a search looks in: the identity, and the title. */
51
+ function searchable(agg: Aggregate, [id, state]: Row): string {
52
+ const title = titleOf(agg);
53
+ return [id, title ? summaryText(state[title]) : ""].join(" ").toLowerCase();
54
+ }
55
+
56
+ /** `status` when it is a state the lifecycle has, else none. */
57
+ export const knownStatus = (agg: Aggregate, status: string): string => (statesOf(agg).includes(status) ? status : "");
58
+
59
+ /** The rows that are in `status` (when it is a state the lifecycle has) and hold every word of `q`. */
60
+ export function narrow(agg: Aggregate, rows: Row[], q: string, status: string): Row[] {
61
+ const words = q.toLowerCase().split(/\s+/).filter(Boolean);
62
+ const inState = status !== "" && agg.lifecycle ? (row: Row) => String(row[1][agg.lifecycle?.field ?? ""]) === status : () => true;
63
+ return rows.filter((row) => inState(row) && words.every((word) => searchable(agg, row).includes(word)));
64
+ }
65
+
66
+ /** The address of a list with its filters; `extra` overrides pairs, and an empty value leaves one out. */
67
+ export function listHref(agg: Aggregate, params: [string, string][], extra: Record<string, string> = {}): string {
68
+ const pairs = [...params.filter(([key]) => !(key in extra)), ...Object.entries(extra)].filter(([, value]) => value !== "");
69
+ const query = new URLSearchParams(pairs).toString();
70
+ return `${href(...at(agg))}${query ? `?${query}` : ""}`;
71
+ }
72
+
73
+ /** What a page of `matching` rows shows: the filters, the page asked for (clamped to the pages there are) and the counts. */
74
+ export function viewOf(matching: number, unfiltered: number, asked: { q: string; status: string; page: number; size: number; params: [string, string][] }): View {
75
+ const pages = Math.max(1, Math.ceil(matching / asked.size));
76
+ const page = Math.min(Math.max(1, asked.page), pages);
77
+ const from = matching === 0 ? 0 : (page - 1) * asked.size + 1;
78
+ return { q: asked.q, status: asked.status, page, pages, size: asked.size, total: matching, unfiltered, from, to: Math.min(matching, page * asked.size), params: asked.params };
79
+ }
80
+
81
+ /** The rows of the page `view` names. */
82
+ export const slice = (rows: Row[], view: View): Row[] => rows.slice(Math.max(0, view.from - 1), view.to);
@@ -0,0 +1,110 @@
1
+ __BANNER__
2
+
3
+ // Pickers: an attribute that names an instance of another aggregate is entered by choosing it. What
4
+ // is on offer comes from the target's own listing query (see choices.ts), and a choice shows the
5
+ // target's title, name or label with its key. A target with few instances is a native `select`; with
6
+ // more, or when any text is allowed, a text box with a `datalist`; a `<kind>:<key>` value is a kind
7
+ // and then a record, which the page's script assembles and a browser with no script leaves as the
8
+ // one text box it is. The server checks every strict key exists before it sends the command.
9
+ import { hostName } from "../names.ts";
10
+ import type { Aggregate, Attr, Picker, PickerKind } from "../schema.ts";
11
+ import { esc } from "./html.ts";
12
+
13
+ /** One instance on offer: its key, and the words that name it to a person. */
14
+ export interface Choice {
15
+ key: string;
16
+ label: string;
17
+ }
18
+
19
+ /** What is on offer for each target, by `targetKey`. */
20
+ export type Choices = Record<string, Choice[]>;
21
+
22
+ /** The part of a picker that says which aggregate it offers. */
23
+ export type Target = Omit<Picker, "kinds">;
24
+
25
+ /** How many choices a native select holds before a text box with suggestions takes over. */
26
+ export const SELECT_MAX = 30;
27
+
28
+ /** The most instances offered for one target; a key past them can still be typed and is still checked. */
29
+ export const OFFER_MAX = 500;
30
+
31
+ /** The host's name for the aggregate a target offers: the key its choices are held under. */
32
+ export const targetKey = (target: Target): string => hostName(target.target ?? "", target.chapter);
33
+
34
+ const idOf = (key: string): string => `choices-${key.replace(/[^A-Za-z0-9]+/g, "-")}`;
35
+
36
+ /** A choice as one line: its words, and its key when that is not the same thing. */
37
+ export const choiceText = (choice: Choice): string => (choice.label && choice.label !== choice.key ? `${choice.label} (${choice.key})` : choice.key);
38
+
39
+ /** The targets a picker names: itself, or each of its kinds that names an aggregate. */
40
+ export function targetsOfPicker(picker: Picker): Target[] {
41
+ return (picker.kinds ?? [picker]).filter((target) => target.target !== undefined);
42
+ }
43
+
44
+ /** Every target `attrs` name, nested value objects included, each once; a target any picker leaves open is not strict. */
45
+ export function targetsOf(agg: Aggregate, attrs: Attr[], found = new Map<string, Target>(), seen = new Set<string>()): Target[] {
46
+ for (const attr of attrs) {
47
+ for (const target of attr.picker ? targetsOfPicker(attr.picker) : []) {
48
+ const was = found.get(targetKey(target));
49
+ found.set(targetKey(target), { ...target, strict: Boolean(target.strict) && (was?.strict ?? true) });
50
+ }
51
+ if (attr.kind === "object" && !seen.has(attr.type)) targetsOf(agg, agg.valueObjects[attr.type] ?? [], found, new Set([...seen, attr.type]));
52
+ }
53
+ return [...found.values()];
54
+ }
55
+
56
+ /** The `datalist`s a form's pickers refer to: one per target that needs one. */
57
+ export function datalists(agg: Aggregate, attrs: Attr[], choices: Choices): string {
58
+ return targetsOf(agg, attrs)
59
+ .filter((target) => (choices[targetKey(target)] ?? []).length > 0 && (!target.strict || choices[targetKey(target)].length > SELECT_MAX))
60
+ .map((target) => {
61
+ const options = choices[targetKey(target)].map((choice) => `<option value="${esc(choice.key)}" label="${esc(choiceText(choice))}"></option>`);
62
+ return `<datalist id="${esc(idOf(targetKey(target)))}">${options.join("")}</datalist>`;
63
+ })
64
+ .join("\n");
65
+ }
66
+
67
+ /** What a picker's control needs from the input it stands in for. */
68
+ export interface Control {
69
+ id: string;
70
+ name: string;
71
+ value: string;
72
+ optional: boolean;
73
+ /** Whether the field is the one a refusal named. */
74
+ invalid: boolean;
75
+ /** Further attributes of the control, already escaped, each with its leading space. */
76
+ extra: string;
77
+ }
78
+
79
+ const inputClasses = (control: Control): string => (control.invalid ? "input input-error w-full max-w-xl" : "input w-full max-w-xl");
80
+
81
+ /** A native select of the choices, with the held value kept when it is not on offer. */
82
+ function select(options: Choice[], control: Control): string {
83
+ const held = control.value !== "" && !options.some((choice) => choice.key === control.value);
84
+ const blank = `<option value="">${control.optional ? "None" : "Choose..."}</option>`;
85
+ const own = held ? [`<option value="${esc(control.value)}" selected>${esc(control.value)} (not on offer)</option>`] : [];
86
+ const each = options.map((choice) => `<option value="${esc(choice.key)}"${choice.key === control.value ? " selected" : ""}>${esc(choiceText(choice))}</option>`);
87
+ const classes = control.invalid ? "select select-error w-full max-w-xl" : "select w-full max-w-xl";
88
+ return `<select class="${classes}" id="${esc(control.id)}" name="${esc(control.name)}"${control.extra}>${[blank, ...own, ...each].join("")}</select>`;
89
+ }
90
+
91
+ /** A text box with suggestions; a strict one is checked against them before the form is sent, unless they were cut short. */
92
+ function suggesting(target: Target, control: Control, cut: boolean): string {
93
+ const strict = target.strict ? (cut ? " data-strict data-open" : " data-strict") : "";
94
+ return `<input type="text" class="${inputClasses(control)}" id="${esc(control.id)}" name="${esc(control.name)}" value="${esc(control.value)}" list="${esc(idOf(targetKey(target)))}" autocomplete="off"${strict}${control.extra}>`;
95
+ }
96
+
97
+ /** A `<kind>:<key>` value: its one text box, and the kinds and records the script builds a pair of selects from. */
98
+ function kindsControl(kinds: PickerKind[], choices: Choices, control: Control): string {
99
+ const shown = kinds.map((kind) => ({ kind: kind.kind, strict: Boolean(kind.strict && kind.target), options: kind.target ? (choices[targetKey(kind)] ?? []) : [] }));
100
+ const input = `<input type="text" class="${inputClasses(control)}" id="${esc(control.id)}" name="${esc(control.name)}" value="${esc(control.value)}" autocomplete="off"${control.extra}>`;
101
+ return `<div class="grid max-w-xl gap-2" data-key-picker data-kinds="${esc(JSON.stringify(shown))}">${input}</div>`;
102
+ }
103
+
104
+ /** The control for a picker, or null when what is on offer could not be read (the input stays a text box). */
105
+ export function pickerControl(picker: Picker, choices: Choices, control: Control): string | null {
106
+ if (picker.kinds) return kindsControl(picker.kinds, choices, control);
107
+ const options = choices[targetKey(picker)];
108
+ if (!options) return null;
109
+ return picker.strict && options.length <= SELECT_MAX ? select(options, control) : suggesting(picker, control, options.length >= OFFER_MAX);
110
+ }
@@ -0,0 +1,87 @@
1
+ __BANNER__
2
+
3
+ // A record as the site shows it, in a side drawer beside the editor. The row's `preview` is an address
4
+ // template; the record's key and its parts fill it, and the page frames the result. This is only a
5
+ // framed address: nothing of the editor is sent to it and nothing is injected into it, and the
6
+ // editor's content security policy names its origin (and no other) as a place a frame may load from.
7
+ //
8
+ // The frame is sandboxed with `allow-scripts allow-same-origin` and nothing else. The site's own
9
+ // scripts must run for the page to look as it does, and without `allow-same-origin` the page would
10
+ // have an opaque origin, so its storage, its cookies and its requests to itself would all fail. On a
11
+ // site served from another origin the flag lets the page keep its own origin and gives it no way to
12
+ // reach the editor. A path template is on the editor's own origin, where the pair removes the sandbox
13
+ // altogether: use an absolute address for a site served elsewhere.
14
+ import { EDITOR } from "../config.ts";
15
+ import type { Aggregate } from "../schema.ts";
16
+ import { esc } from "./html.ts";
17
+ import { icon } from "./icons.ts";
18
+ import { partsOf } from "./links.ts";
19
+ import type { Parts } from "./links.ts";
20
+ import { pictureAggregate, schedulingAggregate } from "./world.ts";
21
+ import { label } from "./words.ts";
22
+
23
+ /** A value placed in an address: encoded, with the colon of a key left as it is. */
24
+ const placed = (value: string): string => encodeURIComponent(value).replace(/%3A/gi, ":");
25
+
26
+ /** The address a record is previewed at, or null when the row names no preview. */
27
+ export function previewUrl(agg: Aggregate, id: string): string | null {
28
+ if (!EDITOR.preview) return null;
29
+ const parts = partsOf(agg, id);
30
+ return EDITOR.preview.replace(/\{(key|kind|slug|id)\}/g, (_, name: keyof Parts) => placed(parts[name]));
31
+ }
32
+
33
+ /** What a page needs to offer a preview: the button for the heading, and the drawer for the end of the page. */
34
+ export interface Tools {
35
+ button: string;
36
+ drawer: string;
37
+ }
38
+
39
+ const NONE: Tools = { button: "", drawer: "" };
40
+
41
+ const WIDTHS = [
42
+ ["phone", "Phone", "375 pixels wide"],
43
+ ["tablet", "Tablet", "768 pixels wide"],
44
+ ["desktop", "Desktop", "the width of the window"],
45
+ ] as const;
46
+
47
+ function widths(): string {
48
+ const buttons = WIDTHS.map(
49
+ ([name, text, note]) =>
50
+ `<button type="button" class="btn btn-sm join-item" data-preview-width="${name}" aria-pressed="${name === "desktop" ? "true" : "false"}" title="${text}, ${note}">${icon(name)}<span class="hidden sm:inline">${text}</span></button>`,
51
+ );
52
+ return `<div class="join" role="group" aria-label="Preview width">${buttons.join("")}</div>`;
53
+ }
54
+
55
+ function drawer(agg: Aggregate, id: string, url: string): string {
56
+ const title = `Preview of ${label(agg.name).toLowerCase()} ${id}`;
57
+ return `<div class="drawer drawer-end" data-preview>
58
+ <input id="preview-toggle" type="checkbox" class="drawer-toggle" aria-hidden="true" tabindex="-1">
59
+ <div class="drawer-side z-40">
60
+ <label for="preview-toggle" aria-label="Close the preview" class="drawer-overlay"></label>
61
+ <section class="flex h-full w-full max-w-6xl flex-col border-l border-base-300 bg-base-200" role="dialog" aria-modal="true" aria-label="${esc(title)}">
62
+ <div class="flex flex-wrap items-center gap-2 border-b border-base-300 bg-base-100 p-2">
63
+ <h2 class="mr-auto px-2 text-base">Preview</h2>
64
+ ${widths()}
65
+ <button type="button" class="btn btn-sm" data-preview-refresh>${icon("refresh")}<span class="hidden sm:inline">Refresh</span></button>
66
+ <a class="btn btn-sm" href="${esc(url)}" target="_blank" rel="noopener" data-preview-external>${icon("external")}<span class="hidden sm:inline">Open in new tab</span></a>
67
+ <button type="button" class="btn btn-ghost btn-sm btn-square" aria-label="Close the preview" data-preview-close>${icon("close")}</button>
68
+ </div>
69
+ <div class="grid min-h-0 flex-1 justify-items-center overflow-auto p-2" data-preview-stage>
70
+ <iframe class="h-full w-full max-w-full border border-base-300 bg-white" title="${esc(title)}" sandbox="allow-scripts allow-same-origin" referrerpolicy="no-referrer" data-preview-frame data-src="${esc(url)}"></iframe>
71
+ </div>
72
+ <p class="px-3 pb-2 text-xs text-muted">This is the site's own page at <span class="break-all">${esc(url)}</span>. It reads only: nothing here publishes. A page that stays blank is one the site does not let this editor show in a frame.</p>
73
+ </section>
74
+ </div>
75
+ </div>`;
76
+ }
77
+
78
+ /** Whether a record of `agg` is something the site shows a page for: the pictures' records and the scheduled actions are not. */
79
+ const shown = (agg: Aggregate): boolean => agg !== pictureAggregate() && agg !== schedulingAggregate();
80
+
81
+ /** The preview button and drawer for one record, or nothing when the row names no preview or the record has no page. */
82
+ export function previewTools(agg: Aggregate, id: string): Tools {
83
+ const url = shown(agg) ? previewUrl(agg, id) : null;
84
+ if (!url) return NONE;
85
+ const button = `<a class="btn" href="${esc(url)}" target="_blank" rel="noopener" data-preview-open="preview-toggle">${icon("eye")}Preview</a>`;
86
+ return { button, drawer: drawer(agg, id, url) };
87
+ }
@@ -0,0 +1,97 @@
1
+ __BANNER__
2
+
3
+ // The records that go with one record, as panels on its page. Another aggregate's records belong to
4
+ // this one's when its identity is a `<kind>:<slug>` key of this aggregate (the generator decides, from
5
+ // the keys' declared patterns; see `Related` in schema.ts). Each such aggregate is a panel: the record
6
+ // when it exists, summarised by its shape (a body as read-only text, pictures as a strip of
7
+ // thumbnails, anything else as its fields) with a link to edit it on its own screen, or an empty
8
+ // state with a link to the form that makes it, with the key already filled. Both links carry the way
9
+ // back, so saving returns here.
10
+ import { at, mayEdit } from "../names.ts";
11
+ import { SCHEMA } from "../schema.ts";
12
+ import type { Aggregate, Related } from "../schema.ts";
13
+ import { esc, href } from "./html.ts";
14
+ import { icon } from "./icons.ts";
15
+ import { partsOf, plainName, plainText, withBack } from "./links.ts";
16
+ import { outline } from "./outline.ts";
17
+ import { valueOf, valueRows } from "./values.ts";
18
+ import type { World } from "./world.ts";
19
+ import { pictureAggregate, relatedAggregate } from "./world.ts";
20
+ import { capital, singular } from "./words.ts";
21
+
22
+ type State = Record<string, unknown>;
23
+
24
+ /** What a related record is called in the panel's title and its buttons: its shape's noun, or its aggregate's own words. */
25
+ const nounOf = (rel: Related, target: Aggregate): string => (rel.shape === "document" ? "text" : rel.shape === "gallery" ? "pictures" : singular(target.name));
26
+
27
+ /** The words that say what a picture shows, from its record when the editor holds it. */
28
+ function altOf(key: string, world: World): string {
29
+ const media = SCHEMA.media;
30
+ const registry = pictureAggregate();
31
+ const row = registry ? world.lookup(registry, key) : undefined;
32
+ return media && row ? plainText(row[media.fields.alt.name]) : "";
33
+ }
34
+
35
+ function thumbnails(keys: string[], world: World): string {
36
+ if (!SCHEMA.media) return outline(keys);
37
+ const items = keys.map(
38
+ (key, index) =>
39
+ `<li class="shrink-0"><img class="size-24 border border-base-300 object-cover" src="${esc(href("media", key))}" alt="${esc(altOf(key, world) || key)}" title="${esc(key)}" width="96" height="96" loading="lazy"><span class="sr-only">Picture ${index + 1} of ${keys.length}</span></li>`,
40
+ );
41
+ return `<ul class="flex gap-2 overflow-x-auto pb-1" aria-label="The pictures, in order">${items.join("")}</ul>`;
42
+ }
43
+
44
+ function summary(rel: Related, target: Aggregate, values: State, world: World): string {
45
+ if (rel.shape === "document") {
46
+ const body = target.attributes.find((attr) => attr.widget === "body" && !attr.list);
47
+ const draft = target.drafts && values[target.drafts.attribute] != null ? `<p class="mb-3"><span class="badge badge-soft badge-info">Changes not published yet</span></p>` : "";
48
+ return `${draft}<div class="max-h-96 overflow-y-auto">${body ? valueOf(target, body, values) : ""}</div>`;
49
+ }
50
+ if (rel.shape === "gallery") {
51
+ const list = target.attributes.find((attr) => attr.list && attr.picker?.target === SCHEMA.media?.aggregate);
52
+ const keys = [values[list?.name ?? ""]].flat().map(plainText).filter((key) => key !== "");
53
+ return keys.length === 0 ? `<p class="text-muted">None chosen yet.</p>` : thumbnails(keys, world);
54
+ }
55
+ return valueRows(target, target.attributes.filter((attr) => attr.name !== target.identity), values);
56
+ }
57
+
58
+ function panel(index: number, noun: string, actions: string, inner: string): string {
59
+ const id = `related-${index}`;
60
+ return `<section class="border border-base-300 bg-base-100 p-4 sm:p-6" aria-labelledby="${id}">
61
+ <div class="mb-3 flex flex-wrap items-center justify-between gap-2"><h2 id="${id}" class="text-lg">${esc(capital(noun))}</h2><div class="flex gap-2">${actions}</div></div>
62
+ ${inner}
63
+ </section>`;
64
+ }
65
+
66
+ /** The address of the screen that makes the related record: its creating command, with the key filled and the way back. */
67
+ function addLink(rel: Related, target: Aggregate, key: string, here: string): string | null {
68
+ const field = target.attributes.find((attr) => attr.name === rel.field);
69
+ const command = target.commands.find((candidate) => candidate.name === rel.create);
70
+ if (!field || !command) return null;
71
+ return withBack(`${href(...at(target), "new", command.name)}?${encodeURIComponent(plainName(target, field))}=${encodeURIComponent(key)}`, here);
72
+ }
73
+
74
+ /** The address of the screen that changes the related record, or of its page when no command does. */
75
+ function editLink(rel: Related, target: Aggregate, key: string, here: string): string {
76
+ return withBack(href(...at(target), "id", key, ...(rel.edit ? [rel.edit] : [])), here);
77
+ }
78
+
79
+ function one(rel: Related, index: number, agg: Aggregate, id: string, world: World, here: string): string {
80
+ const target = relatedAggregate(rel);
81
+ if (!target || !mayEdit(target, world.role)) return "";
82
+ const key = partsOf(agg, id).key;
83
+ const values = world.lookup(target, key);
84
+ const noun = nounOf(rel, target);
85
+ if (values) {
86
+ const edit = `<a class="btn btn-sm" href="${esc(editLink(rel, target, key, here))}">${icon("pencil")}${rel.edit ? "Edit" : "Open"}</a>`;
87
+ return panel(index, noun, edit, summary(rel, target, values, world));
88
+ }
89
+ const add = addLink(rel, target, key, here);
90
+ const button = add ? `<a class="btn btn-neutral btn-sm" href="${esc(add)}">${icon("plus")}Add ${esc(noun)}</a>` : "";
91
+ return panel(index, noun, "", `<p class="mb-3 text-muted">No ${esc(noun)} for this ${esc(singular(agg.name))} yet.</p>${button}`);
92
+ }
93
+
94
+ /** One panel for each aggregate keyed by `agg`'s records, for the record `id` shown at `here`. */
95
+ export function relatedPanels(agg: Aggregate, id: string, world: World, here: string): string {
96
+ return (agg.related ?? []).map((rel, index) => one(rel, index, agg, id, world, here)).filter((html) => html !== "").join("\n");
97
+ }
@@ -0,0 +1,160 @@
1
+ __BANNER__
2
+
3
+ // Actions scheduled for a record, and every pending action across records. The editor only makes and
4
+ // changes the scheduled-action records (their commands, found by shape; see `Scheduling` in
5
+ // schema.ts): nothing here runs an action, and a record's own state never changes because of one. A
6
+ // runner elsewhere reads the records at their due time and moves each out of its first state. A time
7
+ // is chosen with the browser's own date-and-time control, in the viewer's zone.
8
+ import { at, mayEdit } from "../names.ts";
9
+ import { SCHEMA } from "../schema.ts";
10
+ import type { Aggregate, Command, Scheduling } from "../schema.ts";
11
+ import { available } from "./availability.ts";
12
+ import { confirmDialog, dialogId } from "./dialogs.ts";
13
+ import { fields } from "./fields.ts";
14
+ import { esc, href, statusBadge } from "./html.ts";
15
+ import { backInput, partsOf, plainName, plainText, withBack } from "./links.ts";
16
+ import { momentTime, secondsOf } from "./moments.ts";
17
+ import type { World } from "./world.ts";
18
+ import { schedulingAggregate } from "./world.ts";
19
+ import { commandLabel, label, singular } from "./words.ts";
20
+
21
+ type State = Record<string, unknown>;
22
+ type Row = [string, State];
23
+
24
+ /** The scheduling aggregate with its parts, when this editor has one. */
25
+ function scheduler(): { agg: Aggregate; spec: Scheduling } | null {
26
+ const agg = schedulingAggregate();
27
+ return agg && SCHEMA.scheduling ? { agg, spec: SCHEMA.scheduling } : null;
28
+ }
29
+
30
+ /** Whether a scheduled action's subject can be a record of `agg`: its key's kinds name that aggregate. */
31
+ function subjectIs(sched: Aggregate, spec: Scheduling, agg: Aggregate): boolean {
32
+ const subject = sched.attributes.find((attr) => attr.name === spec.subject);
33
+ return (subject?.picker?.kinds ?? []).some((kind) => kind.target === agg.name && kind.chapter === agg.chapter);
34
+ }
35
+
36
+ /** Whether records of `agg` have a Schedule panel and the person may use it. */
37
+ export function schedules(agg: Aggregate, role: string | null): boolean {
38
+ const found = scheduler();
39
+ return found !== null && subjectIs(found.agg, found.spec, agg) && mayEdit(found.agg, role);
40
+ }
41
+
42
+ /** The query that lists one subject's actions, with the key in the shape its argument takes, or null when there is none. */
43
+ export function subjectQuery(agg: Aggregate, id: string): { name: string; args: State } | null {
44
+ const found = scheduler();
45
+ const argument = found?.agg.queries.find((query) => query.name === found.spec.query)?.attributes[0];
46
+ if (!found || !argument || !found.spec.query) return null;
47
+ const attr = found.agg.attributes.find((candidate) => candidate.name === found.spec.subject);
48
+ const key = partsOf(agg, id).key;
49
+ const parts = attr ? (found.agg.valueObjects[attr.type] ?? []) : [];
50
+ return { name: found.spec.query, args: { [argument.name]: attr && attr.kind === "object" && parts.length === 1 ? { [parts[0].name]: key } : key } };
51
+ }
52
+
53
+ const stateOf = (sched: Aggregate, row: State): unknown => (sched.lifecycle ? row[sched.lifecycle.field] : undefined);
54
+
55
+ const dueIn = (spec: Scheduling, row: State): number | null => secondsOf(row[spec.due]);
56
+
57
+ /** Soonest first; an action with no readable time goes last. */
58
+ const soonest = (spec: Scheduling) => (a: Row, b: Row): number => (dueIn(spec, a[1]) ?? Infinity) - (dueIn(spec, b[1]) ?? Infinity);
59
+
60
+ const when = (spec: Scheduling, row: State): string => {
61
+ const seconds = dueIn(spec, row);
62
+ return seconds === null ? `<span class="text-muted">No time set</span>` : momentTime(seconds, "datetime");
63
+ };
64
+
65
+ const actionText = (spec: Scheduling, row: State): string => label(plainText(row[spec.action]));
66
+
67
+ function command(sched: Aggregate, name: string | undefined): Command | undefined {
68
+ return sched.commands.find((candidate) => candidate.name === name);
69
+ }
70
+
71
+ /** The buttons of one pending action, and the question its cancel asks. */
72
+ function tools(sched: Aggregate, spec: Scheduling, id: string, row: State, here: string, index: number): { buttons: string; dialog: string } {
73
+ const change = command(sched, spec.reschedule);
74
+ const stop = command(sched, spec.cancel);
75
+ const page = (cmd: Command): string => withBack(href(...at(sched), "id", id, cmd.name), here);
76
+ const reschedule = change && available(sched, change, row) ? `<a class="btn btn-sm" href="${esc(page(change))}">${esc(commandLabel(sched, change))}</a>` : "";
77
+ if (!stop || !available(sched, stop, row)) return { buttons: reschedule, dialog: "" };
78
+ const suffix = String(index);
79
+ const cancel = `<a class="btn btn-sm btn-error btn-outline" href="${esc(page(stop))}" data-confirm="${dialogId(stop, suffix)}">${esc(commandLabel(sched, stop))}</a>`;
80
+ const asking = { suffix, back: { path: here, label: "" } };
81
+ return { buttons: `${reschedule}${cancel}`, dialog: confirmDialog(sched, stop, href(...at(sched), "id", id, stop.name), id, undefined, asking) };
82
+ }
83
+
84
+ function pendingList(sched: Aggregate, spec: Scheduling, rows: Row[], here: string): string {
85
+ if (rows.length === 0) return `<p class="text-muted">Nothing is scheduled for this record.</p>`;
86
+ const lines = rows.map(([id, row], index) => {
87
+ const { buttons, dialog } = tools(sched, spec, id, row, here, index);
88
+ return `<li class="flex flex-wrap items-center gap-x-4 gap-y-2 py-3"><span class="font-semibold">${esc(actionText(spec, row))}</span><span>${when(spec, row)}</span><span class="ml-auto flex flex-wrap gap-2">${buttons}</span>${dialog}</li>`;
89
+ });
90
+ return `<ul class="divide-y divide-base-300" aria-label="Pending actions">${lines.join("\n")}</ul>`;
91
+ }
92
+
93
+ function pastList(sched: Aggregate, spec: Scheduling, rows: Row[]): string {
94
+ if (rows.length === 0) return "";
95
+ const lines = rows.map(([, row]) => {
96
+ const reason = spec.reason && plainText(row[spec.reason]) !== "" ? `<span class="basis-full text-sm text-muted">${esc(plainText(row[spec.reason]))}</span>` : "";
97
+ return `<li class="flex flex-wrap items-center gap-x-4 gap-y-1 py-2"><span>${esc(actionText(spec, row))}</span><span>${when(spec, row)}</span>${statusBadge(sched, stateOf(sched, row))}${reason}</li>`;
98
+ });
99
+ return `<h3 class="mb-1 mt-6 text-sm">Earlier</h3>\n<ul class="divide-y divide-base-300" aria-label="Earlier actions">${lines.join("\n")}</ul>`;
100
+ }
101
+
102
+ /** The form that adds an action for `key`: which action, when, and anything else the creating command takes. */
103
+ function addForm(sched: Aggregate, spec: Scheduling, subject: string, here: string): string {
104
+ const make = command(sched, spec.schedule);
105
+ const subjectAttr = sched.attributes.find((attr) => attr.name === spec.subject);
106
+ if (!make || !subjectAttr) return "";
107
+ const named = new Map(make.attributes.map((attr) => [attr.name, attr]));
108
+ const rest = make.attributes.filter((attr) => ![spec.subject, spec.action, spec.due].includes(attr.name) && !(spec.generated && attr.name === sched.identity));
109
+ const asked = [named.get(spec.action), named.get(spec.due), ...rest].flatMap((attr) => attr ?? []);
110
+ return `<form class="mt-6 max-w-xl border-t border-base-300 pt-4" method="post" action="${esc(href(...at(sched), "new", make.name))}" data-form>
111
+ <h3 class="text-sm">Add an action</h3>
112
+ ${backInput({ path: here, label: "" })}<input type="hidden" name="${esc(plainName(sched, subjectAttr))}" value="${esc(subject)}">
113
+ ${fields(sched, asked, {}, null, true, {})}
114
+ <div class="mt-4"><button type="submit" class="btn btn-neutral" data-busy>${esc(label(make.name))}</button></div>
115
+ </form>`;
116
+ }
117
+
118
+ /** The Schedule panel of a record: its pending actions, a form to add one, and the ones that have ended. */
119
+ export function schedulePanel(agg: Aggregate, id: string, world: World, here: string): string {
120
+ const found = scheduler();
121
+ if (!found || !schedules(agg, world.role)) return "";
122
+ const { agg: sched, spec } = found;
123
+ const key = partsOf(agg, id).key;
124
+ const rows = (world.actions ?? world.rows(sched)).filter(([, row]) => plainText(row[spec.subject]) === key);
125
+ const pending = rows.filter(([, row]) => stateOf(sched, row) === spec.pending).sort(soonest(spec));
126
+ const ended = rows.filter(([, row]) => stateOf(sched, row) !== spec.pending).sort((a, b) => soonest(spec)(b, a)).slice(0, 10);
127
+ return `<section class="border border-base-300 bg-base-100 p-4 sm:p-6" aria-labelledby="schedule-title">
128
+ <h2 id="schedule-title" class="text-lg">Schedule</h2>
129
+ <p class="mb-4 mt-1 max-w-prose text-sm text-muted">Actions saved here are run later, at the time you choose, by something outside this editor. Saving, changing or cancelling one here never publishes or changes this ${esc(singular(agg.name))} itself.</p>
130
+ ${pendingList(sched, spec, pending, here)}
131
+ ${addForm(sched, spec, key, here)}
132
+ ${pastList(sched, spec, ended)}
133
+ </section>`;
134
+ }
135
+
136
+ /** Where a subject key leads: the record's page when the editor holds it, else the key as plain text. */
137
+ function subjectLink(key: string, sched: Aggregate, spec: Scheduling, world: World): string {
138
+ const kind = key.slice(0, Math.max(key.indexOf(":"), 0));
139
+ const subject = sched.attributes.find((attr) => attr.name === spec.subject);
140
+ const target = (subject?.picker?.kinds ?? []).find((candidate) => candidate.kind === kind && candidate.target);
141
+ const aggregate = target && SCHEMA.aggregates.find((candidate) => candidate.name === target.target && candidate.chapter === target.chapter);
142
+ const id = aggregate && [key.slice(kind.length + 1), key].find((candidate) => world.lookup(aggregate, candidate));
143
+ return aggregate && id ? `<a class="link" href="${esc(href(...at(aggregate), "id", id))}">${esc(key)}</a>` : esc(key);
144
+ }
145
+
146
+ /** Every pending action, soonest first, with the record each is for and the buttons that change it. */
147
+ export function scheduledTable(world: World, here: string): string {
148
+ const found = scheduler();
149
+ if (!found) return "";
150
+ const { agg: sched, spec } = found;
151
+ const rows = world.rows(sched).filter(([, row]) => stateOf(sched, row) === spec.pending).sort(soonest(spec));
152
+ if (rows.length === 0) return `<p class="text-muted">Nothing is scheduled.</p>`;
153
+ const lines = rows.map(([id, row], index) => {
154
+ const { buttons, dialog } = tools(sched, spec, id, row, here, index);
155
+ const key = plainText(row[spec.subject]);
156
+ return `<tr><td>${when(spec, row)}</td><td>${esc(actionText(spec, row))}</td><td class="break-all">${subjectLink(key, sched, spec, world)}</td><td><div class="flex flex-wrap justify-end gap-2">${buttons}</div>${dialog}</td></tr>`;
157
+ });
158
+ const head = `<tr><th scope="col">When</th><th scope="col">Action</th><th scope="col">For</th><th scope="col"><span class="sr-only">Change</span></th></tr>`;
159
+ return `<div class="overflow-x-auto border border-base-300 bg-base-100"><table class="table"><thead>${head}</thead><tbody>${lines.join("\n")}</tbody></table></div>`;
160
+ }