hecks 3.9.1 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (504) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/bluebook/pizzas.bluebook +29 -0
  3. data/examples/pizzas/bluebook/pizzas.hecksagon +7 -0
  4. data/exe/hecks +4 -4
  5. data/lib/hecks/adapters/driven/d1.rb +2 -10
  6. data/lib/hecks/adapters/driven/heki.adapter +1 -1
  7. data/lib/hecks/adapters/driven/heki.rb +2 -12
  8. data/lib/hecks/adapters/driven/lambda.adapter +1 -1
  9. data/lib/hecks/adapters/driven/lambda.rb +3 -18
  10. data/lib/hecks/adapters/driven/postgres.rb +4 -11
  11. data/lib/hecks/adapters/driven/sqlite.adapter +1 -1
  12. data/lib/hecks/adapters/driven/sqlite.rb +2 -10
  13. data/lib/hecks/adapters/driving/admission.rb +46 -0
  14. data/lib/hecks/adapters/driving/cli/nesting.rb +36 -0
  15. data/lib/hecks/adapters/driving/cli/shorthand.rb +88 -0
  16. data/lib/hecks/adapters/driving/cli.rb +127 -0
  17. data/lib/hecks/adapters/driving/cli_runner/answers.rb +160 -0
  18. data/lib/hecks/adapters/driving/cli_runner/resolution.rb +155 -0
  19. data/lib/hecks/adapters/driving/cli_runner/settling.rb +120 -0
  20. data/lib/hecks/adapters/driving/cli_runner/streaming.rb +93 -0
  21. data/lib/hecks/adapters/driving/cli_runner/suggestions.rb +47 -0
  22. data/lib/hecks/adapters/driving/cli_runner.rb +69 -0
  23. data/lib/hecks/adapters/driving/command_request.rb +121 -0
  24. data/lib/hecks/adapters/driving/handle.rb +214 -0
  25. data/lib/hecks/adapters/driving/json.rb +136 -0
  26. data/lib/hecks/adapters/driving/launcher_options.rb +267 -0
  27. data/lib/hecks/adapters/driving/mcp/properties.rb +49 -0
  28. data/lib/hecks/adapters/driving/mcp/tool_calls.rb +116 -0
  29. data/lib/hecks/adapters/driving/mcp/tool_definitions/auditing.rb +53 -0
  30. data/lib/hecks/adapters/driving/mcp/tool_definitions/calling.rb +71 -0
  31. data/lib/hecks/adapters/driving/mcp/tool_definitions/reading.rb +99 -0
  32. data/lib/hecks/adapters/driving/mcp/tool_definitions.rb +18 -0
  33. data/lib/hecks/adapters/driving/mcp.rb +159 -0
  34. data/lib/hecks/adapters/driving/mcp_guide_cache.rb +78 -0
  35. data/lib/hecks/adapters/driving/mcp_scope/argument_policy.rb +97 -0
  36. data/lib/hecks/adapters/driving/mcp_scope/env_problems.rb +69 -0
  37. data/lib/hecks/adapters/driving/mcp_scope/presenter.rb +83 -0
  38. data/lib/hecks/adapters/driving/mcp_scope.rb +213 -0
  39. data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +171 -0
  40. data/lib/hecks/adapters/driving/ruby/chapter.rb +91 -0
  41. data/lib/hecks/adapters/driving/ruby.rb +83 -0
  42. data/lib/hecks/adapters/driving/usage_cache.rb +144 -0
  43. data/lib/hecks/{doors.rb → adapters/driving.rb} +10 -6
  44. data/lib/hecks/behaviors/expectations/boot.rb +1 -1
  45. data/lib/hecks/bench/ruby_runner.rb +1 -1
  46. data/lib/hecks/bluebook/ask_resolution.rb +130 -0
  47. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +8 -2
  48. data/lib/hecks/bluebook/assembly/contracts/rules.rb +4 -2
  49. data/lib/hecks/bluebook/assembly/contracts/structure.rb +3 -1
  50. data/lib/hecks/bluebook/assembly/marks.rb +1 -0
  51. data/lib/hecks/bluebook/attribute.rb +1 -1
  52. data/lib/hecks/bluebook/behaviour/command.rb +13 -8
  53. data/lib/hecks/bluebook/behaviour/policy.rb +30 -0
  54. data/lib/hecks/bluebook/capabilities/attribute_values.rb +72 -0
  55. data/lib/hecks/bluebook/capabilities.rb +91 -9
  56. data/lib/hecks/bluebook/domain_port.rb +16 -4
  57. data/lib/hecks/bluebook/dsl/adapter_builder.rb +18 -6
  58. data/lib/hecks/bluebook/dsl/binding_proxy.rb +13 -0
  59. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/computed_sources.rb +98 -0
  60. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/fan_out_rows.rb +42 -0
  61. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/projected_fields.rb +14 -2
  62. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/provisions.rb +69 -3
  63. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/with_projections.rb +17 -10
  64. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +5 -0
  65. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
  66. data/lib/hecks/bluebook/dsl/command_builder/mutations.rb +8 -1
  67. data/lib/hecks/bluebook/dsl/command_builder/operands.rb +121 -0
  68. data/lib/hecks/bluebook/dsl/command_builder.rb +2 -0
  69. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +10 -1
  70. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +45 -2
  71. data/lib/hecks/bluebook/dsl/policy_builder.rb +47 -10
  72. data/lib/hecks/bluebook/dsl/rule_reference.rb +7 -4
  73. data/lib/hecks/bluebook/dsl/world_builder.rb +1 -1
  74. data/lib/hecks/bluebook/expression/ast_json/emitters.rb +6 -0
  75. data/lib/hecks/bluebook/expression/ast_json.rb +9 -6
  76. data/lib/hecks/bluebook/expression/ast_reader.rb +14 -9
  77. data/lib/hecks/bluebook/expression/projection.json +36 -0
  78. data/lib/hecks/bluebook/expression/resolver/operations.rb +31 -0
  79. data/lib/hecks/bluebook/expression/resolver/parsing.rb +38 -5
  80. data/lib/hecks/bluebook/expression/resolver/scanning.rb +22 -5
  81. data/lib/hecks/bluebook/expression/resolver/values.rb +10 -0
  82. data/lib/hecks/bluebook/expression/resolver.rb +20 -1
  83. data/lib/hecks/bluebook/hecksagon.rb +23 -3
  84. data/lib/hecks/bluebook/lifecycle.rb +22 -3
  85. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +13 -1
  86. data/lib/hecks/bluebook/meta_validator/modes.rb +4 -1
  87. data/lib/hecks/bluebook/meta_validator/readings.rb +18 -1
  88. data/lib/hecks/bluebook/meta_validator/reconstruction/heads.rb +2 -1
  89. data/lib/hecks/bluebook/meta_validator/shapes.rb +5 -0
  90. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +1 -1
  91. data/lib/hecks/bluebook/meta_validator.rb +12 -12
  92. data/lib/hecks/bluebook/model_check/ask_checks.rb +71 -0
  93. data/lib/hecks/bluebook/model_check/policy_checks.rb +2 -1
  94. data/lib/hecks/bluebook/model_check/reference_checks.rb +70 -0
  95. data/lib/hecks/bluebook/model_check.rb +8 -2
  96. data/lib/hecks/bluebook/policy.rb +4 -2
  97. data/lib/hecks/bluebook/smoke_test.rb +1 -1
  98. data/lib/hecks/bluebook/synthesizer.rb +2 -0
  99. data/lib/hecks/bluebook.rb +1 -0
  100. data/lib/hecks/cli/command_table.rb +1 -1
  101. data/lib/hecks/cli/console.rb +2 -1
  102. data/lib/hecks/cli/document.rb +1 -1
  103. data/lib/hecks/cli/launcher_words.rb +4 -4
  104. data/lib/hecks/cli/mcp.rb +4 -4
  105. data/lib/hecks/cli/project_cli/launcher_source.rb +2 -2
  106. data/lib/hecks/cli/project_cli/templates.rb +5 -5
  107. data/lib/hecks/cli/project_cli.rb +9 -6
  108. data/lib/hecks/cli/run.rb +2 -2
  109. data/lib/hecks/cli/smoke_http/checks.rb +4 -1
  110. data/lib/hecks/cli/smoke_http/transport.rb +3 -3
  111. data/lib/hecks/cli/smoke_http.rb +3 -0
  112. data/lib/hecks/codemod.rb +5 -2
  113. data/lib/hecks/corpus/routes.rb +7 -0
  114. data/lib/hecks/corpus/rust.rb +3 -2
  115. data/lib/hecks/deploy/bluebook/deploy.bluebook +124 -2
  116. data/lib/hecks/deploy/bluebook/deploy.hecksagon +8 -0
  117. data/lib/hecks/deploy/oidc.json +16 -3
  118. data/lib/hecks/doc/reference/rendering.rb +6 -3
  119. data/lib/hecks/forms/app/commands.rb +8 -7
  120. data/lib/hecks/forms/app/queries.rb +2 -1
  121. data/lib/hecks/forms/app.rb +1 -1
  122. data/lib/hecks/forms/record_renderer.rb +1 -1
  123. data/lib/hecks/framework/bluebook/privacy.bluebook +11 -0
  124. data/lib/hecks/fuzzing/concurrent_dispatch.rb +4 -1
  125. data/lib/hecks/fuzzing/failure_triage.rb +2 -1
  126. data/lib/hecks/fuzzing/mutation/run.rb +11 -4
  127. data/lib/hecks/fuzzing/properties/catalog.rb +6 -4
  128. data/lib/hecks/fuzzing/properties/guaranteed_by_construction.rb +18 -11
  129. data/lib/hecks/fuzzing/properties/guards.rb +1 -1
  130. data/lib/hecks/fuzzing/self_consistency/saga_redelivery.rb +1 -1
  131. data/lib/hecks/fuzzing/self_consistency.rb +6 -2
  132. data/lib/hecks/fuzzing/shrinker.rb +3 -1
  133. data/lib/hecks/fuzzing/value_generator.rb +19 -5
  134. data/lib/hecks/gate/stages.yml +4 -10
  135. data/lib/hecks/grammar/evolve.rb +1 -1
  136. data/lib/hecks/grammar/expression_operators.json +372 -0
  137. data/lib/hecks/grammar/operators.rb +3 -1
  138. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +10 -10
  139. data/lib/hecks/hecks/adapters/codebase/inspection/rubycritic.rb +35 -0
  140. data/lib/hecks/hecks/adapters/codebase/inspection.rb +104 -0
  141. data/lib/hecks/hecks/adapters/codebase/publishing.rb +2 -1
  142. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +12 -2
  143. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +2 -1
  144. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +1 -1
  145. data/lib/hecks/hecks/adapters/cost_explorer.rb +8 -5
  146. data/lib/hecks/hecks/adapters/deploy_toolchain/clearing.rb +51 -0
  147. data/lib/hecks/hecks/adapters/deploy_toolchain/statuses.rb +2 -1
  148. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +2 -0
  149. data/lib/hecks/hecks/adapters/finding_github.rb +13 -6
  150. data/lib/hecks/hecks/adapters/first_admin.rb +2 -2
  151. data/lib/hecks/hecks/adapters/github_rulesets.rb +6 -3
  152. data/lib/hecks/hecks/adapters/in_process_boot.rb +1 -1
  153. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +1 -1
  154. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +10 -20
  155. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +28 -5
  156. data/lib/hecks/hecks/adapters/local_files.rb +2 -2
  157. data/lib/hecks/hecks/adapters/rehearsal/audit_report.rb +107 -0
  158. data/lib/hecks/hecks/adapters/rehearsal/census.rb +71 -0
  159. data/lib/hecks/hecks/adapters/rehearsal/declaration.rb +133 -0
  160. data/lib/hecks/hecks/adapters/rehearsal/environment.rb +83 -0
  161. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +142 -0
  162. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +96 -0
  163. data/lib/hecks/hecks/adapters/rehearsal/postgres_census.rb +100 -0
  164. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +69 -0
  165. data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +129 -0
  166. data/lib/hecks/hecks/adapters/rehearsal/rows.rb +41 -0
  167. data/lib/hecks/hecks/adapters/rehearsal/rust_audit.rb +83 -0
  168. data/lib/hecks/hecks/adapters/rehearsal/subject.rb +104 -0
  169. data/lib/hecks/hecks/adapters/rehearsal.rb +54 -0
  170. data/lib/hecks/hecks/adapters/rehearsal_store.adapter +7 -0
  171. data/lib/hecks/hecks/adapters/rehearsal_store.rb +81 -0
  172. data/lib/hecks/hecks/adapters/site_toolchain/role_prober.rb +78 -0
  173. data/lib/hecks/hecks/adapters/site_toolchain.rb +29 -4
  174. data/lib/hecks/hecks/adapters/terminal.rb +13 -11
  175. data/lib/hecks/hecks/adapters/world_clearing.rb +65 -0
  176. data/lib/hecks/hecks/codebase.behaviors +13 -1
  177. data/lib/hecks/hecks/codebase.bluebook +239 -5
  178. data/lib/hecks/hecks/custodian.behaviors +24 -6
  179. data/lib/hecks/hecks/custodian.bluebook +6 -1227
  180. data/lib/hecks/hecks/deploy.behaviors +1 -1
  181. data/lib/hecks/hecks/deploy.world +1 -3
  182. data/lib/hecks/hecks/hecks.behaviors +1 -1
  183. data/lib/hecks/hecks/hecks.hecksagon +61 -18
  184. data/lib/hecks/hecks/hecks.world +11 -10
  185. data/lib/hecks/hecks/rehearsal.bluebook +477 -0
  186. data/lib/hecks/hecks/site.behaviors +1 -1
  187. data/lib/hecks/hecks/site.world +0 -1
  188. data/lib/hecks/hecks/tooling.bluebook +1238 -0
  189. data/lib/hecks/indifferent_key.rb +20 -0
  190. data/lib/hecks/language/adapter.bluebook +13 -3
  191. data/lib/hecks/language/bluebook/aggregate.bluebook +23 -0
  192. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -0
  193. data/lib/hecks/language/bluebook/entity.bluebook +18 -0
  194. data/lib/hecks/language/bluebook/policy.bluebook +9 -1
  195. data/lib/hecks/language/bluebook/query.bluebook +1 -1
  196. data/lib/hecks/language/bluebook/vocabulary.bluebook +11 -4
  197. data/lib/hecks/language/hecksagon/domain_port.bluebook +1 -1
  198. data/lib/hecks/language/hecksagon/hecksagon.bluebook +22 -0
  199. data/lib/hecks/language/oidc.json +10 -0
  200. data/lib/hecks/language/port.bluebook +4 -4
  201. data/lib/hecks/literal.rb +12 -3
  202. data/lib/hecks/ports/key_vault.rb +7 -2
  203. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +21 -10
  204. data/lib/hecks/ports/persistence/plugins/era/postgres_era/boot.rb +4 -2
  205. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +2 -1
  206. data/lib/hecks/ports/persistence/repository_factory.rb +6 -3
  207. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +29 -0
  208. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +1 -1
  209. data/lib/hecks/projections/deploy/fargate/settings/tuning.rb +15 -0
  210. data/lib/hecks/projections/deploy/fargate/settings.rb +3 -1
  211. data/lib/hecks/projections/deploy/fargate/stack/readers.rb +1 -0
  212. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  213. data/lib/hecks/projections/deploy/shared.rb +5 -2
  214. data/lib/hecks/projections/deploy/templates/fargate/Makefile.tmpl +7 -4
  215. data/lib/hecks/projections/model/deviations.rb +2 -2
  216. data/lib/hecks/projections/model.rb +2 -2
  217. data/lib/hecks/projections/oidc.rb +13 -1
  218. data/lib/hecks/projections/shape.rb +5 -2
  219. data/lib/hecks/projections/site/cms_editor/attributes.rb +95 -0
  220. data/lib/hecks/projections/site/cms_editor/blocks.rb +157 -0
  221. data/lib/hecks/projections/site/cms_editor/checks.rb +77 -0
  222. data/lib/hecks/projections/site/cms_editor/clearing.rb +70 -0
  223. data/lib/hecks/projections/site/cms_editor/color.rb +124 -0
  224. data/lib/hecks/projections/site/cms_editor/composition.rb +42 -0
  225. data/lib/hecks/projections/site/cms_editor/destructive.rb +61 -0
  226. data/lib/hecks/projections/site/cms_editor/drafts.rb +102 -0
  227. data/lib/hecks/projections/site/cms_editor/hints.rb +74 -0
  228. data/lib/hecks/projections/site/cms_editor/icons.rb +86 -0
  229. data/lib/hecks/projections/site/cms_editor/key_kinds.rb +31 -0
  230. data/lib/hecks/projections/site/cms_editor/limits.rb +72 -0
  231. data/lib/hecks/projections/site/cms_editor/media.rb +85 -0
  232. data/lib/hecks/projections/site/cms_editor/moment.rb +34 -0
  233. data/lib/hecks/projections/site/cms_editor/palette.rb +115 -0
  234. data/lib/hecks/projections/site/cms_editor/pickers.rb +85 -0
  235. data/lib/hecks/projections/site/cms_editor/pretty.rb +40 -0
  236. data/lib/hecks/projections/site/cms_editor/preview.rb +91 -0
  237. data/lib/hecks/projections/site/cms_editor/relations.rb +92 -0
  238. data/lib/hecks/projections/site/cms_editor/scale_checks.rb +66 -0
  239. data/lib/hecks/projections/site/cms_editor/scheduling.rb +150 -0
  240. data/lib/hecks/projections/site/cms_editor/schema.rb +161 -0
  241. data/lib/hecks/projections/site/cms_editor/setting.rb +130 -0
  242. data/lib/hecks/projections/site/cms_editor/targets.rb +86 -0
  243. data/lib/hecks/projections/site/cms_editor/templates/package.json.tmpl +36 -0
  244. data/lib/hecks/projections/site/cms_editor/templates/src/app.ts.tmpl +370 -0
  245. data/lib/hecks/projections/site/cms_editor/templates/src/assets.ts.tmpl +98 -0
  246. data/lib/hecks/projections/site/cms_editor/templates/src/auth/membership.ts.tmpl +87 -0
  247. data/lib/hecks/projections/site/cms_editor/templates/src/auth/session.ts.tmpl +36 -0
  248. data/lib/hecks/projections/site/cms_editor/templates/src/auth/sso.ts.tmpl +20 -0
  249. data/lib/hecks/projections/site/cms_editor/templates/src/browser/app.css.tmpl +412 -0
  250. data/lib/hecks/projections/site/cms_editor/templates/src/browser/autosave.js.tmpl +144 -0
  251. data/lib/hecks/projections/site/cms_editor/templates/src/browser/blocks.js.tmpl +500 -0
  252. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_doc.js.tmpl +175 -0
  253. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_editor.js.tmpl +418 -0
  254. data/lib/hecks/projections/site/cms_editor/templates/src/browser/epoch.js.tmpl +61 -0
  255. data/lib/hecks/projections/site/cms_editor/templates/src/browser/forms.js.tmpl +210 -0
  256. data/lib/hecks/projections/site/cms_editor/templates/src/browser/lists.js.tmpl +40 -0
  257. data/lib/hecks/projections/site/cms_editor/templates/src/browser/main.js.tmpl +26 -0
  258. data/lib/hecks/projections/site/cms_editor/templates/src/browser/media_picker.js.tmpl +232 -0
  259. data/lib/hecks/projections/site/cms_editor/templates/src/browser/moments.js.tmpl +73 -0
  260. data/lib/hecks/projections/site/cms_editor/templates/src/browser/pickers.js.tmpl +152 -0
  261. data/lib/hecks/projections/site/cms_editor/templates/src/browser/preview.js.tmpl +98 -0
  262. data/lib/hecks/projections/site/cms_editor/templates/src/browser/shell.js.tmpl +140 -0
  263. data/lib/hecks/projections/site/cms_editor/templates/src/browser/toast.js.tmpl +66 -0
  264. data/lib/hecks/projections/site/cms_editor/templates/src/choices.ts.tmpl +99 -0
  265. data/lib/hecks/projections/site/cms_editor/templates/src/commands.ts.tmpl +133 -0
  266. data/lib/hecks/projections/site/cms_editor/templates/src/config.ts.tmpl +29 -0
  267. data/lib/hecks/projections/site/cms_editor/templates/src/flash.ts.tmpl +52 -0
  268. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +43 -0
  269. data/lib/hecks/projections/site/cms_editor/templates/src/media/handler.ts.tmpl +177 -0
  270. data/lib/hecks/projections/site/cms_editor/templates/src/media/multipart.ts.tmpl +53 -0
  271. data/lib/hecks/projections/site/cms_editor/templates/src/media/sniff.ts.tmpl +94 -0
  272. data/lib/hecks/projections/site/cms_editor/templates/src/media/storage.ts.tmpl +50 -0
  273. data/lib/hecks/projections/site/cms_editor/templates/src/names.ts.tmpl +36 -0
  274. data/lib/hecks/projections/site/cms_editor/templates/src/schema.ts.tmpl +214 -0
  275. data/lib/hecks/projections/site/cms_editor/templates/src/server.ts.tmpl +65 -0
  276. data/lib/hecks/projections/site/cms_editor/templates/src/ui/availability.ts.tmpl +15 -0
  277. data/lib/hecks/projections/site/cms_editor/templates/src/ui/block_list.js.tmpl +213 -0
  278. data/lib/hecks/projections/site/cms_editor/templates/src/ui/blocks.ts.tmpl +275 -0
  279. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_model.js.tmpl +123 -0
  280. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_parse.js.tmpl +320 -0
  281. data/lib/hecks/projections/site/cms_editor/templates/src/ui/detail.ts.tmpl +88 -0
  282. data/lib/hecks/projections/site/cms_editor/templates/src/ui/dialogs.ts.tmpl +43 -0
  283. data/lib/hecks/projections/site/cms_editor/templates/src/ui/fields.ts.tmpl +275 -0
  284. data/lib/hecks/projections/site/cms_editor/templates/src/ui/html.ts.tmpl +61 -0
  285. data/lib/hecks/projections/site/cms_editor/templates/src/ui/icons.ts.tmpl +15 -0
  286. data/lib/hecks/projections/site/cms_editor/templates/src/ui/input.ts.tmpl +92 -0
  287. data/lib/hecks/projections/site/cms_editor/templates/src/ui/links.ts.tmpl +100 -0
  288. data/lib/hecks/projections/site/cms_editor/templates/src/ui/moments.ts.tmpl +44 -0
  289. data/lib/hecks/projections/site/cms_editor/templates/src/ui/outline.ts.tmpl +63 -0
  290. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pages.ts.tmpl +213 -0
  291. data/lib/hecks/projections/site/cms_editor/templates/src/ui/paging.ts.tmpl +82 -0
  292. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pickers.ts.tmpl +110 -0
  293. data/lib/hecks/projections/site/cms_editor/templates/src/ui/preview.ts.tmpl +87 -0
  294. data/lib/hecks/projections/site/cms_editor/templates/src/ui/related.ts.tmpl +97 -0
  295. data/lib/hecks/projections/site/cms_editor/templates/src/ui/schedule.ts.tmpl +160 -0
  296. data/lib/hecks/projections/site/cms_editor/templates/src/ui/shell.ts.tmpl +211 -0
  297. data/lib/hecks/projections/site/cms_editor/templates/src/ui/table.ts.tmpl +151 -0
  298. data/lib/hecks/projections/site/cms_editor/templates/src/ui/theme.js.tmpl +16 -0
  299. data/lib/hecks/projections/site/cms_editor/templates/src/ui/values.ts.tmpl +69 -0
  300. data/lib/hecks/projections/site/cms_editor/templates/src/ui/words.ts.tmpl +97 -0
  301. data/lib/hecks/projections/site/cms_editor/templates/src/ui/world.ts.tmpl +35 -0
  302. data/lib/hecks/projections/site/cms_editor/templates/tsconfig.json.tmpl +17 -0
  303. data/lib/hecks/projections/site/cms_editor/theme.rb +29 -0
  304. data/lib/hecks/projections/site/cms_editor/theme_checks.rb +57 -0
  305. data/lib/hecks/projections/site/cms_editor.rb +133 -0
  306. data/lib/hecks/projections/site/role_probe.rb +74 -0
  307. data/lib/hecks/projector/cli_audience.rb +2 -1
  308. data/lib/hecks/projector/cli_projector/listing.rb +1 -1
  309. data/lib/hecks/projector/cli_projector/option_specs.rb +1 -1
  310. data/lib/hecks/projector/docs_projector/closing.rb +1 -1
  311. data/lib/hecks/projector/docs_projector.rb +1 -1
  312. data/lib/hecks/projector/exporter/bindings.rb +40 -10
  313. data/lib/hecks/projector/exporter/capability_values.rb +55 -0
  314. data/lib/hecks/projector/narrate_projector/reactions.rb +1 -1
  315. data/lib/hecks/projector/target.rb +11 -0
  316. data/lib/hecks/projector.rb +32 -5
  317. data/lib/hecks/quality_control/adapters/github_checks.rb +12 -8
  318. data/lib/hecks/quality_control/adapters/github_issues.rb +8 -4
  319. data/lib/hecks/quality_control/cli/qa_open_pr.rb +2 -2
  320. data/lib/hecks/quality_control/cli/qa_sweep/checks/shrinking.rb +1 -1
  321. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +1 -1
  322. data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
  323. data/lib/hecks/quality_control/quality_control.bluebook +6 -2
  324. data/lib/hecks/release/approval.rb +64 -0
  325. data/lib/hecks/router/{door.rb → aggregate_reader.rb} +5 -4
  326. data/lib/hecks/router/namespace_installer.rb +16 -16
  327. data/lib/hecks/router.rb +1 -1
  328. data/lib/hecks/runtime/best_effort.rb +22 -0
  329. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -2
  330. data/lib/hecks/runtime/command_interpreter.rb +3 -2
  331. data/lib/hecks/runtime/command_rules/emission.rb +1 -1
  332. data/lib/hecks/runtime/dispatcher.rb +4 -4
  333. data/lib/hecks/runtime/entity_element/location.rb +1 -1
  334. data/lib/hecks/runtime/event.rb +5 -0
  335. data/lib/hecks/runtime/identity.rb +7 -6
  336. data/lib/hecks/runtime/interpreting.rb +2 -1
  337. data/lib/hecks/runtime/invocation_builder.rb +1 -1
  338. data/lib/hecks/runtime/loader/boot_steps.rb +8 -5
  339. data/lib/hecks/runtime/loader.rb +10 -10
  340. data/lib/hecks/runtime/policy_interpreter/arguments.rb +7 -4
  341. data/lib/hecks/runtime/policy_interpreter/fan_out.rb +3 -3
  342. data/lib/hecks/runtime/policy_interpreter.rb +6 -6
  343. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -1
  344. data/lib/hecks/runtime/reaction_invocation.rb +1 -1
  345. data/lib/hecks/runtime/rebuild_sweep.rb +11 -1
  346. data/lib/hecks/runtime/registry/ask_verification.rb +35 -0
  347. data/lib/hecks/runtime/registry/hecksagon_rules.rb +10 -0
  348. data/lib/hecks/runtime/registry/merging.rb +1 -1
  349. data/lib/hecks/runtime/registry/providers.rb +16 -0
  350. data/lib/hecks/runtime/registry/read_repository.rb +6 -4
  351. data/lib/hecks/runtime/registry/verification.rb +4 -1
  352. data/lib/hecks/runtime/registry/world_defaults.rb +29 -0
  353. data/lib/hecks/runtime/registry.rb +1 -1
  354. data/lib/hecks/runtime/saga_interpreter/compensation.rb +1 -1
  355. data/lib/hecks/runtime/saga_interpreter/delivery.rb +4 -4
  356. data/lib/hecks/runtime/saga_interpreter/transitions.rb +1 -1
  357. data/lib/hecks/runtime/saga_interpreter.rb +3 -3
  358. data/lib/hecks/runtime/tenant_check.rb +5 -4
  359. data/lib/hecks/runtime/value/coercion.rb +2 -2
  360. data/lib/hecks/runtime/value/validation.rb +16 -4
  361. data/lib/hecks/runtime.rb +9 -9
  362. data/lib/hecks/rust_build/project_rust/target_ir.rb +1 -1
  363. data/lib/hecks/rust_build/wasm.rb +1 -1
  364. data/lib/hecks/rust_build/wasm_browser.rb +1 -1
  365. data/lib/hecks/rust_build.rb +11 -0
  366. data/lib/hecks/served_options.rb +53 -0
  367. data/lib/hecks/site/bluebook/site.bluebook +29 -1
  368. data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
  369. data/lib/hecks/storehouse/audit.rb +1 -1
  370. data/lib/hecks/storehouse/dispatching.rb +8 -6
  371. data/lib/hecks/storehouse/introspection.rb +2 -2
  372. data/lib/hecks/storehouse/querying.rb +6 -4
  373. data/lib/hecks/storehouse.rb +4 -4
  374. data/lib/hecks/tools/comment_style/source_file.rb +4 -1
  375. data/lib/hecks/tools/deploy_recipe_lint/fixtures.rb +4 -1
  376. data/lib/hecks/tools/lanes/rows.rb +6 -0
  377. data/lib/hecks/tools/oidc_manifests.rb +4 -5
  378. data/lib/hecks/tools/regeneration_run.rb +11 -4
  379. data/lib/hecks/tools/site_routes/cli.rb +1 -1
  380. data/lib/hecks/tools/site_routes/editor_files.rb +45 -0
  381. data/lib/hecks/tools/site_routes.rb +14 -7
  382. data/lib/hecks/tools/tenant_provisioning.rb +2 -2
  383. data/lib/hecks/tools/tools_doc.rb +4 -4
  384. data/lib/hecks/version.rb +1 -1
  385. data/lib/hecks/vocabulary.rb +4 -4
  386. data/lib/hecks.rb +15 -15
  387. data/lib/rubocop/cop/hecks/env_dump.rb +69 -0
  388. data/lib/rubocop/cop/hecks/monotonic_duration.rb +53 -0
  389. data/lib/rubocop/cop/hecks/no_wall_clock_in_runtime.rb +34 -0
  390. data/lib/rubocop/cop/hecks/raw_shell_interpolation.rb +79 -0
  391. data/lib/rubocop/cop/hecks/sleep_in_spec.rb +34 -0
  392. data/lib/rubocop/cop/hecks/swallowed_rescue.rb +37 -0
  393. data/lib/rubocop/cop/hecks/symbol_string_key_mix.rb +50 -0
  394. data/lib/rubocop/cop/hecks/untyped_raise.rb +37 -0
  395. data/prebuilt/05c358d38d661a3774f813d49c66f5b68939af94432d66257a56f49ee2598994.marshal +0 -0
  396. data/prebuilt/26c40c61f09b09b11651c64668eda3ceb180f83cf2a3802d3033d2fd99ac841b.marshal +0 -0
  397. data/prebuilt/verdicts-230ac18c25abfd3d92ede032bee2661a4c2cd2773ab0cf0e2ae52546ba2f7af9.json +1 -0
  398. data/rust/.gitignore +6 -0
  399. data/rust/Cargo.toml +9 -1
  400. data/rust/build/src/binding_pass.rs +103 -0
  401. data/rust/build/src/cargo_sync.rs +6 -0
  402. data/rust/build/src/lineage_pass.rs +3 -3
  403. data/rust/build/src/main.rs +17 -5
  404. data/rust/build/src/pipeline.rs +48 -5
  405. data/rust/build/src/seams_pass.rs +123 -0
  406. data/rust/build/src/translation_pass.rs +167 -0
  407. data/rust/codegen/src/asks.rs +103 -0
  408. data/rust/codegen/src/bridging.rs +1 -1
  409. data/rust/codegen/src/commands.rs +13 -8
  410. data/rust/codegen/src/constraints.rs +25 -0
  411. data/rust/codegen/src/domain_generator.rs +11 -9
  412. data/rust/codegen/src/expr_emitter.rs +13 -0
  413. data/rust/codegen/src/json_codec.rs +54 -15
  414. data/rust/codegen/src/main.rs +2 -1
  415. data/rust/codegen/src/mutations.rs +174 -4
  416. data/rust/codegen/src/naming.rs +30 -0
  417. data/rust/codegen/src/queries.rs +11 -2
  418. data/rust/codegen/src/reactions.rs +1 -1
  419. data/rust/codegen/src/types.rs +166 -8
  420. data/rust/host/HECKS_RELEASE +1 -1
  421. data/rust/host/src/api.rs +4 -4
  422. data/rust/host/src/bin/validate_harness.rs +43 -0
  423. data/rust/host/src/boundary_fuzz/expr_json.rs +2 -2
  424. data/rust/host/src/boundary_fuzz/ir.rs +1 -0
  425. data/rust/host/src/boundary_fuzz/server.rs +1 -1
  426. data/rust/host/src/checkout.rs +35 -19
  427. data/rust/host/src/commerce_ir.rs +408 -2
  428. data/rust/host/src/expr_json.rs +118 -0
  429. data/rust/host/src/mint.rs +1 -1
  430. data/rust/host/src/payments/tests.rs +27 -1
  431. data/rust/host/src/payments.rs +12 -4
  432. data/rust/host/src/query_step.rs +22 -4
  433. data/rust/host/src/reference_validate.rs +653 -108
  434. data/rust/host/src/resend.rs +2 -0
  435. data/rust/host/src/server.rs +19 -6
  436. data/rust/host/src/ui_schema.rs +132 -5
  437. data/rust/host/src/web/newsletter.rs +7 -8
  438. data/rust/host/src/web/newsletter_send.rs +38 -13
  439. data/rust/host/src/web/registrations.rs +229 -26
  440. data/rust/host/src/web.rs +3 -3
  441. data/rust/parser/src/emit.rs +38 -6
  442. data/rust/parser/src/expr/ast_json.rs +7 -1
  443. data/rust/parser/src/expr/resolver.rs +171 -6
  444. data/rust/parser/src/ir.rs +7 -0
  445. data/rust/parser/src/keywords.rs +24 -0
  446. data/rust/parser/src/main.rs +3 -0
  447. data/rust/parser/src/parse/aggregate.rs +2 -1
  448. data/rust/parser/src/parse/chapter.rs +2 -2
  449. data/rust/parser/src/parse/command.rs +2 -0
  450. data/rust/parser/src/parse/hecksagon.rs +5 -2
  451. data/rust/parser/src/parse/lifecycle.rs +81 -0
  452. data/rust/parser/src/parse/mod.rs +15 -1
  453. data/rust/parser/src/parse/policy.rs +13 -2
  454. data/rust/parser/src/parse/value_object.rs +13 -8
  455. data/rust/parser/src/ruby_value.rs +1 -1
  456. data/rust/src/exemplar/commands.rs +4 -4
  457. data/rust/src/exemplar/constraints.rs +1 -1
  458. data/rust/src/exemplar/json.rs +43 -1
  459. data/rust/src/kernel/cli.rs +7 -5
  460. data/rust/src/kernel/dispatch.rs +2 -2
  461. data/rust/src/kernel/dispatch_guard_tests.rs +1 -1
  462. data/rust/src/kernel/expr.rs +73 -4
  463. data/rust/src/kernel/expression_operators/arithmetic.rs +130 -5
  464. data/rust/src/kernel/expression_operators/presence.rs +41 -3
  465. data/rust/src/kernel/expression_operators/text.rs +26 -2
  466. data/rust/src/kernel/json.rs +15 -0
  467. data/rust/src/kernel/mod.rs +1 -1
  468. data/rust/src/kernel/orchestrate.rs +54 -5
  469. data/rust/src/kernel/orchestrate_guard_tests.rs +67 -0
  470. data/rust/src/kernel/pattern.rs +60 -0
  471. data/rust/src/kernel/reference_lookup.rs +90 -7
  472. metadata +174 -36
  473. data/lib/hecks/doors/cli_door/nesting.rb +0 -34
  474. data/lib/hecks/doors/cli_door/shorthand.rb +0 -85
  475. data/lib/hecks/doors/cli_door.rb +0 -124
  476. data/lib/hecks/doors/cli_runner/answers.rb +0 -138
  477. data/lib/hecks/doors/cli_runner/resolution.rb +0 -146
  478. data/lib/hecks/doors/cli_runner/settling.rb +0 -115
  479. data/lib/hecks/doors/cli_runner/streaming.rb +0 -90
  480. data/lib/hecks/doors/cli_runner/suggestions.rb +0 -43
  481. data/lib/hecks/doors/cli_runner.rb +0 -66
  482. data/lib/hecks/doors/command_request.rb +0 -119
  483. data/lib/hecks/doors/handle.rb +0 -208
  484. data/lib/hecks/doors/json_door.rb +0 -132
  485. data/lib/hecks/doors/launcher_options.rb +0 -247
  486. data/lib/hecks/doors/mcp_door/properties.rb +0 -47
  487. data/lib/hecks/doors/mcp_door/tool_calls.rb +0 -113
  488. data/lib/hecks/doors/mcp_door/tool_definitions/auditing.rb +0 -51
  489. data/lib/hecks/doors/mcp_door/tool_definitions/calling.rb +0 -69
  490. data/lib/hecks/doors/mcp_door/tool_definitions/reading.rb +0 -97
  491. data/lib/hecks/doors/mcp_door/tool_definitions.rb +0 -15
  492. data/lib/hecks/doors/mcp_door.rb +0 -156
  493. data/lib/hecks/doors/mcp_door_scope/argument_policy.rb +0 -92
  494. data/lib/hecks/doors/mcp_door_scope/env_problems.rb +0 -55
  495. data/lib/hecks/doors/mcp_door_scope/presenter.rb +0 -78
  496. data/lib/hecks/doors/mcp_door_scope.rb +0 -201
  497. data/lib/hecks/doors/mcp_guide_cache.rb +0 -75
  498. data/lib/hecks/doors/ruby_door/aggregate_door.rb +0 -162
  499. data/lib/hecks/doors/ruby_door/chapter.rb +0 -89
  500. data/lib/hecks/doors/ruby_door.rb +0 -80
  501. data/lib/hecks/doors/usage_cache.rb +0 -139
  502. data/prebuilt/0ee7f83cba59ce7d1c45c65ba166b071779f70143449f584f33ff7989f64eccb.marshal +0 -0
  503. data/prebuilt/6e4a6bcf0aa880e2e6b056e1846b70e5a768090b9dc998bbadeaf5d6bcfd3db8.marshal +0 -0
  504. data/prebuilt/verdicts-91aaf93ce8981bd9a754948f075d95650dc2ec6435015376f5c60992b36a24b1.json +0 -1
