hecks 3.10.0 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/bluebook/pizzas.bluebook +29 -0
  3. data/examples/pizzas/bluebook/pizzas.hecksagon +7 -0
  4. data/exe/hecks +4 -4
  5. data/lib/hecks/adapters/driven/d1.rb +2 -10
  6. data/lib/hecks/adapters/driven/heki.adapter +1 -1
  7. data/lib/hecks/adapters/driven/heki.rb +2 -12
  8. data/lib/hecks/adapters/driven/lambda.adapter +1 -1
  9. data/lib/hecks/adapters/driven/lambda.rb +3 -18
  10. data/lib/hecks/adapters/driven/postgres.rb +4 -11
  11. data/lib/hecks/adapters/driven/sqlite.adapter +1 -1
  12. data/lib/hecks/adapters/driven/sqlite.rb +2 -10
  13. data/lib/hecks/adapters/driving/admission.rb +46 -0
  14. data/lib/hecks/adapters/driving/cli/nesting.rb +36 -0
  15. data/lib/hecks/adapters/driving/cli/shorthand.rb +88 -0
  16. data/lib/hecks/adapters/driving/cli.rb +127 -0
  17. data/lib/hecks/adapters/driving/cli_runner/answers.rb +160 -0
  18. data/lib/hecks/adapters/driving/cli_runner/resolution.rb +155 -0
  19. data/lib/hecks/adapters/driving/cli_runner/settling.rb +120 -0
  20. data/lib/hecks/adapters/driving/cli_runner/streaming.rb +93 -0
  21. data/lib/hecks/adapters/driving/cli_runner/suggestions.rb +47 -0
  22. data/lib/hecks/adapters/driving/cli_runner.rb +69 -0
  23. data/lib/hecks/adapters/driving/command_request.rb +121 -0
  24. data/lib/hecks/adapters/driving/handle.rb +214 -0
  25. data/lib/hecks/adapters/driving/json.rb +136 -0
  26. data/lib/hecks/adapters/driving/launcher_options.rb +267 -0
  27. data/lib/hecks/adapters/driving/mcp/properties.rb +49 -0
  28. data/lib/hecks/adapters/driving/mcp/tool_calls.rb +116 -0
  29. data/lib/hecks/adapters/driving/mcp/tool_definitions/auditing.rb +53 -0
  30. data/lib/hecks/adapters/driving/mcp/tool_definitions/calling.rb +71 -0
  31. data/lib/hecks/adapters/driving/mcp/tool_definitions/reading.rb +99 -0
  32. data/lib/hecks/adapters/driving/mcp/tool_definitions.rb +18 -0
  33. data/lib/hecks/adapters/driving/mcp.rb +159 -0
  34. data/lib/hecks/adapters/driving/mcp_guide_cache.rb +78 -0
  35. data/lib/hecks/adapters/driving/mcp_scope/argument_policy.rb +97 -0
  36. data/lib/hecks/adapters/driving/mcp_scope/env_problems.rb +69 -0
  37. data/lib/hecks/adapters/driving/mcp_scope/presenter.rb +83 -0
  38. data/lib/hecks/adapters/driving/mcp_scope.rb +213 -0
  39. data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +171 -0
  40. data/lib/hecks/adapters/driving/ruby/chapter.rb +91 -0
  41. data/lib/hecks/adapters/driving/ruby.rb +83 -0
  42. data/lib/hecks/adapters/driving/usage_cache.rb +144 -0
  43. data/lib/hecks/{doors.rb → adapters/driving.rb} +10 -6
  44. data/lib/hecks/behaviors/expectations/boot.rb +1 -1
  45. data/lib/hecks/bench/ruby_runner.rb +1 -1
  46. data/lib/hecks/bluebook/ask_resolution.rb +130 -0
  47. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +8 -2
  48. data/lib/hecks/bluebook/assembly/contracts/rules.rb +4 -2
  49. data/lib/hecks/bluebook/assembly/contracts/structure.rb +3 -1
  50. data/lib/hecks/bluebook/assembly/marks.rb +1 -0
  51. data/lib/hecks/bluebook/attribute.rb +1 -1
  52. data/lib/hecks/bluebook/behaviour/command.rb +13 -8
  53. data/lib/hecks/bluebook/behaviour/policy.rb +30 -0
  54. data/lib/hecks/bluebook/capabilities/attribute_values.rb +72 -0
  55. data/lib/hecks/bluebook/capabilities.rb +91 -9
  56. data/lib/hecks/bluebook/domain_port.rb +16 -4
  57. data/lib/hecks/bluebook/dsl/adapter_builder.rb +18 -6
  58. data/lib/hecks/bluebook/dsl/binding_proxy.rb +13 -0
  59. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/computed_sources.rb +98 -0
  60. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/fan_out_rows.rb +42 -0
  61. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/projected_fields.rb +14 -2
  62. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/provisions.rb +69 -3
  63. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/with_projections.rb +17 -10
  64. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +5 -0
  65. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
  66. data/lib/hecks/bluebook/dsl/command_builder/mutations.rb +8 -1
  67. data/lib/hecks/bluebook/dsl/command_builder/operands.rb +121 -0
  68. data/lib/hecks/bluebook/dsl/command_builder.rb +2 -0
  69. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +10 -1
  70. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +45 -2
  71. data/lib/hecks/bluebook/dsl/policy_builder.rb +47 -10
  72. data/lib/hecks/bluebook/dsl/rule_reference.rb +7 -4
  73. data/lib/hecks/bluebook/dsl/world_builder.rb +1 -1
  74. data/lib/hecks/bluebook/expression/ast_json/emitters.rb +6 -0
  75. data/lib/hecks/bluebook/expression/ast_json.rb +9 -6
  76. data/lib/hecks/bluebook/expression/ast_reader.rb +14 -9
  77. data/lib/hecks/bluebook/expression/projection.json +36 -0
  78. data/lib/hecks/bluebook/expression/resolver/operations.rb +31 -0
  79. data/lib/hecks/bluebook/expression/resolver/parsing.rb +38 -5
  80. data/lib/hecks/bluebook/expression/resolver/scanning.rb +22 -5
  81. data/lib/hecks/bluebook/expression/resolver/values.rb +10 -0
  82. data/lib/hecks/bluebook/expression/resolver.rb +20 -1
  83. data/lib/hecks/bluebook/hecksagon.rb +23 -3
  84. data/lib/hecks/bluebook/lifecycle.rb +22 -3
  85. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +13 -1
  86. data/lib/hecks/bluebook/meta_validator/modes.rb +4 -1
  87. data/lib/hecks/bluebook/meta_validator/readings.rb +18 -1
  88. data/lib/hecks/bluebook/meta_validator/reconstruction/heads.rb +2 -1
  89. data/lib/hecks/bluebook/meta_validator/shapes.rb +5 -0
  90. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +1 -1
  91. data/lib/hecks/bluebook/meta_validator.rb +12 -12
  92. data/lib/hecks/bluebook/model_check/ask_checks.rb +71 -0
  93. data/lib/hecks/bluebook/model_check/policy_checks.rb +2 -1
  94. data/lib/hecks/bluebook/model_check/reference_checks.rb +70 -0
  95. data/lib/hecks/bluebook/model_check.rb +8 -2
  96. data/lib/hecks/bluebook/policy.rb +4 -2
  97. data/lib/hecks/bluebook/smoke_test.rb +1 -1
  98. data/lib/hecks/bluebook/synthesizer.rb +2 -0
  99. data/lib/hecks/bluebook.rb +1 -0
  100. data/lib/hecks/cli/command_table.rb +1 -1
  101. data/lib/hecks/cli/console.rb +2 -1
  102. data/lib/hecks/cli/document.rb +1 -1
  103. data/lib/hecks/cli/launcher_words.rb +4 -4
  104. data/lib/hecks/cli/mcp.rb +4 -4
  105. data/lib/hecks/cli/project_cli/launcher_source.rb +2 -2
  106. data/lib/hecks/cli/project_cli/templates.rb +5 -5
  107. data/lib/hecks/cli/project_cli.rb +9 -6
  108. data/lib/hecks/cli/run.rb +2 -2
  109. data/lib/hecks/cli/smoke_http/checks.rb +4 -1
  110. data/lib/hecks/cli/smoke_http/transport.rb +3 -3
  111. data/lib/hecks/cli/smoke_http.rb +3 -0
  112. data/lib/hecks/codemod.rb +5 -2
  113. data/lib/hecks/corpus/routes.rb +7 -0
  114. data/lib/hecks/corpus/rust.rb +3 -2
  115. data/lib/hecks/deploy/bluebook/deploy.bluebook +18 -2
  116. data/lib/hecks/doc/reference/rendering.rb +6 -3
  117. data/lib/hecks/forms/app/commands.rb +8 -7
  118. data/lib/hecks/forms/app/queries.rb +2 -1
  119. data/lib/hecks/forms/app.rb +1 -1
  120. data/lib/hecks/forms/record_renderer.rb +1 -1
  121. data/lib/hecks/framework/bluebook/privacy.bluebook +11 -0
  122. data/lib/hecks/fuzzing/concurrent_dispatch.rb +4 -1
  123. data/lib/hecks/fuzzing/failure_triage.rb +2 -1
  124. data/lib/hecks/fuzzing/mutation/run.rb +11 -4
  125. data/lib/hecks/fuzzing/properties/catalog.rb +6 -4
  126. data/lib/hecks/fuzzing/properties/guaranteed_by_construction.rb +18 -11
  127. data/lib/hecks/fuzzing/properties/guards.rb +1 -1
  128. data/lib/hecks/fuzzing/self_consistency/saga_redelivery.rb +1 -1
  129. data/lib/hecks/fuzzing/self_consistency.rb +6 -2
  130. data/lib/hecks/fuzzing/shrinker.rb +3 -1
  131. data/lib/hecks/fuzzing/value_generator.rb +19 -5
  132. data/lib/hecks/gate/stages.yml +4 -10
  133. data/lib/hecks/grammar/evolve.rb +1 -1
  134. data/lib/hecks/grammar/expression_operators.json +372 -0
  135. data/lib/hecks/grammar/operators.rb +3 -1
  136. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +10 -10
  137. data/lib/hecks/hecks/adapters/codebase/inspection/rubycritic.rb +35 -0
  138. data/lib/hecks/hecks/adapters/codebase/inspection.rb +104 -0
  139. data/lib/hecks/hecks/adapters/codebase/publishing.rb +2 -1
  140. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +12 -2
  141. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +2 -1
  142. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +1 -1
  143. data/lib/hecks/hecks/adapters/cost_explorer.rb +8 -5
  144. data/lib/hecks/hecks/adapters/deploy_toolchain/statuses.rb +2 -1
  145. data/lib/hecks/hecks/adapters/finding_github.rb +13 -6
  146. data/lib/hecks/hecks/adapters/first_admin.rb +2 -2
  147. data/lib/hecks/hecks/adapters/github_rulesets.rb +6 -3
  148. data/lib/hecks/hecks/adapters/in_process_boot.rb +1 -1
  149. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +1 -1
  150. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +10 -20
  151. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +28 -5
  152. data/lib/hecks/hecks/adapters/local_files.rb +2 -2
  153. data/lib/hecks/hecks/adapters/rehearsal/audit_report.rb +107 -0
  154. data/lib/hecks/hecks/adapters/rehearsal/census.rb +71 -0
  155. data/lib/hecks/hecks/adapters/rehearsal/declaration.rb +133 -0
  156. data/lib/hecks/hecks/adapters/rehearsal/environment.rb +83 -0
  157. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +142 -0
  158. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +96 -0
  159. data/lib/hecks/hecks/adapters/rehearsal/postgres_census.rb +100 -0
  160. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +69 -0
  161. data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +129 -0
  162. data/lib/hecks/hecks/adapters/rehearsal/rows.rb +41 -0
  163. data/lib/hecks/hecks/adapters/rehearsal/rust_audit.rb +83 -0
  164. data/lib/hecks/hecks/adapters/rehearsal/subject.rb +104 -0
  165. data/lib/hecks/hecks/adapters/rehearsal.rb +54 -0
  166. data/lib/hecks/hecks/adapters/rehearsal_store.adapter +7 -0
  167. data/lib/hecks/hecks/adapters/rehearsal_store.rb +81 -0
  168. data/lib/hecks/hecks/adapters/terminal.rb +13 -11
  169. data/lib/hecks/hecks/codebase.behaviors +13 -1
  170. data/lib/hecks/hecks/codebase.bluebook +239 -5
  171. data/lib/hecks/hecks/custodian.behaviors +24 -6
  172. data/lib/hecks/hecks/custodian.bluebook +6 -1227
  173. data/lib/hecks/hecks/deploy.behaviors +1 -1
  174. data/lib/hecks/hecks/deploy.world +1 -3
  175. data/lib/hecks/hecks/hecks.behaviors +1 -1
  176. data/lib/hecks/hecks/hecks.hecksagon +53 -18
  177. data/lib/hecks/hecks/hecks.world +10 -10
  178. data/lib/hecks/hecks/rehearsal.bluebook +477 -0
  179. data/lib/hecks/hecks/site.behaviors +1 -1
  180. data/lib/hecks/hecks/site.world +0 -1
  181. data/lib/hecks/hecks/tooling.bluebook +1238 -0
  182. data/lib/hecks/indifferent_key.rb +20 -0
  183. data/lib/hecks/language/adapter.bluebook +13 -3
  184. data/lib/hecks/language/bluebook/aggregate.bluebook +23 -0
  185. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -0
  186. data/lib/hecks/language/bluebook/entity.bluebook +18 -0
  187. data/lib/hecks/language/bluebook/policy.bluebook +9 -1
  188. data/lib/hecks/language/bluebook/query.bluebook +1 -1
  189. data/lib/hecks/language/bluebook/vocabulary.bluebook +11 -4
  190. data/lib/hecks/language/hecksagon/domain_port.bluebook +1 -1
  191. data/lib/hecks/language/hecksagon/hecksagon.bluebook +22 -0
  192. data/lib/hecks/language/oidc.json +10 -0
  193. data/lib/hecks/language/port.bluebook +4 -4
  194. data/lib/hecks/literal.rb +12 -3
  195. data/lib/hecks/ports/key_vault.rb +7 -2
  196. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +21 -10
  197. data/lib/hecks/ports/persistence/plugins/era/postgres_era/boot.rb +4 -2
  198. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +2 -1
  199. data/lib/hecks/ports/persistence/repository_factory.rb +6 -3
  200. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +29 -0
  201. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +1 -1
  202. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  203. data/lib/hecks/projections/deploy/shared.rb +5 -2
  204. data/lib/hecks/projections/model/deviations.rb +2 -2
  205. data/lib/hecks/projections/model.rb +2 -2
  206. data/lib/hecks/projections/oidc.rb +13 -1
  207. data/lib/hecks/projections/shape.rb +5 -2
  208. data/lib/hecks/projections/site/cms_editor/attributes.rb +26 -6
  209. data/lib/hecks/projections/site/cms_editor/blocks.rb +157 -0
  210. data/lib/hecks/projections/site/cms_editor/checks.rb +14 -1
  211. data/lib/hecks/projections/site/cms_editor/clearing.rb +70 -0
  212. data/lib/hecks/projections/site/cms_editor/color.rb +124 -0
  213. data/lib/hecks/projections/site/cms_editor/composition.rb +42 -0
  214. data/lib/hecks/projections/site/cms_editor/destructive.rb +61 -0
  215. data/lib/hecks/projections/site/cms_editor/drafts.rb +102 -0
  216. data/lib/hecks/projections/site/cms_editor/hints.rb +74 -0
  217. data/lib/hecks/projections/site/cms_editor/icons.rb +86 -0
  218. data/lib/hecks/projections/site/cms_editor/key_kinds.rb +31 -0
  219. data/lib/hecks/projections/site/cms_editor/limits.rb +72 -0
  220. data/lib/hecks/projections/site/cms_editor/media.rb +1 -1
  221. data/lib/hecks/projections/site/cms_editor/moment.rb +34 -0
  222. data/lib/hecks/projections/site/cms_editor/palette.rb +115 -0
  223. data/lib/hecks/projections/site/cms_editor/pickers.rb +85 -0
  224. data/lib/hecks/projections/site/cms_editor/preview.rb +91 -0
  225. data/lib/hecks/projections/site/cms_editor/relations.rb +92 -0
  226. data/lib/hecks/projections/site/cms_editor/scale_checks.rb +66 -0
  227. data/lib/hecks/projections/site/cms_editor/scheduling.rb +150 -0
  228. data/lib/hecks/projections/site/cms_editor/schema.rb +96 -19
  229. data/lib/hecks/projections/site/cms_editor/setting.rb +52 -9
  230. data/lib/hecks/projections/site/cms_editor/targets.rb +86 -0
  231. data/lib/hecks/projections/site/cms_editor/templates/package.json.tmpl +18 -1
  232. data/lib/hecks/projections/site/cms_editor/templates/src/app.ts.tmpl +251 -57
  233. data/lib/hecks/projections/site/cms_editor/templates/src/assets.ts.tmpl +98 -0
  234. data/lib/hecks/projections/site/cms_editor/templates/src/auth/membership.ts.tmpl +32 -24
  235. data/lib/hecks/projections/site/cms_editor/templates/src/browser/app.css.tmpl +412 -0
  236. data/lib/hecks/projections/site/cms_editor/templates/src/browser/autosave.js.tmpl +144 -0
  237. data/lib/hecks/projections/site/cms_editor/templates/src/browser/blocks.js.tmpl +500 -0
  238. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_doc.js.tmpl +175 -0
  239. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_editor.js.tmpl +418 -0
  240. data/lib/hecks/projections/site/cms_editor/templates/src/browser/epoch.js.tmpl +61 -0
  241. data/lib/hecks/projections/site/cms_editor/templates/src/browser/forms.js.tmpl +210 -0
  242. data/lib/hecks/projections/site/cms_editor/templates/src/browser/lists.js.tmpl +40 -0
  243. data/lib/hecks/projections/site/cms_editor/templates/src/browser/main.js.tmpl +26 -0
  244. data/lib/hecks/projections/site/cms_editor/templates/src/browser/media_picker.js.tmpl +232 -0
  245. data/lib/hecks/projections/site/cms_editor/templates/src/browser/moments.js.tmpl +73 -0
  246. data/lib/hecks/projections/site/cms_editor/templates/src/browser/pickers.js.tmpl +152 -0
  247. data/lib/hecks/projections/site/cms_editor/templates/src/browser/preview.js.tmpl +98 -0
  248. data/lib/hecks/projections/site/cms_editor/templates/src/browser/shell.js.tmpl +140 -0
  249. data/lib/hecks/projections/site/cms_editor/templates/src/browser/toast.js.tmpl +66 -0
  250. data/lib/hecks/projections/site/cms_editor/templates/src/choices.ts.tmpl +99 -0
  251. data/lib/hecks/projections/site/cms_editor/templates/src/commands.ts.tmpl +78 -24
  252. data/lib/hecks/projections/site/cms_editor/templates/src/config.ts.tmpl +6 -0
  253. data/lib/hecks/projections/site/cms_editor/templates/src/flash.ts.tmpl +52 -0
  254. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +19 -2
  255. data/lib/hecks/projections/site/cms_editor/templates/src/media/handler.ts.tmpl +31 -8
  256. data/lib/hecks/projections/site/cms_editor/templates/src/names.ts.tmpl +36 -0
  257. data/lib/hecks/projections/site/cms_editor/templates/src/schema.ts.tmpl +131 -2
  258. data/lib/hecks/projections/site/cms_editor/templates/src/ui/availability.ts.tmpl +15 -0
  259. data/lib/hecks/projections/site/cms_editor/templates/src/ui/block_list.js.tmpl +213 -0
  260. data/lib/hecks/projections/site/cms_editor/templates/src/ui/blocks.ts.tmpl +275 -0
  261. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_model.js.tmpl +8 -5
  262. data/lib/hecks/projections/site/cms_editor/templates/src/ui/detail.ts.tmpl +88 -0
  263. data/lib/hecks/projections/site/cms_editor/templates/src/ui/dialogs.ts.tmpl +43 -0
  264. data/lib/hecks/projections/site/cms_editor/templates/src/ui/fields.ts.tmpl +229 -34
  265. data/lib/hecks/projections/site/cms_editor/templates/src/ui/html.ts.tmpl +40 -81
  266. data/lib/hecks/projections/site/cms_editor/templates/src/ui/icons.ts.tmpl +15 -0
  267. data/lib/hecks/projections/site/cms_editor/templates/src/ui/input.ts.tmpl +29 -12
  268. data/lib/hecks/projections/site/cms_editor/templates/src/ui/links.ts.tmpl +100 -0
  269. data/lib/hecks/projections/site/cms_editor/templates/src/ui/moments.ts.tmpl +44 -0
  270. data/lib/hecks/projections/site/cms_editor/templates/src/ui/outline.ts.tmpl +53 -17
  271. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pages.ts.tmpl +178 -88
  272. data/lib/hecks/projections/site/cms_editor/templates/src/ui/paging.ts.tmpl +82 -0
  273. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pickers.ts.tmpl +110 -0
  274. data/lib/hecks/projections/site/cms_editor/templates/src/ui/preview.ts.tmpl +87 -0
  275. data/lib/hecks/projections/site/cms_editor/templates/src/ui/related.ts.tmpl +97 -0
  276. data/lib/hecks/projections/site/cms_editor/templates/src/ui/schedule.ts.tmpl +160 -0
  277. data/lib/hecks/projections/site/cms_editor/templates/src/ui/shell.ts.tmpl +211 -0
  278. data/lib/hecks/projections/site/cms_editor/templates/src/ui/table.ts.tmpl +151 -0
  279. data/lib/hecks/projections/site/cms_editor/templates/src/ui/theme.js.tmpl +16 -0
  280. data/lib/hecks/projections/site/cms_editor/templates/src/ui/values.ts.tmpl +69 -0
  281. data/lib/hecks/projections/site/cms_editor/templates/src/ui/words.ts.tmpl +97 -0
  282. data/lib/hecks/projections/site/cms_editor/templates/src/ui/world.ts.tmpl +35 -0
  283. data/lib/hecks/projections/site/cms_editor/templates/tsconfig.json.tmpl +2 -1
  284. data/lib/hecks/projections/site/cms_editor/theme.rb +29 -0
  285. data/lib/hecks/projections/site/cms_editor/theme_checks.rb +57 -0
  286. data/lib/hecks/projections/site/cms_editor.rb +49 -23
  287. data/lib/hecks/projections/site/role_probe.rb +2 -0
  288. data/lib/hecks/projector/cli_audience.rb +2 -1
  289. data/lib/hecks/projector/cli_projector/listing.rb +1 -1
  290. data/lib/hecks/projector/cli_projector/option_specs.rb +1 -1
  291. data/lib/hecks/projector/docs_projector/closing.rb +1 -1
  292. data/lib/hecks/projector/docs_projector.rb +1 -1
  293. data/lib/hecks/projector/exporter/bindings.rb +40 -10
  294. data/lib/hecks/projector/exporter/capability_values.rb +55 -0
  295. data/lib/hecks/projector/narrate_projector/reactions.rb +1 -1
  296. data/lib/hecks/projector/target.rb +11 -0
  297. data/lib/hecks/projector.rb +32 -5
  298. data/lib/hecks/quality_control/adapters/github_checks.rb +12 -8
  299. data/lib/hecks/quality_control/adapters/github_issues.rb +8 -4
  300. data/lib/hecks/quality_control/cli/qa_open_pr.rb +2 -2
  301. data/lib/hecks/quality_control/cli/qa_sweep/checks/shrinking.rb +1 -1
  302. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +1 -1
  303. data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
  304. data/lib/hecks/quality_control/quality_control.bluebook +6 -2
  305. data/lib/hecks/release/approval.rb +64 -0
  306. data/lib/hecks/router/{door.rb → aggregate_reader.rb} +5 -4
  307. data/lib/hecks/router/namespace_installer.rb +16 -16
  308. data/lib/hecks/router.rb +1 -1
  309. data/lib/hecks/runtime/best_effort.rb +22 -0
  310. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -2
  311. data/lib/hecks/runtime/command_interpreter.rb +3 -2
  312. data/lib/hecks/runtime/command_rules/emission.rb +1 -1
  313. data/lib/hecks/runtime/dispatcher.rb +4 -4
  314. data/lib/hecks/runtime/entity_element/location.rb +1 -1
  315. data/lib/hecks/runtime/event.rb +5 -0
  316. data/lib/hecks/runtime/identity.rb +7 -6
  317. data/lib/hecks/runtime/interpreting.rb +2 -1
  318. data/lib/hecks/runtime/invocation_builder.rb +1 -1
  319. data/lib/hecks/runtime/loader/boot_steps.rb +8 -5
  320. data/lib/hecks/runtime/loader.rb +10 -10
  321. data/lib/hecks/runtime/policy_interpreter/arguments.rb +7 -4
  322. data/lib/hecks/runtime/policy_interpreter/fan_out.rb +3 -3
  323. data/lib/hecks/runtime/policy_interpreter.rb +6 -6
  324. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -1
  325. data/lib/hecks/runtime/reaction_invocation.rb +1 -1
  326. data/lib/hecks/runtime/rebuild_sweep.rb +11 -1
  327. data/lib/hecks/runtime/registry/ask_verification.rb +35 -0
  328. data/lib/hecks/runtime/registry/hecksagon_rules.rb +10 -0
  329. data/lib/hecks/runtime/registry/merging.rb +1 -1
  330. data/lib/hecks/runtime/registry/providers.rb +16 -0
  331. data/lib/hecks/runtime/registry/read_repository.rb +6 -4
  332. data/lib/hecks/runtime/registry/verification.rb +4 -1
  333. data/lib/hecks/runtime/registry/world_defaults.rb +29 -0
  334. data/lib/hecks/runtime/registry.rb +1 -1
  335. data/lib/hecks/runtime/saga_interpreter/compensation.rb +1 -1
  336. data/lib/hecks/runtime/saga_interpreter/delivery.rb +4 -4
  337. data/lib/hecks/runtime/saga_interpreter/transitions.rb +1 -1
  338. data/lib/hecks/runtime/saga_interpreter.rb +3 -3
  339. data/lib/hecks/runtime/tenant_check.rb +5 -4
  340. data/lib/hecks/runtime/value/coercion.rb +2 -2
  341. data/lib/hecks/runtime/value/validation.rb +16 -4
  342. data/lib/hecks/runtime.rb +9 -9
  343. data/lib/hecks/rust_build/project_rust/target_ir.rb +1 -1
  344. data/lib/hecks/rust_build/wasm.rb +1 -1
  345. data/lib/hecks/rust_build/wasm_browser.rb +1 -1
  346. data/lib/hecks/rust_build.rb +11 -0
  347. data/lib/hecks/served_options.rb +53 -0
  348. data/lib/hecks/site/bluebook/site.bluebook +1 -0
  349. data/lib/hecks/storehouse/audit.rb +1 -1
  350. data/lib/hecks/storehouse/dispatching.rb +8 -6
  351. data/lib/hecks/storehouse/introspection.rb +2 -2
  352. data/lib/hecks/storehouse/querying.rb +6 -4
  353. data/lib/hecks/storehouse.rb +4 -4
  354. data/lib/hecks/tools/comment_style/source_file.rb +4 -1
  355. data/lib/hecks/tools/deploy_recipe_lint/fixtures.rb +4 -1
  356. data/lib/hecks/tools/lanes/rows.rb +6 -0
  357. data/lib/hecks/tools/oidc_manifests.rb +4 -5
  358. data/lib/hecks/tools/regeneration_run.rb +11 -4
  359. data/lib/hecks/tools/site_routes/editor_files.rb +12 -5
  360. data/lib/hecks/tools/tenant_provisioning.rb +2 -2
  361. data/lib/hecks/tools/tools_doc.rb +4 -4
  362. data/lib/hecks/version.rb +1 -1
  363. data/lib/hecks/vocabulary.rb +4 -4
  364. data/lib/hecks.rb +15 -15
  365. data/lib/rubocop/cop/hecks/env_dump.rb +69 -0
  366. data/lib/rubocop/cop/hecks/monotonic_duration.rb +53 -0
  367. data/lib/rubocop/cop/hecks/no_wall_clock_in_runtime.rb +34 -0
  368. data/lib/rubocop/cop/hecks/raw_shell_interpolation.rb +79 -0
  369. data/lib/rubocop/cop/hecks/sleep_in_spec.rb +34 -0
  370. data/lib/rubocop/cop/hecks/swallowed_rescue.rb +37 -0
  371. data/lib/rubocop/cop/hecks/symbol_string_key_mix.rb +50 -0
  372. data/lib/rubocop/cop/hecks/untyped_raise.rb +37 -0
  373. data/prebuilt/05c358d38d661a3774f813d49c66f5b68939af94432d66257a56f49ee2598994.marshal +0 -0
  374. data/prebuilt/26c40c61f09b09b11651c64668eda3ceb180f83cf2a3802d3033d2fd99ac841b.marshal +0 -0
  375. data/prebuilt/verdicts-230ac18c25abfd3d92ede032bee2661a4c2cd2773ab0cf0e2ae52546ba2f7af9.json +1 -0
  376. data/rust/.gitignore +6 -0
  377. data/rust/Cargo.toml +6 -0
  378. data/rust/build/src/binding_pass.rs +103 -0
  379. data/rust/build/src/cargo_sync.rs +6 -0
  380. data/rust/build/src/lineage_pass.rs +3 -3
  381. data/rust/build/src/main.rs +17 -5
  382. data/rust/build/src/pipeline.rs +48 -5
  383. data/rust/build/src/seams_pass.rs +123 -0
  384. data/rust/build/src/translation_pass.rs +167 -0
  385. data/rust/codegen/src/asks.rs +103 -0
  386. data/rust/codegen/src/commands.rs +13 -8
  387. data/rust/codegen/src/constraints.rs +25 -0
  388. data/rust/codegen/src/domain_generator.rs +2 -0
  389. data/rust/codegen/src/expr_emitter.rs +13 -0
  390. data/rust/codegen/src/json_codec.rs +54 -15
  391. data/rust/codegen/src/main.rs +1 -0
  392. data/rust/codegen/src/mutations.rs +128 -3
  393. data/rust/codegen/src/queries.rs +11 -2
  394. data/rust/codegen/src/reactions.rs +1 -1
  395. data/rust/codegen/src/types.rs +166 -8
  396. data/rust/host/HECKS_RELEASE +1 -1
  397. data/rust/host/src/api.rs +4 -4
  398. data/rust/host/src/bin/validate_harness.rs +43 -0
  399. data/rust/host/src/boundary_fuzz/expr_json.rs +2 -2
  400. data/rust/host/src/boundary_fuzz/ir.rs +1 -0
  401. data/rust/host/src/boundary_fuzz/server.rs +1 -1
  402. data/rust/host/src/checkout.rs +35 -19
  403. data/rust/host/src/commerce_ir.rs +408 -2
  404. data/rust/host/src/expr_json.rs +118 -0
  405. data/rust/host/src/mint.rs +1 -1
  406. data/rust/host/src/payments/tests.rs +27 -1
  407. data/rust/host/src/payments.rs +12 -4
  408. data/rust/host/src/query_step.rs +22 -4
  409. data/rust/host/src/reference_validate.rs +653 -108
  410. data/rust/host/src/resend.rs +2 -0
  411. data/rust/host/src/server.rs +19 -6
  412. data/rust/host/src/ui_schema.rs +132 -5
  413. data/rust/host/src/web/newsletter.rs +7 -8
  414. data/rust/host/src/web/newsletter_send.rs +38 -13
  415. data/rust/host/src/web/registrations.rs +229 -26
  416. data/rust/host/src/web.rs +3 -3
  417. data/rust/parser/src/emit.rs +38 -6
  418. data/rust/parser/src/expr/ast_json.rs +7 -1
  419. data/rust/parser/src/expr/resolver.rs +171 -6
  420. data/rust/parser/src/ir.rs +7 -0
  421. data/rust/parser/src/keywords.rs +24 -0
  422. data/rust/parser/src/main.rs +3 -0
  423. data/rust/parser/src/parse/aggregate.rs +2 -1
  424. data/rust/parser/src/parse/chapter.rs +2 -2
  425. data/rust/parser/src/parse/command.rs +2 -0
  426. data/rust/parser/src/parse/hecksagon.rs +5 -2
  427. data/rust/parser/src/parse/lifecycle.rs +81 -0
  428. data/rust/parser/src/parse/mod.rs +15 -1
  429. data/rust/parser/src/parse/policy.rs +13 -2
  430. data/rust/parser/src/parse/value_object.rs +13 -8
  431. data/rust/parser/src/ruby_value.rs +1 -1
  432. data/rust/src/exemplar/commands.rs +4 -4
  433. data/rust/src/exemplar/constraints.rs +1 -1
  434. data/rust/src/exemplar/json.rs +43 -1
  435. data/rust/src/kernel/cli.rs +7 -5
  436. data/rust/src/kernel/dispatch.rs +2 -2
  437. data/rust/src/kernel/dispatch_guard_tests.rs +1 -1
  438. data/rust/src/kernel/expr.rs +66 -3
  439. data/rust/src/kernel/expression_operators/arithmetic.rs +130 -5
  440. data/rust/src/kernel/expression_operators/text.rs +26 -2
  441. data/rust/src/kernel/json.rs +15 -0
  442. data/rust/src/kernel/mod.rs +1 -1
  443. data/rust/src/kernel/orchestrate.rs +54 -5
  444. data/rust/src/kernel/orchestrate_guard_tests.rs +67 -0
  445. data/rust/src/kernel/pattern.rs +60 -0
  446. data/rust/src/kernel/reference_lookup.rs +90 -7
  447. metadata +140 -38
  448. data/lib/hecks/doors/cli_door/nesting.rb +0 -34
  449. data/lib/hecks/doors/cli_door/shorthand.rb +0 -85
  450. data/lib/hecks/doors/cli_door.rb +0 -124
  451. data/lib/hecks/doors/cli_runner/answers.rb +0 -138
  452. data/lib/hecks/doors/cli_runner/resolution.rb +0 -146
  453. data/lib/hecks/doors/cli_runner/settling.rb +0 -115
  454. data/lib/hecks/doors/cli_runner/streaming.rb +0 -90
  455. data/lib/hecks/doors/cli_runner/suggestions.rb +0 -43
  456. data/lib/hecks/doors/cli_runner.rb +0 -66
  457. data/lib/hecks/doors/command_request.rb +0 -119
  458. data/lib/hecks/doors/handle.rb +0 -208
  459. data/lib/hecks/doors/json_door.rb +0 -132
  460. data/lib/hecks/doors/launcher_options.rb +0 -247
  461. data/lib/hecks/doors/mcp_door/properties.rb +0 -47
  462. data/lib/hecks/doors/mcp_door/tool_calls.rb +0 -113
  463. data/lib/hecks/doors/mcp_door/tool_definitions/auditing.rb +0 -51
  464. data/lib/hecks/doors/mcp_door/tool_definitions/calling.rb +0 -69
  465. data/lib/hecks/doors/mcp_door/tool_definitions/reading.rb +0 -97
  466. data/lib/hecks/doors/mcp_door/tool_definitions.rb +0 -15
  467. data/lib/hecks/doors/mcp_door.rb +0 -156
  468. data/lib/hecks/doors/mcp_door_scope/argument_policy.rb +0 -92
  469. data/lib/hecks/doors/mcp_door_scope/env_problems.rb +0 -55
  470. data/lib/hecks/doors/mcp_door_scope/presenter.rb +0 -78
  471. data/lib/hecks/doors/mcp_door_scope.rb +0 -201
  472. data/lib/hecks/doors/mcp_guide_cache.rb +0 -75
  473. data/lib/hecks/doors/ruby_door/aggregate_door.rb +0 -162
  474. data/lib/hecks/doors/ruby_door/chapter.rb +0 -89
  475. data/lib/hecks/doors/ruby_door.rb +0 -80
  476. data/lib/hecks/doors/usage_cache.rb +0 -139
  477. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_widget.js.tmpl +0 -282
  478. data/lib/hecks/projections/site/cms_editor/templates/src/ui/media_picker.js.tmpl +0 -109
  479. data/prebuilt/c28d6d9db14eea3e17161e9d9c262efee30b256cf97693a7cfd552bce24b2f7f.marshal +0 -0
  480. data/prebuilt/cb375cba61ca0dbe8ddb9739c306510fe05e5f4b7a9b6955caff8224c63ea0e0.marshal +0 -0
  481. data/prebuilt/verdicts-d6d584ad43b5f3cebf88aafbbdbf024c38574acf18e06adffbc555f7786b1c2f.json +0 -1