@@ -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
+ }
@@ -0,0 +1,211 @@
1
+ __BANNER__
2
+
3
+ // The frame of every page: skip link, header (breadcrumbs, who is signed in, theme and sign-out),
4
+ // the navigation drawer and the main column. A page is built without knowing who is looking, so the
5
+ // frame leaves marked places (`<!--editor:...-->`) for what belongs to the request: the person, their
6
+ // role, the counts in the navigation and the notice. `decorate` fills them in, escaping everything;
7
+ // the marker text is the only markup it ever inserts raw, and a marker cannot be written by the
8
+ // domain, because `<` is escaped in everything it supplies.
9
+ //
10
+ // The navigation is a daisyUI drawer: open beside the page on a wide screen, and behind the menu
11
+ // button on a narrow one. It works with no script (the checkbox is the state); the script adds the
12
+ // button, the focus handling and Escape.
13
+ import { EDITOR } from "../config.ts";
14
+ import { at, keyOf, mayEdit } from "../names.ts";
15
+ import { SCHEMA } from "../schema.ts";
16
+ import type { Aggregate } from "../schema.ts";
17
+ import { assetUrl, hasLogo, isBuilt } from "../assets.ts";
18
+ import type { Flash } from "../flash.ts";
19
+ import { esc, href } from "./html.ts";
20
+ import { SPRITE, icon } from "./icons.ts";
21
+ import { schedulingAggregate } from "./world.ts";
22
+ import { label, plural } from "./words.ts";
23
+
24
+ /** One step of the trail above a page's title; the last has no address. */
25
+ export type Crumb = [label: string, to?: string];
26
+
27
+ export interface Frame {
28
+ /** The aggregate whose page this is, for the navigation's current item; none for the overview. */
29
+ active?: string;
30
+ crumbs?: Crumb[];
31
+ /** A drawer the page opens over itself, such as the preview; it is placed after the navigation. */
32
+ drawer?: string;
33
+ }
34
+
35
+ /** What the navigation holds the screen of scheduled actions under. */
36
+ export const SCHEDULED = "__scheduled";
37
+
38
+ const MARK = {
39
+ person: "<!--editor:person-->",
40
+ role: "<!--editor:role-->",
41
+ initial: "<!--editor:initial-->",
42
+ flash: "<!--editor:flash-->",
43
+ count: (name: string) => `<!--editor:count:${name}-->`,
44
+ };
45
+
46
+ /** The place a page leaves for the number of instances an aggregate holds, by `keyOf`. */
47
+ export const countMark = MARK.count;
48
+
49
+ /** `html` as the part of a page only the people who may edit `agg` see: `decorate` cuts it out for the rest. */
50
+ export const onlyFor = (agg: Aggregate, html: string): string => `<!--editor:only:${keyOf(agg)}-->${html}<!--editor:/only:${keyOf(agg)}-->`;
51
+
52
+ /** `html` as the part of a page that belongs to a chapter, cut out when the person may edit none of it. */
53
+ export const groupOf = (chapter: string, html: string): string => `<!--editor:group:${chapter}-->${html}<!--editor:/group:${chapter}-->`;
54
+
55
+ /** The chapters in the order they are shown, each with the aggregates it holds; one nameless group for an editor of one chapter. */
56
+ export function groups(): [chapter: string | null, aggregates: Aggregate[]][] {
57
+ if (!SCHEMA.chapters) return [[null, SCHEMA.aggregates]];
58
+ return SCHEMA.chapters.map((chapter): [string, Aggregate[]] => [chapter, SCHEMA.aggregates.filter((agg) => agg.chapter === chapter)]);
59
+ }
60
+
61
+ function brandMark(extra: string): string {
62
+ const logo = hasLogo() ? `<img class="size-7 rounded object-contain" src="${esc(assetUrl("logo"))}" alt="" width="28" height="28">` : "";
63
+ return `<a class="${extra}" href="${esc(EDITOR.basePath)}">${logo}<span class="truncate">${esc(EDITOR.brand)}</span></a>`;
64
+ }
65
+
66
+ function navLink(to: string, text: string, current: boolean, extra = ""): string {
67
+ return `<li><a href="${esc(to)}"${current ? ' aria-current="page"' : ""}><span class="truncate">${esc(text)}</span>${extra}</a></li>`;
68
+ }
69
+
70
+ function navItems(aggregates: Aggregate[], active: string | undefined): string {
71
+ return aggregates
72
+ .map((agg) => onlyFor(agg, navLink(href(...at(agg)), plural(agg.name), keyOf(agg) === active, `<span class="ml-auto text-xs text-muted tabular-nums">${MARK.count(keyOf(agg))}</span>`)))
73
+ .join("\n");
74
+ }
75
+
76
+ /** The link to the screen of scheduled actions, for the people who may edit them; nothing when the editor has none. */
77
+ function scheduledLink(active: string | undefined): string {
78
+ const sched = schedulingAggregate();
79
+ return sched ? `${onlyFor(sched, navLink(href("scheduled"), "Scheduled", active === SCHEDULED))}\n` : "";
80
+ }
81
+
82
+ function navigation(active: string | undefined): string {
83
+ const sections = groups().map(([chapter, aggregates]) => {
84
+ const heading = `<li class="menu-title mt-4 px-3 text-muted">${chapter === null ? "Content" : esc(label(chapter))}</li>`;
85
+ const section = `${heading}\n${navItems(aggregates, active)}`;
86
+ return chapter === null ? section : groupOf(chapter, section);
87
+ });
88
+ return `<nav id="rail" aria-label="Sections" class="flex-1 overflow-y-auto">
89
+ <ul class="menu w-full gap-0.5 p-3">
90
+ ${navLink(EDITOR.basePath, "Overview", active === undefined)}
91
+ ${scheduledLink(active)}${sections.join("\n")}
92
+ </ul>
93
+ </nav>`;
94
+ }
95
+
96
+ function trail(crumbs: Crumb[]): string {
97
+ const steps = crumbs.map(([text, to], at) => {
98
+ const last = at === crumbs.length - 1;
99
+ return `<li>${to && !last ? `<a href="${esc(to)}">${esc(text)}</a>` : `<span${last ? ' aria-current="page" class="font-semibold"' : ""}>${esc(text)}</span>`}</li>`;
100
+ });
101
+ return `<nav class="breadcrumbs hidden min-w-0 flex-1 text-sm text-muted md:block" aria-label="Breadcrumb"><ul>${steps.join("")}</ul></nav>`;
102
+ }
103
+
104
+ function header(crumbs: Crumb[]): string {
105
+ return `<header class="navbar sticky top-0 z-20 min-h-14 gap-2 border-b border-base-300 bg-base-100 px-3 lg:px-6">
106
+ <label for="nav-toggle" class="btn btn-ghost btn-square in-[.js]:hidden lg:hidden" aria-label="Open the menu">${icon("menu")}</label>
107
+ <button type="button" class="btn btn-ghost btn-square hidden in-[.js]:inline-flex lg:in-[.js]:hidden" data-nav-toggle aria-controls="rail" aria-expanded="false" aria-label="Open the menu">${icon("menu")}</button>
108
+ ${brandMark("mr-3 flex min-w-0 items-center gap-2 font-semibold lg:hidden")}
109
+ ${trail(crumbs)}
110
+ <div class="ml-auto flex items-center gap-1 sm:gap-2">
111
+ <button type="button" class="btn btn-ghost btn-square" data-theme-toggle aria-label="Switch theme" title="Switch theme">${icon("moon")}</button>
112
+ <p class="flex items-center gap-2"><span class="grid size-8 place-items-center rounded-full bg-soft font-semibold text-primary" aria-hidden="true">${MARK.initial}</span><span class="hidden leading-tight sm:grid"><span class="max-w-56 truncate font-semibold">${MARK.person}</span><span class="text-xs text-muted">${MARK.role}</span></span></p>
113
+ <form method="post" action="${esc(href("logout"))}"><button type="submit" class="btn btn-ghost">${icon("sign-out")}<span class="hidden sm:inline">Sign out</span></button></form>
114
+ </div>
115
+ </header>`;
116
+ }
117
+
118
+ /** The frame of every page: `body` is what goes in the main column. */
119
+ export function page(title: string, body: string, frame: Frame = {}): string {
120
+ const sheet = isBuilt() ? `<link rel="stylesheet" href="${esc(assetUrl("editor.css"))}">` : "";
121
+ const script = isBuilt() ? `<script type="module" src="${esc(assetUrl("editor.js"))}"></script>` : "";
122
+ return `<!doctype html>
123
+ <html lang="en">
124
+ <head>
125
+ <meta charset="utf-8">
126
+ <meta name="viewport" content="width=device-width, initial-scale=1">
127
+ <meta name="color-scheme" content="light dark">
128
+ <title>${esc(title)} - ${esc(EDITOR.title)}</title>
129
+ <script src="${esc(assetUrl("theme.js"))}"></script>
130
+ ${sheet}
131
+ </head>
132
+ <body class="min-h-screen bg-base-200 text-base-content">
133
+ ${SPRITE}
134
+ <a class="sr-only focus:not-sr-only focus:absolute focus:left-3 focus:top-3 focus:z-50 focus:rounded-field focus:bg-neutral focus:px-3 focus:py-2 focus:text-neutral-content" href="#main">Skip to the content</a>
135
+ <div class="drawer lg:drawer-open">
136
+ <input id="nav-toggle" type="checkbox" class="drawer-toggle" aria-hidden="true" tabindex="-1">
137
+ <div class="drawer-content min-w-0">
138
+ ${header(frame.crumbs ?? [])}
139
+ <main id="main" tabindex="-1" class="mx-auto w-full max-w-6xl px-4 pb-24 pt-6 lg:px-8 lg:pt-8">
140
+ ${body}
141
+ </main>
142
+ </div>
143
+ <div class="drawer-side z-30">
144
+ <label for="nav-toggle" aria-label="Close the menu" class="drawer-overlay"></label>
145
+ <aside class="flex min-h-full w-60 flex-col border-r border-base-300 bg-base-200">
146
+ <div class="flex h-14 items-center border-b border-base-300 bg-base-100 px-4">${brandMark("flex min-w-0 items-center gap-2 text-base font-semibold tracking-tight")}</div>
147
+ ${navigation(frame.active)}
148
+ </aside>
149
+ </div>
150
+ </div>
151
+ ${frame.drawer ?? ""}
152
+ <div class="toast toast-top toast-center z-50 mt-14" data-toasts>${MARK.flash}</div>
153
+ ${script}
154
+ </body>
155
+ </html>
156
+ `;
157
+ }
158
+
159
+ /** What belongs to this request and not to the page: who is signed in, the navigation's counts, a notice. */
160
+ export interface Context {
161
+ email: string;
162
+ role: string | null;
163
+ counts: Record<string, number>;
164
+ flash: Flash | null;
165
+ }
166
+
167
+ /** A notice's classes and mark by kind, and how it is announced: an error interrupts, the rest wait their turn. */
168
+ const NOTICES = {
169
+ success: ['alert alert-success alert-soft', "success", "status"],
170
+ error: ['alert alert-error alert-soft', "alert", "alert"],
171
+ info: ['alert alert-info alert-soft', "info", "status"],
172
+ } as const;
173
+
174
+ function notice(flash: Flash | null): string {
175
+ if (!flash) return "";
176
+ const [classes, mark, role] = NOTICES[flash.kind];
177
+ return `<div class="${classes}" role="${role}" data-toast>${icon(mark)}<p>${esc(flash.message)}</p></div>`;
178
+ }
179
+
180
+ /** `html` without each stretch from `open` to the next `close`. */
181
+ function cutBetween(html: string, open: string, close: string): string {
182
+ let text = html;
183
+ for (let start = text.indexOf(open); start !== -1; start = text.indexOf(open)) {
184
+ const end = text.indexOf(close, start);
185
+ text = end === -1 ? text.slice(0, start) + text.slice(start + open.length) : text.slice(0, start) + text.slice(end + close.length);
186
+ }
187
+ return text;
188
+ }
189
+
190
+ /** `html` without the parts only another role may see, and without the markers that held them. */
191
+ function visibleTo(html: string, role: string | null): string {
192
+ const hidden = SCHEMA.aggregates.filter((agg) => !mayEdit(agg, role));
193
+ const gone = hidden.map((agg): [string, string] => [`<!--editor:only:${keyOf(agg)}-->`, `<!--editor:/only:${keyOf(agg)}-->`]);
194
+ const empty = (SCHEMA.chapters ?? []).filter((chapter) => SCHEMA.aggregates.every((agg) => agg.chapter !== chapter || hidden.includes(agg)));
195
+ const groups = empty.map((chapter): [string, string] => [`<!--editor:group:${chapter}-->`, `<!--editor:/group:${chapter}-->`]);
196
+ const cut = [...gone, ...groups].reduce((text, [open, close]) => cutBetween(text, open, close), html);
197
+ return cut.replace(/<!--editor:\/?(?:only|group):[^>]*-->/g, "");
198
+ }
199
+
200
+ /** `html` with the places the frame left filled for this request. */
201
+ export function decorate(html: string, context: Context): string {
202
+ const counts = SCHEMA.aggregates.map((agg) => [MARK.count(keyOf(agg)), context.counts[keyOf(agg)] === undefined ? "" : String(context.counts[keyOf(agg)])] as const);
203
+ const fills: (readonly [string, string])[] = [
204
+ [MARK.person, esc(context.email)],
205
+ [MARK.role, esc(context.role ?? "")],
206
+ [MARK.initial, esc(context.email.charAt(0).toUpperCase())],
207
+ [MARK.flash, notice(context.flash)],
208
+ ...counts,
209
+ ];
210
+ return fills.reduce((text, [marker, value]) => text.split(marker).join(value), visibleTo(html, context.role));
211
+ }
@@ -0,0 +1,151 @@
1
+ __BANNER__
2
+
3
+ // An aggregate's list: a title with its primary command, the views the domain's queries give, a
4
+ // search and a status filter, and the instances as hairline rows, a page at a time (see paging.ts).
5
+ // Everything works without the script, and every page of a list is a link; the script adds the sort
6
+ // of the page's rows, a live narrowing as the search is typed and the filters it remembers. A list
7
+ // that is empty says so in one sentence and offers the command that makes the first one.
8
+ import { at } from "../names.ts";
9
+ import type { Aggregate, Attr, Command, Query } from "../schema.ts";
10
+ import { esc, href, refusal, statusBadge } from "./html.ts";
11
+ import { icon } from "./icons.ts";
12
+ import { fields } from "./fields.ts";
13
+ import { isMoment, momentTime, secondsOf } from "./moments.ts";
14
+ import { summary, summaryText } from "./outline.ts";
15
+ import { listHref, statesOf } from "./paging.ts";
16
+ import type { View } from "./paging.ts";
17
+ import type { Choices } from "./pickers.ts";
18
+ import { commandLabel, humanize, label, plural, singular } from "./words.ts";
19
+
20
+ type State = Record<string, unknown>;
21
+ type Row = [string, State];
22
+
23
+ const numeric = (attr: Attr): boolean => !attr.list && (attr.kind === "integer" || attr.kind === "number");
24
+
25
+ const columns = (agg: Aggregate): Attr[] => agg.attributes.filter((attr) => !attr.list && attr.widget !== "body" && attr.name !== agg.identity).slice(0, 4);
26
+
27
+ /** The most characters a cell shows before the rest is cut; the whole text stays in its title. */
28
+ const CLIP = 80;
29
+
30
+ function cell(attr: Attr, value: unknown): string {
31
+ const seconds = isMoment(attr) ? secondsOf(value) : null;
32
+ if (seconds !== null) return `<td data-value="${seconds}">${momentTime(seconds, attr.widget ?? "datetime")}</td>`;
33
+ const text = summaryText(value);
34
+ const long = text.length > CLIP;
35
+ const classes = numeric(attr) ? "text-right" : long ? "max-w-sm truncate" : "";
36
+ const title = long ? ` title="${esc(text)}"` : "";
37
+ return `<td class="${classes}"${title} data-value="${esc(text)}">${summary(value)}</td>`;
38
+ }
39
+
40
+ function head(attr: Attr | null, text: string): string {
41
+ const kind = attr && (numeric(attr) || isMoment(attr)) ? "number" : "text";
42
+ return `<th scope="col" class="${attr && numeric(attr) ? "text-right " : ""}whitespace-nowrap" data-sort="${kind}">${esc(text)}</th>`;
43
+ }
44
+
45
+ function rows(agg: Aggregate, found: Row[]): string {
46
+ const cols = columns(agg);
47
+ const heads = [head(null, label(agg.identity)), ...cols.map((attr) => head(attr, label(attr.name))), ...(agg.lifecycle ? [head(null, "Status")] : [])];
48
+ const lines = found.map(([id, state]) => {
49
+ const status = agg.lifecycle ? `<td data-value="${esc(state[agg.lifecycle.field])}">${statusBadge(agg, state[agg.lifecycle.field])}</td>` : "";
50
+ return `<tr class="hover:bg-base-200/60" data-row><th scope="row" class="font-normal" data-value="${esc(id)}"><a class="link link-hover font-semibold" href="${esc(href(...at(agg), "id", id))}">${esc(id)}</a></th>${cols.map((attr) => cell(attr, state[attr.name])).join("")}${status}</tr>`;
51
+ });
52
+ return `<div class="overflow-x-auto border-y border-base-300 bg-base-100 lg:overflow-visible">
53
+ <table class="table lg:[&_thead_th]:sticky lg:[&_thead_th]:top-14 lg:[&_thead_th]:z-10 [&_thead_th]:bg-base-100 [&_thead_th]:text-xs [&_thead_th]:text-muted" data-sortable>
54
+ <thead><tr>${heads.join("")}</tr></thead>
55
+ <tbody>
56
+ ${lines.join("\n")}
57
+ </tbody>
58
+ </table>
59
+ </div>`;
60
+ }
61
+
62
+ function empty(agg: Aggregate, primary: Command | undefined, matching: boolean): string {
63
+ const noun = esc(humanize(plural(agg.name)));
64
+ const action = primary && !matching ? `<a class="btn btn-neutral" href="${esc(href(...at(agg), "new", primary.name))}">${icon("plus")}<span>${esc(commandLabel(agg, primary))}</span></a>` : "";
65
+ const text = matching ? `No ${noun} match.` : `No ${noun} yet.${primary ? " Add the first one." : ""}`;
66
+ return `<div class="grid justify-items-center gap-3 border-y border-base-300 bg-base-100 px-4 py-16 text-center text-muted">${icon("inbox")}<p class="text-lg text-base-content">${text}</p>${action}</div>`;
67
+ }
68
+
69
+ function views(agg: Aggregate, current: Query | null): string {
70
+ const link = (to: string, text: string, active: boolean, title = ""): string =>
71
+ `<a class="${active ? "tab tab-active font-semibold" : "tab"}" href="${esc(to)}"${active ? ' aria-current="page"' : ""}${title ? ` title="${esc(title)}"` : ""}>${esc(text)}</a>`;
72
+ const all = link(href(...at(agg)), "All", current === null);
73
+ const each = agg.queries.map((query) => link(`${href(...at(agg))}?query=${encodeURIComponent(query.name)}`, label(query.name), current?.name === query.name, query.description ?? ""));
74
+ return agg.queries.length === 0 ? "" : `<nav class="tabs tabs-border mb-4" aria-label="Views of ${esc(humanize(plural(agg.name)))}">${[all, ...each].join("")}</nav>`;
75
+ }
76
+
77
+ function queryForm(agg: Aggregate, query: Query | null, choices: Choices): string {
78
+ if (!query || query.attributes.length === 0) return "";
79
+ const note = query.description ? `<p class="mb-2 text-muted">${esc(query.description)}</p>` : "";
80
+ return `<form class="mb-6 max-w-xl" method="get" action="${esc(href(...at(agg)))}">${note}<input type="hidden" name="query" value="${esc(query.name)}">
81
+ ${fields(agg, query.attributes, {}, null, false, choices)}
82
+ <button type="submit" class="btn mt-3">${icon("search")}<span>Show matching</span></button></form>`;
83
+ }
84
+
85
+ /** What the count says: the rows there are, or the range of them this page shows, and that they match when filtered. */
86
+ function countText(agg: Aggregate, view: View): string {
87
+ const active = view.q !== "" || view.status !== "";
88
+ const noun = view.total === 1 ? singular(agg.name) : humanize(plural(agg.name));
89
+ const range = view.total > view.size ? `${view.from}–${view.to} of ${view.total}` : String(view.total);
90
+ return `${range} ${noun}${active ? " match" : ""}`;
91
+ }
92
+
93
+ function statusSelect(agg: Aggregate, view: View): string {
94
+ if (!agg.lifecycle) return "";
95
+ const states = statesOf(agg).map((state) => `<option value="${esc(state)}"${state === view.status ? " selected" : ""}>${esc(label(state))}</option>`);
96
+ return `<select class="select select-sm h-9 w-auto" name="_status" aria-label="Status"><option value="">Any status</option>${states.join("")}</select>`;
97
+ }
98
+
99
+ function filters(agg: Aggregate, view: View): string {
100
+ const keep = view.params.map(([key, value]) => `<input type="hidden" name="${esc(key)}" value="${esc(value)}">`).join("");
101
+ const active = view.q !== "" || view.status !== "";
102
+ const clear = active ? `<a class="btn btn-ghost btn-sm h-9" href="${esc(listHref(agg, view.params))}" data-clear-filters>Clear</a>` : "";
103
+ const noun = esc(humanize(plural(agg.name)));
104
+ return `<div class="mb-3 flex flex-wrap items-center justify-between gap-3">
105
+ <form class="flex min-w-0 flex-1 flex-wrap items-center gap-2" method="get" action="${esc(href(...at(agg)))}" role="search" aria-label="Search ${noun}" data-list-filters data-list="${esc(at(agg).join("/"))}">${keep}
106
+ <label class="input input-sm h-9 w-full max-w-sm items-center gap-2">${icon("search")}<span class="sr-only">Search ${noun}</span><input type="search" class="grow" name="_q" value="${esc(view.q)}" data-filter placeholder="Search" autocomplete="off"></label>
107
+ ${statusSelect(agg, view)}
108
+ <button type="submit" class="btn btn-sm h-9">Search</button>${clear}
109
+ </form>
110
+ <p class="tabular-nums text-muted" data-count data-text="${esc(countText(agg, view))}" aria-live="polite">${esc(countText(agg, view))}</p>
111
+ </div>`;
112
+ }
113
+
114
+ /** The links to the pages on either side, and which page this is; none when everything fits on one. */
115
+ function pager(agg: Aggregate, view: View): string {
116
+ if (view.pages <= 1) return "";
117
+ const step = (to: number, text: string, enabled: boolean): string =>
118
+ enabled
119
+ ? `<a class="join-item btn btn-sm" href="${esc(listHref(agg, [...view.params, ["_q", view.q], ["_status", view.status]], { _page: to === 1 ? "" : String(to) }))}" rel="${to < view.page ? "prev" : "next"}">${text}</a>`
120
+ : `<span class="join-item btn btn-sm btn-disabled" aria-disabled="true">${text}</span>`;
121
+ return `<nav class="mt-4 flex items-center justify-between gap-3" aria-label="Pages of ${esc(humanize(plural(agg.name)))}">
122
+ <p class="tabular-nums text-muted">Page ${view.page} of ${view.pages}</p>
123
+ <div class="join">${step(view.page - 1, "Previous", view.page > 1)}${step(view.page + 1, "Next", view.page < view.pages)}</div>
124
+ </nav>`;
125
+ }
126
+
127
+ function actions(agg: Aggregate): string {
128
+ const creating = agg.commands.filter((command) => command.creates);
129
+ const elsewhere = agg.commands.filter((command) => !command.creates && command.on !== agg.name);
130
+ const primary = (command: Command): string => `<a class="btn btn-neutral" href="${esc(href(...at(agg), "new", command.name))}">${icon("plus")}<span>${esc(commandLabel(agg, command))}</span></a>`;
131
+ const secondary = (command: Command, verb: string): string => `<a class="btn" href="${esc(href(...at(agg), verb, command.name))}"><span>${esc(commandLabel(agg, command))}</span></a>`;
132
+ return [...creating.map((command, index) => (index === 0 ? primary(command) : secondary(command, "new"))), ...elsewhere.map((command) => secondary(command, "run"))].join("\n");
133
+ }
134
+
135
+ /** An aggregate's list: the page of rows of the query asked, or of everything it holds, matching the filters in `view`. */
136
+ export function listPage(agg: Aggregate, found: Row[], query: Query | null, error: string | null, asked: boolean, view: View, choices: Choices = {}): string {
137
+ const primary = agg.commands.find((command) => command.creates);
138
+ const filtered = view.q !== "" || view.status !== "";
139
+ const nothing = view.unfiltered === 0 && !filtered;
140
+ const body = found.length > 0 ? rows(agg, found) : !asked || error ? "" : empty(agg, primary, query !== null || filtered);
141
+ const tools = asked && !error && !nothing ? filters(agg, view) : "";
142
+ return `<div class="mb-6 flex flex-wrap items-end justify-between gap-4">
143
+ <div><h1 class="text-3xl">${esc(plural(agg.name))}</h1>${agg.description ? `<p class="mt-1 max-w-prose text-muted">${esc(agg.description)}</p>` : ""}</div>
144
+ <div class="flex flex-wrap gap-2">${actions(agg)}</div>
145
+ </div>
146
+ ${views(agg, query)}
147
+ ${queryForm(agg, query, choices)}${refusal(error)}
148
+ ${tools}
149
+ ${body}
150
+ ${pager(agg, view)}`;
151
+ }
@@ -0,0 +1,16 @@
1
+ __BANNER__
2
+
3
+ // Applies the remembered theme before the page is painted, so a person who chose dark never sees a
4
+ // flash of light. It is a separate small file, loaded first, because a page may carry no inline
5
+ // script. The choice is kept in localStorage (see browser/shell.js); a browser that blocks storage
6
+ // simply follows the system setting.
7
+ (function () {
8
+ var root = document.documentElement;
9
+ root.classList.add("js");
10
+ try {
11
+ var saved = window.localStorage.getItem("editor-theme");
12
+ if (saved === "dark" || saved === "light") root.setAttribute("data-theme", "editor-" + saved);
13
+ } catch (error) {
14
+ /* storage is blocked: follow the system */
15
+ }
16
+ })();