@@ -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
+ })();
@@ -0,0 +1,69 @@
1
+ __BANNER__
2
+
3
+ // A record's values shown read-only, by shape: a moment in the viewer's zone, a rich-text body as
4
+ // typeset text, a key with a button that copies it, anything else as `outline` shows it. The detail
5
+ // page lists them as a document, and a related record's panel lists the ones it holds.
6
+ import type { Aggregate, Attr } from "../schema.ts";
7
+ import { bodyToHtml } from "./body_model.js";
8
+ import { kindOf, summaryOf } from "./block_list.js";
9
+ import { copyable, esc } from "./html.ts";
10
+ import { isMoment, momentValue } from "./moments.ts";
11
+ import { outline } from "./outline.ts";
12
+ import { label } from "./words.ts";
13
+
14
+ /** A body shown on a page sits under the page's h1 and its section's h2, so its own headings start at h3. */
15
+ const READ_ONLY_DEMOTE = 2;
16
+
17
+ type State = Record<string, unknown>;
18
+
19
+ /** A list of mixed blocks as a numbered list: each block's kind and the line that stands for it. */
20
+ function blockLines(agg: Aggregate, attr: Attr, held: unknown): string {
21
+ const blocks = Array.isArray(held) ? (held as Record<string, unknown>[]) : [];
22
+ if (blocks.length === 0 || !attr.blockList) return `<span class="text-muted">None</span>`;
23
+ const parts = agg.valueObjects[attr.type] ?? [];
24
+ const lines = blocks.map((block) => {
25
+ const kind = kindOf(attr.blockList, block);
26
+ const text = String(summaryOf(attr.blockList, parts, block));
27
+ return `<li class="flex flex-wrap items-baseline gap-2"><span class="badge badge-soft badge-neutral font-semibold">${esc(label(kind))}</span><span class="min-w-0 break-words">${esc(text)}</span></li>`;
28
+ });
29
+ return `<ol class="grid list-decimal gap-1 pl-6">${lines.join("")}</ol>`;
30
+ }
31
+
32
+ const isBlank = (value: unknown): boolean => value === undefined || value === null || value === "" || (Array.isArray(value) && value.length === 0);
33
+
34
+ /** A value object of several parts as its named parts, each shown by what it is (a body typeset, a picture by its description, a list as a run). */
35
+ function parts(agg: Aggregate, type: string, held: unknown, depth: number): string {
36
+ const value = held && typeof held === "object" ? (held as State) : {};
37
+ const rows = (agg.valueObjects[type] ?? [])
38
+ .filter((part) => !isBlank(value[part.name]))
39
+ .map((part) => `<div class="grid gap-1 sm:grid-cols-[8rem_minmax(0,1fr)] sm:gap-3"><dt class="text-muted">${esc(label(part.name))}</dt><dd class="min-w-0 break-words">${valueOf(agg, part, value, depth + 1)}</dd></div>`);
40
+ return rows.length === 0 ? `<span class="text-muted">Not set</span>` : `<dl class="grid gap-2">${rows.join("")}</dl>`;
41
+ }
42
+
43
+ /** Whether the attribute's value object has several parts to show (a one-part value object is shown as its part). */
44
+ const composite = (agg: Aggregate, attr: Attr): boolean => attr.kind === "object" && (agg.valueObjects[attr.type] ?? []).length > 1;
45
+
46
+ /** One attribute's value as markup. */
47
+ export function valueOf(agg: Aggregate, attr: Attr, values: State, depth = 0): string {
48
+ const held = values[attr.name];
49
+ if (isMoment(attr)) return momentValue(attr, held);
50
+ if (attr.widget === "body") return held && typeof held === "object" ? `<div class="prose">${bodyToHtml(held, READ_ONLY_DEMOTE)}</div>` : `<span class="text-muted">Not set</span>`;
51
+ if (attr.blockList && attr.list) return blockLines(agg, attr, held);
52
+ if (composite(agg, attr) && depth < 5) {
53
+ if (!attr.list) return parts(agg, attr.type, held, depth);
54
+ const items = Array.isArray(held) ? held : [];
55
+ return items.length === 0 ? `<span class="text-muted">None</span>` : `<ol class="grid list-decimal gap-3 pl-6">${items.map((item) => `<li class="pl-1">${parts(agg, attr.type, item, depth)}</li>`).join("")}</ol>`;
56
+ }
57
+ const copy = attr.name === agg.identity || attr.kind === "reference";
58
+ return copy && typeof held !== "object" && held !== undefined && held !== null && held !== "" ? copyable(String(held), label(attr.name).toLowerCase()) : outline(held);
59
+ }
60
+
61
+ /** `attrs` as a list of names and values, a wide value (a body, a list, a value object) on a line of its own. */
62
+ export function valueRows(agg: Aggregate, attrs: Attr[], values: State): string {
63
+ const rows = attrs.map((attr) => {
64
+ const wide = attr.widget === "body" || attr.list || attr.kind === "object";
65
+ const layout = wide ? "grid gap-2 py-4" : "grid gap-1 py-4 sm:grid-cols-[11rem_minmax(0,1fr)] sm:gap-4";
66
+ return `<div class="${layout}"><dt class="font-medium text-muted">${esc(label(attr.name))}</dt><dd class="min-w-0 break-words">${valueOf(agg, attr, values)}</dd></div>`;
67
+ });
68
+ return `<dl class="divide-y divide-base-300">\n${rows.join("\n")}\n</dl>`;
69
+ }
@@ -0,0 +1,97 @@
1
+ __BANNER__
2
+
3
+ // The words the editor shows people. The domain's names are CamelCase and singular; people read
4
+ // sentence case, plural for a list, and the same verb through a whole flow: the button that says
5
+ // "Publish" leaves a notice that says "Published." Nothing here asks the domain; it reads names.
6
+ import type { Aggregate, Command } from "../schema.ts";
7
+
8
+ /** The lower-case words of a CamelCase or snake_case name: "MediaItem" is "media item". */
9
+ export const humanize = (name: string): string =>
10
+ name
11
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
12
+ .replace(/_/g, " ")
13
+ .toLowerCase();
14
+
15
+ /** `text` with its first letter capitalised. */
16
+ export const capital = (text: string): string => text.charAt(0).toUpperCase() + text.slice(1);
17
+
18
+ /** A name as a sentence-case label: "SaveDraft" is "Save draft". */
19
+ export const label = (name: string): string => capital(humanize(name));
20
+
21
+ /** The plural of an English noun, for the common regular shapes. */
22
+ export function pluralOf(noun: string): string {
23
+ if (/[^aeiou]y$/.test(noun)) return `${noun.slice(0, -1)}ies`;
24
+ if (/(s|x|z|ch|sh)$/.test(noun)) return `${noun}es`;
25
+ return `${noun}s`;
26
+ }
27
+
28
+ /** An aggregate as a list's name: "MediaItem" is "Media items". */
29
+ export function plural(name: string): string {
30
+ const words = humanize(name).split(" ");
31
+ words[words.length - 1] = pluralOf(words[words.length - 1]);
32
+ return capital(words.join(" "));
33
+ }
34
+
35
+ /** The limits a list's holder gives it as a sentence ("At least 1 and at most 60."), or nothing when it gives none. */
36
+ export function limitsNote(min?: number, max?: number): string {
37
+ if (min && max) return `At least ${min} and at most ${max}.`;
38
+ return min ? `At least ${min}.` : max ? `At most ${max}.` : "";
39
+ }
40
+
41
+ /** An aggregate as one thing, lower case: "MediaItem" is "media item". */
42
+ export const singular = (name: string): string => humanize(name);
43
+
44
+ const IRREGULAR: Record<string, string> = {
45
+ set: "set", put: "put", send: "sent", make: "made", undo: "undone", redo: "redone", show: "shown", withdraw: "withdrawn",
46
+ draw: "drawn", hide: "hidden", write: "written", take: "taken", give: "given", keep: "kept", read: "read", pay: "paid", sell: "sold",
47
+ begin: "begun", run: "run", get: "got", find: "found", build: "built", choose: "chosen", leave: "left", hold: "held", lose: "lost",
48
+ break: "broken", bring: "brought", cut: "cut", shut: "shut", split: "split", lay: "laid", say: "said", spend: "spent", lend: "lent",
49
+ bind: "bound", wind: "wound", know: "known", grow: "grown", throw: "thrown", see: "seen", do: "done", go: "gone", have: "had",
50
+ };
51
+
52
+ const DOUBLED = new Set([
53
+ "submit", "admit", "commit", "omit", "plan", "drop", "ship", "stop", "skip", "tag", "pin", "log", "map", "ban", "trim", "wrap",
54
+ "grab", "strip", "refer", "transfer", "permit", "equip", "occur", "defer", "prefer",
55
+ ]);
56
+
57
+ const PREFIXES = ["re", "un", "over", "out", "pre", "mis"];
58
+
59
+ /** The irregular participle of `verb`, or of the verb under a prefix ("reset" is "set" again), or undefined. */
60
+ function irregular(verb: string): string | undefined {
61
+ if (Object.hasOwn(IRREGULAR, verb)) return IRREGULAR[verb];
62
+ const prefix = PREFIXES.find((one) => verb.startsWith(one) && Object.hasOwn(IRREGULAR, verb.slice(one.length)));
63
+ return prefix ? `${prefix}${IRREGULAR[verb.slice(prefix.length)]}` : undefined;
64
+ }
65
+
66
+ /** The past participle of a verb: "publish" is "published", "submit" is "submitted", "begin" is "begun", "cancel" is "canceled". */
67
+ export function past(verb: string): string {
68
+ const odd = irregular(verb);
69
+ if (odd) return odd;
70
+ if (verb.endsWith("e")) return `${verb}d`;
71
+ if (/[^aeiou]y$/.test(verb)) return `${verb.slice(0, -1)}ied`;
72
+ return DOUBLED.has(verb) ? `${verb}${verb.slice(-1)}ed` : `${verb}ed`;
73
+ }
74
+
75
+ /** A word cut back to what its forms share: "schedule", "scheduled" and "scheduling" are all "schedul". */
76
+ const stem = (word: string): string => word.replace(/(ing|ed|es|e|d|s)$/, "");
77
+
78
+ /**
79
+ * What a command's button says. A command named by one word acts on a thing the page already
80
+ * names, except one that makes something new: "Draft" on the articles page is "Draft article".
81
+ */
82
+ export function commandLabel(agg: Aggregate, command: Command): string {
83
+ const words = humanize(command.name).split(" ");
84
+ return capital(command.creates && words.length === 1 ? `${words[0]} ${singular(agg.name)}` : words.join(" "));
85
+ }
86
+
87
+ /**
88
+ * What the notice says when the command has run, in the same verb as its button: "Published draft."
89
+ * When the thing's name already starts with the verb ("Schedule" making a "scheduled action") the
90
+ * repeated word is dropped, so the notice reads "Scheduled action." and never "Scheduled scheduled action."
91
+ */
92
+ export function done(agg: Aggregate, command: Command): string {
93
+ const [verb, ...rest] = commandLabel(agg, command).toLowerCase().split(" ");
94
+ const repeated = rest.length > 0 && stem(rest[0]) === stem(verb);
95
+ const object = repeated ? rest.slice(1) : rest;
96
+ return capital(`${[past(verb), ...object].join(" ")}.`);
97
+ }
@@ -0,0 +1,35 @@
1
+ __BANNER__
2
+
3
+ // What a page of one record may read of the rest of the domain: the records that go with it, and the
4
+ // actions scheduled for it. The server builds it from the one read of the host the page already makes
5
+ // (app.ts), so a page with panels costs no more reads than a page without.
6
+ import type { Aggregate, Related } from "../schema.ts";
7
+ import { SCHEMA } from "../schema.ts";
8
+
9
+ type State = Record<string, unknown>;
10
+
11
+ export interface World {
12
+ /** The person's role, which decides which aggregates' records may be shown. */
13
+ role: string | null;
14
+ /** One record of an aggregate by its identity, or undefined when the host holds none. */
15
+ lookup(agg: Aggregate, id: string): State | undefined;
16
+ /** Every record of an aggregate, as `[id, state]` pairs. */
17
+ rows(agg: Aggregate): [string, State][];
18
+ /** The scheduled actions of the record being shown, when the host answered a query for them. */
19
+ actions?: [string, State][];
20
+ }
21
+
22
+ /** The aggregate a `related` entry names. */
23
+ export const relatedAggregate = (rel: Related): Aggregate | undefined =>
24
+ SCHEMA.aggregates.find((candidate) => candidate.name === rel.aggregate && candidate.chapter === rel.chapter);
25
+
26
+ /** The aggregate that registers pictures, when the editor has it among its own. */
27
+ export function pictureAggregate(): Aggregate | undefined {
28
+ const media = SCHEMA.media;
29
+ if (!media) return undefined;
30
+ return SCHEMA.aggregates.find((agg) => agg.name === media.aggregate && (media.domain ? agg.chapter === media.domain : !agg.chapter || agg.chapter === SCHEMA.domain));
31
+ }
32
+
33
+ /** The aggregate whose records are the scheduled actions, when the editor has one. */
34
+ export const schedulingAggregate = (): Aggregate | undefined =>
35
+ SCHEMA.scheduling && SCHEMA.aggregates.find((candidate) => candidate.name === SCHEMA.scheduling?.aggregate && candidate.chapter === SCHEMA.scheduling?.chapter);
@@ -12,5 +12,6 @@
12
12
  "skipLibCheck": true,
13
13
  "types": ["node"]
14
14
  },
15
- "include": ["src"]
15
+ "include": ["src"],
16
+ "exclude": ["src/browser"]
16
17
  }
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "palette"
4
+
5
+ module Hecks
6
+ module Projections
7
+ module Site
8
+ module CmsEditor
9
+ # The editor's theme as stylesheet text: the light and dark colour properties of a
10
+ # `Palette`, as the lines that go inside the two daisyUI themes `src/browser/app.css`
11
+ # declares (`editor-light`, and `editor-dark` for a visitor whose system prefers dark).
12
+ module Theme
13
+ module_function
14
+
15
+ # @param accent [String, nil] the Editor row's accent, a six-digit hex colour, or nil
16
+ # @return [Hash{String => String}] the placeholders of `app.css.tmpl` and their text
17
+ def tokens(accent)
18
+ themes = Palette.new(accent).themes
19
+ { "__LIGHT__" => lines(themes[:light]), "__DARK__" => lines(themes[:dark]) }
20
+ end
21
+
22
+ # @param properties [Hash{String => String}] custom properties and their values
23
+ # @return [String] one declaration per line
24
+ def lines(properties) = properties.map { |name, value| " #{name}: #{value};" }.join("\n")
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end