hecks 3.10.0 → 3.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (542) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/bluebook/pizzas.bluebook +29 -0
  3. data/examples/pizzas/bluebook/pizzas.hecksagon +7 -0
  4. data/exe/hecks +4 -4
  5. data/lib/hecks/adapters/driven/d1.rb +2 -10
  6. data/lib/hecks/adapters/driven/heki.adapter +1 -1
  7. data/lib/hecks/adapters/driven/heki.rb +2 -12
  8. data/lib/hecks/adapters/driven/lambda.adapter +1 -1
  9. data/lib/hecks/adapters/driven/lambda.rb +3 -18
  10. data/lib/hecks/adapters/driven/postgres.rb +4 -11
  11. data/lib/hecks/adapters/driven/sqlite.adapter +1 -1
  12. data/lib/hecks/adapters/driven/sqlite.rb +2 -10
  13. data/lib/hecks/adapters/driving/admission.rb +46 -0
  14. data/lib/hecks/adapters/driving/cli/nesting.rb +36 -0
  15. data/lib/hecks/adapters/driving/cli/shorthand.rb +88 -0
  16. data/lib/hecks/adapters/driving/cli.rb +127 -0
  17. data/lib/hecks/adapters/driving/cli_runner/answers.rb +160 -0
  18. data/lib/hecks/adapters/driving/cli_runner/resolution.rb +158 -0
  19. data/lib/hecks/adapters/driving/cli_runner/settling.rb +120 -0
  20. data/lib/hecks/adapters/driving/cli_runner/streaming.rb +93 -0
  21. data/lib/hecks/adapters/driving/cli_runner/suggestions.rb +47 -0
  22. data/lib/hecks/adapters/driving/cli_runner.rb +70 -0
  23. data/lib/hecks/adapters/driving/command_request.rb +121 -0
  24. data/lib/hecks/adapters/driving/handle.rb +214 -0
  25. data/lib/hecks/adapters/driving/json.rb +136 -0
  26. data/lib/hecks/adapters/driving/launcher_options.rb +267 -0
  27. data/lib/hecks/adapters/driving/mcp/properties.rb +49 -0
  28. data/lib/hecks/adapters/driving/mcp/tool_calls.rb +116 -0
  29. data/lib/hecks/adapters/driving/mcp/tool_definitions/auditing.rb +53 -0
  30. data/lib/hecks/adapters/driving/mcp/tool_definitions/calling.rb +71 -0
  31. data/lib/hecks/adapters/driving/mcp/tool_definitions/reading.rb +99 -0
  32. data/lib/hecks/adapters/driving/mcp/tool_definitions.rb +18 -0
  33. data/lib/hecks/adapters/driving/mcp.rb +159 -0
  34. data/lib/hecks/adapters/driving/mcp_guide_cache.rb +78 -0
  35. data/lib/hecks/adapters/driving/mcp_scope/argument_policy.rb +97 -0
  36. data/lib/hecks/adapters/driving/mcp_scope/env_problems.rb +69 -0
  37. data/lib/hecks/adapters/driving/mcp_scope/presenter.rb +83 -0
  38. data/lib/hecks/adapters/driving/mcp_scope.rb +213 -0
  39. data/lib/hecks/adapters/driving/newcomer.rb +50 -0
  40. data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +171 -0
  41. data/lib/hecks/adapters/driving/ruby/chapter.rb +91 -0
  42. data/lib/hecks/adapters/driving/ruby.rb +83 -0
  43. data/lib/hecks/adapters/driving/usage_cache.rb +144 -0
  44. data/lib/hecks/{doors.rb → adapters/driving.rb} +10 -6
  45. data/lib/hecks/behaviors/expectations/boot.rb +1 -1
  46. data/lib/hecks/bench/ruby_runner.rb +1 -1
  47. data/lib/hecks/bluebook/ask_resolution.rb +130 -0
  48. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +8 -2
  49. data/lib/hecks/bluebook/assembly/contracts/read_models.rb +1 -1
  50. data/lib/hecks/bluebook/assembly/contracts/rules.rb +4 -2
  51. data/lib/hecks/bluebook/assembly/contracts/structure.rb +3 -1
  52. data/lib/hecks/bluebook/assembly/marks.rb +4 -15
  53. data/lib/hecks/bluebook/assembly/scalars.rb +39 -0
  54. data/lib/hecks/bluebook/attribute.rb +1 -1
  55. data/lib/hecks/bluebook/behaviour/command.rb +13 -8
  56. data/lib/hecks/bluebook/behaviour/policy.rb +30 -0
  57. data/lib/hecks/bluebook/capabilities/attribute_values.rb +72 -0
  58. data/lib/hecks/bluebook/capabilities.rb +91 -9
  59. data/lib/hecks/bluebook/domain_port.rb +16 -4
  60. data/lib/hecks/bluebook/dsl/adapter_builder.rb +18 -6
  61. data/lib/hecks/bluebook/dsl/binding_proxy.rb +13 -0
  62. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/computed_sources.rb +98 -0
  63. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/fan_out_rows.rb +42 -0
  64. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/projected_fields.rb +14 -2
  65. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/provisions.rb +69 -3
  66. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/with_projections.rb +17 -10
  67. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +5 -0
  68. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
  69. data/lib/hecks/bluebook/dsl/command_builder/mutations.rb +8 -1
  70. data/lib/hecks/bluebook/dsl/command_builder/operands.rb +121 -0
  71. data/lib/hecks/bluebook/dsl/command_builder.rb +2 -0
  72. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +10 -1
  73. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +45 -2
  74. data/lib/hecks/bluebook/dsl/policy_builder.rb +47 -10
  75. data/lib/hecks/bluebook/dsl/rule_reference.rb +7 -4
  76. data/lib/hecks/bluebook/dsl/world_builder.rb +1 -1
  77. data/lib/hecks/bluebook/expression/ast_json/emitters.rb +6 -0
  78. data/lib/hecks/bluebook/expression/ast_json.rb +9 -6
  79. data/lib/hecks/bluebook/expression/ast_reader.rb +14 -9
  80. data/lib/hecks/bluebook/expression/projection.json +36 -0
  81. data/lib/hecks/bluebook/expression/resolver/operations.rb +31 -0
  82. data/lib/hecks/bluebook/expression/resolver/parsing.rb +38 -5
  83. data/lib/hecks/bluebook/expression/resolver/scanning.rb +22 -5
  84. data/lib/hecks/bluebook/expression/resolver/values.rb +10 -0
  85. data/lib/hecks/bluebook/expression/resolver.rb +20 -1
  86. data/lib/hecks/bluebook/hecksagon.rb +23 -3
  87. data/lib/hecks/bluebook/lifecycle.rb +22 -3
  88. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +13 -1
  89. data/lib/hecks/bluebook/meta_validator/modes.rb +4 -1
  90. data/lib/hecks/bluebook/meta_validator/readings.rb +24 -1
  91. data/lib/hecks/bluebook/meta_validator/reconstruction/heads.rb +2 -1
  92. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +6 -3
  93. data/lib/hecks/bluebook/meta_validator/shapes.rb +5 -0
  94. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +1 -1
  95. data/lib/hecks/bluebook/meta_validator.rb +12 -12
  96. data/lib/hecks/bluebook/model_check/ask_checks.rb +71 -0
  97. data/lib/hecks/bluebook/model_check/policy_checks.rb +2 -1
  98. data/lib/hecks/bluebook/model_check/reference_checks.rb +70 -0
  99. data/lib/hecks/bluebook/model_check.rb +8 -2
  100. data/lib/hecks/bluebook/policy.rb +4 -2
  101. data/lib/hecks/bluebook/smoke_test.rb +1 -1
  102. data/lib/hecks/bluebook/synthesizer.rb +2 -0
  103. data/lib/hecks/bluebook.rb +2 -0
  104. data/lib/hecks/cli/command_table.rb +1 -1
  105. data/lib/hecks/cli/console.rb +2 -1
  106. data/lib/hecks/cli/document.rb +1 -1
  107. data/lib/hecks/cli/launcher_words.rb +4 -4
  108. data/lib/hecks/cli/mcp.rb +4 -4
  109. data/lib/hecks/cli/project_cli/launcher_source.rb +2 -2
  110. data/lib/hecks/cli/project_cli/templates.rb +5 -5
  111. data/lib/hecks/cli/project_cli.rb +9 -6
  112. data/lib/hecks/cli/run.rb +2 -2
  113. data/lib/hecks/cli/smoke_http/checks.rb +4 -1
  114. data/lib/hecks/cli/smoke_http/transport.rb +3 -3
  115. data/lib/hecks/cli/smoke_http.rb +3 -0
  116. data/lib/hecks/codemod.rb +5 -2
  117. data/lib/hecks/corpus/routes.rb +7 -0
  118. data/lib/hecks/corpus/rust.rb +3 -2
  119. data/lib/hecks/deploy/bluebook/deploy.bluebook +34 -4
  120. data/lib/hecks/doc/reference/rendering.rb +6 -3
  121. data/lib/hecks/forms/app/commands.rb +8 -7
  122. data/lib/hecks/forms/app/queries.rb +2 -1
  123. data/lib/hecks/forms/app.rb +1 -1
  124. data/lib/hecks/forms/record_renderer.rb +1 -1
  125. data/lib/hecks/framework/bluebook/privacy.bluebook +11 -0
  126. data/lib/hecks/fuzzing/concurrent_dispatch.rb +4 -1
  127. data/lib/hecks/fuzzing/failure_triage.rb +2 -1
  128. data/lib/hecks/fuzzing/mutation/run.rb +11 -4
  129. data/lib/hecks/fuzzing/properties/catalog.rb +6 -4
  130. data/lib/hecks/fuzzing/properties/guaranteed_by_construction.rb +18 -11
  131. data/lib/hecks/fuzzing/properties/guards.rb +1 -1
  132. data/lib/hecks/fuzzing/self_consistency/saga_redelivery.rb +1 -1
  133. data/lib/hecks/fuzzing/self_consistency.rb +6 -2
  134. data/lib/hecks/fuzzing/shrinker.rb +3 -1
  135. data/lib/hecks/fuzzing/value_generator.rb +19 -5
  136. data/lib/hecks/gate/stages.yml +33 -7
  137. data/lib/hecks/grammar/evolve.rb +1 -1
  138. data/lib/hecks/grammar/expression_operators.json +372 -0
  139. data/lib/hecks/grammar/operators.rb +3 -1
  140. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +10 -10
  141. data/lib/hecks/hecks/adapters/codebase/inspection/rubycritic.rb +35 -0
  142. data/lib/hecks/hecks/adapters/codebase/inspection.rb +104 -0
  143. data/lib/hecks/hecks/adapters/codebase/publishing.rb +2 -1
  144. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +12 -2
  145. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +2 -1
  146. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +1 -1
  147. data/lib/hecks/hecks/adapters/cost_explorer.rb +8 -5
  148. data/lib/hecks/hecks/adapters/deploy_toolchain/statuses.rb +2 -1
  149. data/lib/hecks/hecks/adapters/finding_github.rb +13 -6
  150. data/lib/hecks/hecks/adapters/first_admin.rb +2 -2
  151. data/lib/hecks/hecks/adapters/github_rulesets.rb +6 -3
  152. data/lib/hecks/hecks/adapters/in_process_boot.rb +1 -1
  153. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +1 -1
  154. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +10 -20
  155. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +28 -5
  156. data/lib/hecks/hecks/adapters/local_files.rb +2 -2
  157. data/lib/hecks/hecks/adapters/rehearsal/audit_report.rb +107 -0
  158. data/lib/hecks/hecks/adapters/rehearsal/census.rb +71 -0
  159. data/lib/hecks/hecks/adapters/rehearsal/declaration.rb +133 -0
  160. data/lib/hecks/hecks/adapters/rehearsal/environment.rb +83 -0
  161. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +142 -0
  162. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +96 -0
  163. data/lib/hecks/hecks/adapters/rehearsal/postgres_census.rb +100 -0
  164. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +69 -0
  165. data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +133 -0
  166. data/lib/hecks/hecks/adapters/rehearsal/rows.rb +41 -0
  167. data/lib/hecks/hecks/adapters/rehearsal/rust_audit.rb +83 -0
  168. data/lib/hecks/hecks/adapters/rehearsal/subject.rb +104 -0
  169. data/lib/hecks/hecks/adapters/rehearsal.rb +54 -0
  170. data/lib/hecks/hecks/adapters/rehearsal_store.adapter +7 -0
  171. data/lib/hecks/hecks/adapters/rehearsal_store.rb +81 -0
  172. data/lib/hecks/hecks/adapters/terminal.rb +13 -11
  173. data/lib/hecks/hecks/codebase.behaviors +13 -1
  174. data/lib/hecks/hecks/codebase.bluebook +239 -5
  175. data/lib/hecks/hecks/custodian.behaviors +24 -6
  176. data/lib/hecks/hecks/custodian.bluebook +6 -1227
  177. data/lib/hecks/hecks/deploy.behaviors +1 -1
  178. data/lib/hecks/hecks/deploy.world +1 -3
  179. data/lib/hecks/hecks/hecks.behaviors +1 -1
  180. data/lib/hecks/hecks/hecks.hecksagon +53 -18
  181. data/lib/hecks/hecks/hecks.world +13 -10
  182. data/lib/hecks/hecks/rehearsal.bluebook +477 -0
  183. data/lib/hecks/hecks/site.behaviors +1 -1
  184. data/lib/hecks/hecks/site.world +0 -1
  185. data/lib/hecks/hecks/tooling.bluebook +1238 -0
  186. data/lib/hecks/indifferent_key.rb +20 -0
  187. data/lib/hecks/language/adapter.bluebook +13 -3
  188. data/lib/hecks/language/bluebook/aggregate.bluebook +23 -0
  189. data/lib/hecks/language/bluebook/bluebook.behaviors +23 -0
  190. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -0
  191. data/lib/hecks/language/bluebook/entity.bluebook +18 -0
  192. data/lib/hecks/language/bluebook/policy.behaviors +19 -0
  193. data/lib/hecks/language/bluebook/policy.bluebook +9 -1
  194. data/lib/hecks/language/bluebook/query.bluebook +1 -1
  195. data/lib/hecks/language/bluebook/vocabulary.bluebook +63 -4
  196. data/lib/hecks/language/hecksagon/domain_port.bluebook +1 -1
  197. data/lib/hecks/language/hecksagon/hecksagon.bluebook +22 -0
  198. data/lib/hecks/language/oidc.json +10 -0
  199. data/lib/hecks/language/port.bluebook +4 -4
  200. data/lib/hecks/literal.rb +12 -3
  201. data/lib/hecks/ports/key_vault.rb +7 -2
  202. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +21 -10
  203. data/lib/hecks/ports/persistence/plugins/era/postgres_era/boot.rb +4 -2
  204. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +2 -1
  205. data/lib/hecks/ports/persistence/repository_factory.rb +6 -3
  206. data/lib/hecks/projection_files/expression_tables.rb +7 -2
  207. data/lib/hecks/projections/deploy/box/compose.rb +6 -4
  208. data/lib/hecks/projections/deploy/box/edge.rb +62 -0
  209. data/lib/hecks/projections/deploy/box/logging.rb +99 -0
  210. data/lib/hecks/projections/deploy/box/roll_scripts.rb +16 -8
  211. data/lib/hecks/projections/deploy/box/settings/cdn.rb +40 -0
  212. data/lib/hecks/projections/deploy/box/settings/logs.rb +40 -0
  213. data/lib/hecks/projections/deploy/box/settings/patterns.rb +11 -0
  214. data/lib/hecks/projections/deploy/box/settings/types.rb +16 -1
  215. data/lib/hecks/projections/deploy/box/settings.rb +6 -2
  216. data/lib/hecks/projections/deploy/box/stack_yaml.rb +5 -2
  217. data/lib/hecks/projections/deploy/box/templates/Makefile.tmpl +16 -0
  218. data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +8 -4
  219. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +32 -0
  220. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +1 -1
  221. data/lib/hecks/projections/deploy/box/templates/edge.mk.tmpl +17 -0
  222. data/lib/hecks/projections/deploy/box/templates/edge.yaml.tmpl +65 -0
  223. data/lib/hecks/projections/deploy/box/templates/log-group.yaml.tmpl +12 -0
  224. data/lib/hecks/projections/deploy/box/templates/log-policy.yaml.tmpl +7 -0
  225. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +5 -4
  226. data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +5 -4
  227. data/lib/hecks/projections/deploy/box.rb +15 -3
  228. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  229. data/lib/hecks/projections/deploy/shared.rb +5 -2
  230. data/lib/hecks/projections/model/deviations.rb +2 -2
  231. data/lib/hecks/projections/model.rb +2 -2
  232. data/lib/hecks/projections/oidc.rb +13 -1
  233. data/lib/hecks/projections/shape.rb +5 -2
  234. data/lib/hecks/projections/site/cms_editor/attributes.rb +26 -6
  235. data/lib/hecks/projections/site/cms_editor/blocks.rb +157 -0
  236. data/lib/hecks/projections/site/cms_editor/checks.rb +14 -1
  237. data/lib/hecks/projections/site/cms_editor/clearing.rb +70 -0
  238. data/lib/hecks/projections/site/cms_editor/color.rb +124 -0
  239. data/lib/hecks/projections/site/cms_editor/composition.rb +42 -0
  240. data/lib/hecks/projections/site/cms_editor/destructive.rb +61 -0
  241. data/lib/hecks/projections/site/cms_editor/drafts.rb +102 -0
  242. data/lib/hecks/projections/site/cms_editor/hints.rb +74 -0
  243. data/lib/hecks/projections/site/cms_editor/icons.rb +86 -0
  244. data/lib/hecks/projections/site/cms_editor/key_kinds.rb +31 -0
  245. data/lib/hecks/projections/site/cms_editor/limits.rb +72 -0
  246. data/lib/hecks/projections/site/cms_editor/media.rb +1 -1
  247. data/lib/hecks/projections/site/cms_editor/moment.rb +34 -0
  248. data/lib/hecks/projections/site/cms_editor/palette.rb +115 -0
  249. data/lib/hecks/projections/site/cms_editor/pickers.rb +85 -0
  250. data/lib/hecks/projections/site/cms_editor/preview.rb +91 -0
  251. data/lib/hecks/projections/site/cms_editor/relations.rb +92 -0
  252. data/lib/hecks/projections/site/cms_editor/scale_checks.rb +66 -0
  253. data/lib/hecks/projections/site/cms_editor/scheduling.rb +150 -0
  254. data/lib/hecks/projections/site/cms_editor/schema.rb +96 -19
  255. data/lib/hecks/projections/site/cms_editor/setting.rb +52 -9
  256. data/lib/hecks/projections/site/cms_editor/targets.rb +86 -0
  257. data/lib/hecks/projections/site/cms_editor/templates/package.json.tmpl +18 -1
  258. data/lib/hecks/projections/site/cms_editor/templates/src/app.ts.tmpl +251 -57
  259. data/lib/hecks/projections/site/cms_editor/templates/src/assets.ts.tmpl +98 -0
  260. data/lib/hecks/projections/site/cms_editor/templates/src/auth/membership.ts.tmpl +32 -24
  261. data/lib/hecks/projections/site/cms_editor/templates/src/browser/app.css.tmpl +412 -0
  262. data/lib/hecks/projections/site/cms_editor/templates/src/browser/autosave.js.tmpl +144 -0
  263. data/lib/hecks/projections/site/cms_editor/templates/src/browser/blocks.js.tmpl +500 -0
  264. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_doc.js.tmpl +175 -0
  265. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_editor.js.tmpl +418 -0
  266. data/lib/hecks/projections/site/cms_editor/templates/src/browser/epoch.js.tmpl +61 -0
  267. data/lib/hecks/projections/site/cms_editor/templates/src/browser/forms.js.tmpl +210 -0
  268. data/lib/hecks/projections/site/cms_editor/templates/src/browser/lists.js.tmpl +40 -0
  269. data/lib/hecks/projections/site/cms_editor/templates/src/browser/main.js.tmpl +26 -0
  270. data/lib/hecks/projections/site/cms_editor/templates/src/browser/media_picker.js.tmpl +232 -0
  271. data/lib/hecks/projections/site/cms_editor/templates/src/browser/moments.js.tmpl +73 -0
  272. data/lib/hecks/projections/site/cms_editor/templates/src/browser/pickers.js.tmpl +152 -0
  273. data/lib/hecks/projections/site/cms_editor/templates/src/browser/preview.js.tmpl +98 -0
  274. data/lib/hecks/projections/site/cms_editor/templates/src/browser/shell.js.tmpl +140 -0
  275. data/lib/hecks/projections/site/cms_editor/templates/src/browser/toast.js.tmpl +66 -0
  276. data/lib/hecks/projections/site/cms_editor/templates/src/choices.ts.tmpl +99 -0
  277. data/lib/hecks/projections/site/cms_editor/templates/src/commands.ts.tmpl +78 -24
  278. data/lib/hecks/projections/site/cms_editor/templates/src/config.ts.tmpl +6 -0
  279. data/lib/hecks/projections/site/cms_editor/templates/src/flash.ts.tmpl +52 -0
  280. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +19 -2
  281. data/lib/hecks/projections/site/cms_editor/templates/src/media/handler.ts.tmpl +31 -8
  282. data/lib/hecks/projections/site/cms_editor/templates/src/names.ts.tmpl +36 -0
  283. data/lib/hecks/projections/site/cms_editor/templates/src/schema.ts.tmpl +131 -2
  284. data/lib/hecks/projections/site/cms_editor/templates/src/ui/availability.ts.tmpl +15 -0
  285. data/lib/hecks/projections/site/cms_editor/templates/src/ui/block_list.js.tmpl +213 -0
  286. data/lib/hecks/projections/site/cms_editor/templates/src/ui/blocks.ts.tmpl +275 -0
  287. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_model.js.tmpl +8 -5
  288. data/lib/hecks/projections/site/cms_editor/templates/src/ui/detail.ts.tmpl +88 -0
  289. data/lib/hecks/projections/site/cms_editor/templates/src/ui/dialogs.ts.tmpl +43 -0
  290. data/lib/hecks/projections/site/cms_editor/templates/src/ui/fields.ts.tmpl +229 -34
  291. data/lib/hecks/projections/site/cms_editor/templates/src/ui/html.ts.tmpl +40 -81
  292. data/lib/hecks/projections/site/cms_editor/templates/src/ui/icons.ts.tmpl +15 -0
  293. data/lib/hecks/projections/site/cms_editor/templates/src/ui/input.ts.tmpl +29 -12
  294. data/lib/hecks/projections/site/cms_editor/templates/src/ui/links.ts.tmpl +100 -0
  295. data/lib/hecks/projections/site/cms_editor/templates/src/ui/moments.ts.tmpl +44 -0
  296. data/lib/hecks/projections/site/cms_editor/templates/src/ui/outline.ts.tmpl +53 -17
  297. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pages.ts.tmpl +178 -88
  298. data/lib/hecks/projections/site/cms_editor/templates/src/ui/paging.ts.tmpl +82 -0
  299. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pickers.ts.tmpl +110 -0
  300. data/lib/hecks/projections/site/cms_editor/templates/src/ui/preview.ts.tmpl +87 -0
  301. data/lib/hecks/projections/site/cms_editor/templates/src/ui/related.ts.tmpl +97 -0
  302. data/lib/hecks/projections/site/cms_editor/templates/src/ui/schedule.ts.tmpl +160 -0
  303. data/lib/hecks/projections/site/cms_editor/templates/src/ui/shell.ts.tmpl +211 -0
  304. data/lib/hecks/projections/site/cms_editor/templates/src/ui/table.ts.tmpl +151 -0
  305. data/lib/hecks/projections/site/cms_editor/templates/src/ui/theme.js.tmpl +16 -0
  306. data/lib/hecks/projections/site/cms_editor/templates/src/ui/values.ts.tmpl +69 -0
  307. data/lib/hecks/projections/site/cms_editor/templates/src/ui/words.ts.tmpl +97 -0
  308. data/lib/hecks/projections/site/cms_editor/templates/src/ui/world.ts.tmpl +35 -0
  309. data/lib/hecks/projections/site/cms_editor/templates/tsconfig.json.tmpl +2 -1
  310. data/lib/hecks/projections/site/cms_editor/theme.rb +29 -0
  311. data/lib/hecks/projections/site/cms_editor/theme_checks.rb +57 -0
  312. data/lib/hecks/projections/site/cms_editor.rb +49 -23
  313. data/lib/hecks/projections/site/role_probe.rb +2 -0
  314. data/lib/hecks/projections/site/site_root/workflow.rb +5 -0
  315. data/lib/hecks/projector/cli_audience.rb +22 -1
  316. data/lib/hecks/projector/cli_projector/listing.rb +1 -1
  317. data/lib/hecks/projector/cli_projector/option_specs.rb +1 -1
  318. data/lib/hecks/projector/cli_projector/usage.rb +15 -4
  319. data/lib/hecks/projector/docs_projector/closing.rb +1 -1
  320. data/lib/hecks/projector/docs_projector.rb +1 -1
  321. data/lib/hecks/projector/exporter/bindings.rb +40 -10
  322. data/lib/hecks/projector/exporter/capability_values.rb +55 -0
  323. data/lib/hecks/projector/narrate_projector/reactions.rb +1 -1
  324. data/lib/hecks/projector/target.rb +11 -0
  325. data/lib/hecks/projector.rb +32 -5
  326. data/lib/hecks/quality_control/adapters/github_checks.rb +12 -8
  327. data/lib/hecks/quality_control/adapters/github_issues.rb +8 -4
  328. data/lib/hecks/quality_control/cli/qa_open_pr.rb +2 -2
  329. data/lib/hecks/quality_control/cli/qa_sweep/checks/shrinking.rb +1 -1
  330. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +1 -1
  331. data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
  332. data/lib/hecks/quality_control/quality_control.bluebook +6 -2
  333. data/lib/hecks/release/approval.rb +64 -0
  334. data/lib/hecks/router/{door.rb → aggregate_reader.rb} +5 -4
  335. data/lib/hecks/router/namespace_installer.rb +16 -16
  336. data/lib/hecks/router.rb +1 -1
  337. data/lib/hecks/runtime/best_effort.rb +22 -0
  338. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -2
  339. data/lib/hecks/runtime/command_interpreter.rb +3 -2
  340. data/lib/hecks/runtime/command_rules/emission.rb +1 -1
  341. data/lib/hecks/runtime/dispatcher.rb +4 -4
  342. data/lib/hecks/runtime/entity_element/location.rb +1 -1
  343. data/lib/hecks/runtime/event.rb +5 -0
  344. data/lib/hecks/runtime/identity.rb +7 -6
  345. data/lib/hecks/runtime/interpreting.rb +2 -1
  346. data/lib/hecks/runtime/invocation_builder.rb +1 -1
  347. data/lib/hecks/runtime/loader/boot_steps.rb +8 -5
  348. data/lib/hecks/runtime/loader.rb +10 -10
  349. data/lib/hecks/runtime/policy_interpreter/arguments.rb +7 -4
  350. data/lib/hecks/runtime/policy_interpreter/fan_out.rb +3 -3
  351. data/lib/hecks/runtime/policy_interpreter.rb +6 -6
  352. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -1
  353. data/lib/hecks/runtime/reaction_invocation.rb +1 -1
  354. data/lib/hecks/runtime/rebuild_sweep.rb +11 -1
  355. data/lib/hecks/runtime/registry/ask_verification.rb +35 -0
  356. data/lib/hecks/runtime/registry/hecksagon_rules.rb +10 -0
  357. data/lib/hecks/runtime/registry/merging.rb +1 -1
  358. data/lib/hecks/runtime/registry/providers.rb +16 -0
  359. data/lib/hecks/runtime/registry/read_repository.rb +6 -4
  360. data/lib/hecks/runtime/registry/verification.rb +4 -1
  361. data/lib/hecks/runtime/registry/world_defaults.rb +29 -0
  362. data/lib/hecks/runtime/registry.rb +1 -1
  363. data/lib/hecks/runtime/saga_interpreter/compensation.rb +1 -1
  364. data/lib/hecks/runtime/saga_interpreter/delivery.rb +4 -4
  365. data/lib/hecks/runtime/saga_interpreter/transitions.rb +1 -1
  366. data/lib/hecks/runtime/saga_interpreter.rb +3 -3
  367. data/lib/hecks/runtime/tenant_check.rb +5 -4
  368. data/lib/hecks/runtime/value/coercion.rb +2 -2
  369. data/lib/hecks/runtime/value/validation.rb +16 -4
  370. data/lib/hecks/runtime.rb +9 -9
  371. data/lib/hecks/rust_build/host.rb +11 -4
  372. data/lib/hecks/rust_build/prebuilt_binary.rb +101 -0
  373. data/lib/hecks/rust_build/project_rust/target_ir.rb +1 -1
  374. data/lib/hecks/rust_build/wasm.rb +1 -1
  375. data/lib/hecks/rust_build/wasm_browser.rb +1 -1
  376. data/lib/hecks/rust_build.rb +11 -0
  377. data/lib/hecks/served_options.rb +53 -0
  378. data/lib/hecks/site/bluebook/site.bluebook +1 -0
  379. data/lib/hecks/storehouse/audit.rb +1 -1
  380. data/lib/hecks/storehouse/dispatching.rb +8 -6
  381. data/lib/hecks/storehouse/introspection.rb +2 -2
  382. data/lib/hecks/storehouse/querying.rb +6 -4
  383. data/lib/hecks/storehouse.rb +4 -4
  384. data/lib/hecks/tools/ci_gate_decision.rb +35 -0
  385. data/lib/hecks/tools/ci_gates.rb +20 -1
  386. data/lib/hecks/tools/comment_style/source_file.rb +4 -1
  387. data/lib/hecks/tools/deploy_recipe_lint/fixtures.rb +4 -1
  388. data/lib/hecks/tools/lanes/files.rb +8 -0
  389. data/lib/hecks/tools/lanes/prebuilt_workflow.rb +81 -0
  390. data/lib/hecks/tools/lanes/rows.rb +6 -0
  391. data/lib/hecks/tools/lanes.rb +1 -0
  392. data/lib/hecks/tools/oidc_manifests.rb +4 -5
  393. data/lib/hecks/tools/regeneration_run.rb +11 -4
  394. data/lib/hecks/tools/release_only.rb +111 -0
  395. data/lib/hecks/tools/site_routes/editor_files.rb +12 -5
  396. data/lib/hecks/tools/tenant_provisioning.rb +2 -2
  397. data/lib/hecks/tools/tools_doc.rb +4 -4
  398. data/lib/hecks/version.rb +1 -1
  399. data/lib/hecks/vocabulary.rb +19 -5
  400. data/lib/hecks.rb +15 -15
  401. data/lib/rubocop/cop/hecks/env_dump.rb +69 -0
  402. data/lib/rubocop/cop/hecks/monotonic_duration.rb +53 -0
  403. data/lib/rubocop/cop/hecks/no_wall_clock_in_runtime.rb +34 -0
  404. data/lib/rubocop/cop/hecks/raw_shell_interpolation.rb +79 -0
  405. data/lib/rubocop/cop/hecks/sleep_in_spec.rb +34 -0
  406. data/lib/rubocop/cop/hecks/swallowed_rescue.rb +37 -0
  407. data/lib/rubocop/cop/hecks/symbol_string_key_mix.rb +50 -0
  408. data/lib/rubocop/cop/hecks/untyped_raise.rb +37 -0
  409. data/prebuilt/9ee54e2f0d4d6d3fe046827779b221e02de665fba911a048ae5dd8b8fbfb7d90.marshal +0 -0
  410. data/prebuilt/d74a00e996222c1d8b676d5959092effc46b4dc59e0555f3bf9554ac58e3094a.marshal +0 -0
  411. data/prebuilt/verdicts-df19f35868d3284a70f55e42dcef87ef4e65e6a678e5135f72e1f6cff4c1357b.json +1 -0
  412. data/rust/.gitignore +6 -0
  413. data/rust/Cargo.toml +6 -0
  414. data/rust/build/src/binding_pass.rs +103 -0
  415. data/rust/build/src/cargo_sync.rs +6 -0
  416. data/rust/build/src/lineage_pass.rs +3 -3
  417. data/rust/build/src/main.rs +17 -5
  418. data/rust/build/src/pipeline.rs +48 -5
  419. data/rust/build/src/seams_pass.rs +123 -0
  420. data/rust/build/src/translation_pass.rs +167 -0
  421. data/rust/codegen/src/asks.rs +103 -0
  422. data/rust/codegen/src/commands.rs +13 -8
  423. data/rust/codegen/src/constraints.rs +25 -0
  424. data/rust/codegen/src/domain_generator.rs +2 -0
  425. data/rust/codegen/src/expr_emitter.rs +13 -0
  426. data/rust/codegen/src/json_codec.rs +54 -15
  427. data/rust/codegen/src/main.rs +1 -0
  428. data/rust/codegen/src/mutations.rs +128 -3
  429. data/rust/codegen/src/queries.rs +11 -2
  430. data/rust/codegen/src/reactions.rs +1 -1
  431. data/rust/codegen/src/types.rs +166 -8
  432. data/rust/host/HECKS_RELEASE +1 -1
  433. data/rust/host/src/api.rs +4 -4
  434. data/rust/host/src/bin/validate_harness.rs +43 -0
  435. data/rust/host/src/boundary_fuzz/expr_json.rs +2 -2
  436. data/rust/host/src/boundary_fuzz/ir.rs +1 -0
  437. data/rust/host/src/boundary_fuzz/server.rs +1 -1
  438. data/rust/host/src/checkout.rs +35 -19
  439. data/rust/host/src/commerce_ir.rs +408 -2
  440. data/rust/host/src/expr_json.rs +118 -0
  441. data/rust/host/src/extension.rs +13 -1
  442. data/rust/host/src/lib.rs +1 -0
  443. data/rust/host/src/mint.rs +1 -1
  444. data/rust/host/src/payments/tests.rs +27 -1
  445. data/rust/host/src/payments.rs +12 -4
  446. data/rust/host/src/platform_signin/attack_tests.rs +1100 -0
  447. data/rust/host/src/platform_signin/config.rs +512 -0
  448. data/rust/host/src/platform_signin/cookie.rs +237 -0
  449. data/rust/host/src/platform_signin/extension.rs +190 -0
  450. data/rust/host/src/platform_signin/fetcher.rs +322 -0
  451. data/rust/host/src/platform_signin/handler.rs +251 -0
  452. data/rust/host/src/platform_signin/handler_tests.rs +554 -0
  453. data/rust/host/src/platform_signin/jti.rs +196 -0
  454. data/rust/host/src/platform_signin/jwks.rs +542 -0
  455. data/rust/host/src/platform_signin/jws.rs +249 -0
  456. data/rust/host/src/platform_signin/known_answer_tests.rs +237 -0
  457. data/rust/host/src/platform_signin/mapping_tests.rs +254 -0
  458. data/rust/host/src/platform_signin/mod.rs +75 -0
  459. data/rust/host/src/platform_signin/mode_tests.rs +382 -0
  460. data/rust/host/src/platform_signin/pages.rs +204 -0
  461. data/rust/host/src/platform_signin/query.rs +233 -0
  462. data/rust/host/src/platform_signin/session.rs +151 -0
  463. data/rust/host/src/platform_signin/settings.rs +385 -0
  464. data/rust/host/src/platform_signin/strict_json.rs +351 -0
  465. data/rust/host/src/platform_signin/test_support.rs +235 -0
  466. data/rust/host/src/platform_signin/ttl_tests.rs +94 -0
  467. data/rust/host/src/platform_signin/verify.rs +604 -0
  468. data/rust/host/src/platform_signin/wiring_support.rs +319 -0
  469. data/rust/host/src/query_step.rs +22 -4
  470. data/rust/host/src/reference_validate.rs +653 -108
  471. data/rust/host/src/resend.rs +2 -0
  472. data/rust/host/src/server.rs +19 -6
  473. data/rust/host/src/ui_schema.rs +132 -5
  474. data/rust/host/src/web/newsletter.rs +7 -8
  475. data/rust/host/src/web/newsletter_send.rs +38 -13
  476. data/rust/host/src/web/registrations.rs +229 -26
  477. data/rust/host/src/web.rs +53 -9
  478. data/rust/parser/src/emit.rs +38 -6
  479. data/rust/parser/src/expr/ast_json.rs +7 -1
  480. data/rust/parser/src/expr/resolver.rs +171 -6
  481. data/rust/parser/src/ir.rs +7 -0
  482. data/rust/parser/src/keywords.rs +24 -0
  483. data/rust/parser/src/main.rs +3 -0
  484. data/rust/parser/src/parse/aggregate.rs +2 -1
  485. data/rust/parser/src/parse/chapter.rs +2 -2
  486. data/rust/parser/src/parse/command.rs +2 -0
  487. data/rust/parser/src/parse/hecksagon.rs +5 -2
  488. data/rust/parser/src/parse/lifecycle.rs +81 -0
  489. data/rust/parser/src/parse/mod.rs +15 -1
  490. data/rust/parser/src/parse/policy.rs +13 -2
  491. data/rust/parser/src/parse/value_object.rs +34 -17
  492. data/rust/parser/src/ruby_value.rs +1 -1
  493. data/rust/src/exemplar/commands.rs +4 -4
  494. data/rust/src/exemplar/constraints.rs +1 -1
  495. data/rust/src/exemplar/json.rs +43 -1
  496. data/rust/src/kernel/cli.rs +7 -5
  497. data/rust/src/kernel/dispatch.rs +2 -2
  498. data/rust/src/kernel/dispatch_guard_tests.rs +1 -1
  499. data/rust/src/kernel/expr.rs +66 -3
  500. data/rust/src/kernel/expression_operators/arithmetic.rs +130 -5
  501. data/rust/src/kernel/expression_operators/text.rs +26 -2
  502. data/rust/src/kernel/json.rs +15 -0
  503. data/rust/src/kernel/mod.rs +1 -1
  504. data/rust/src/kernel/orchestrate.rs +54 -5
  505. data/rust/src/kernel/orchestrate_guard_tests.rs +67 -0
  506. data/rust/src/kernel/pattern.rs +60 -0
  507. data/rust/src/kernel/reference_lookup.rs +90 -7
  508. metadata +178 -38
  509. data/lib/hecks/doors/cli_door/nesting.rb +0 -34
  510. data/lib/hecks/doors/cli_door/shorthand.rb +0 -85
  511. data/lib/hecks/doors/cli_door.rb +0 -124
  512. data/lib/hecks/doors/cli_runner/answers.rb +0 -138
  513. data/lib/hecks/doors/cli_runner/resolution.rb +0 -146
  514. data/lib/hecks/doors/cli_runner/settling.rb +0 -115
  515. data/lib/hecks/doors/cli_runner/streaming.rb +0 -90
  516. data/lib/hecks/doors/cli_runner/suggestions.rb +0 -43
  517. data/lib/hecks/doors/cli_runner.rb +0 -66
  518. data/lib/hecks/doors/command_request.rb +0 -119
  519. data/lib/hecks/doors/handle.rb +0 -208
  520. data/lib/hecks/doors/json_door.rb +0 -132
  521. data/lib/hecks/doors/launcher_options.rb +0 -247
  522. data/lib/hecks/doors/mcp_door/properties.rb +0 -47
  523. data/lib/hecks/doors/mcp_door/tool_calls.rb +0 -113
  524. data/lib/hecks/doors/mcp_door/tool_definitions/auditing.rb +0 -51
  525. data/lib/hecks/doors/mcp_door/tool_definitions/calling.rb +0 -69
  526. data/lib/hecks/doors/mcp_door/tool_definitions/reading.rb +0 -97
  527. data/lib/hecks/doors/mcp_door/tool_definitions.rb +0 -15
  528. data/lib/hecks/doors/mcp_door.rb +0 -156
  529. data/lib/hecks/doors/mcp_door_scope/argument_policy.rb +0 -92
  530. data/lib/hecks/doors/mcp_door_scope/env_problems.rb +0 -55
  531. data/lib/hecks/doors/mcp_door_scope/presenter.rb +0 -78
  532. data/lib/hecks/doors/mcp_door_scope.rb +0 -201
  533. data/lib/hecks/doors/mcp_guide_cache.rb +0 -75
  534. data/lib/hecks/doors/ruby_door/aggregate_door.rb +0 -162
  535. data/lib/hecks/doors/ruby_door/chapter.rb +0 -89
  536. data/lib/hecks/doors/ruby_door.rb +0 -80
  537. data/lib/hecks/doors/usage_cache.rb +0 -139
  538. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_widget.js.tmpl +0 -282
  539. data/lib/hecks/projections/site/cms_editor/templates/src/ui/media_picker.js.tmpl +0 -109
  540. data/prebuilt/c28d6d9db14eea3e17161e9d9c262efee30b256cf97693a7cfd552bce24b2f7f.marshal +0 -0
  541. data/prebuilt/cb375cba61ca0dbe8ddb9739c306510fe05e5f4b7a9b6955caff8224c63ea0e0.marshal +0 -0
  542. data/prebuilt/verdicts-d6d584ad43b5f3cebf88aafbbdbf024c38574acf18e06adffbc555f7786b1c2f.json +0 -1
@@ -0,0 +1,43 @@
1
+ __BANNER__
2
+
3
+ // The question a command asks before it runs, as a native `<dialog>`: a destructive command (cancel,
4
+ // discard, withdraw) and a command that publishes. The button that asks carries `data-confirm` with
5
+ // the dialog's id; the page's script opens it, and a browser with no script follows the button's link
6
+ // to a page that asks the same question.
7
+ import type { Aggregate, Command } from "../schema.ts";
8
+ import { esc } from "./html.ts";
9
+ import { backInput } from "./links.ts";
10
+ import type { Back } from "./links.ts";
11
+ import { commandLabel } from "./words.ts";
12
+
13
+ /** What makes one dialog differ from another on a page that holds several. */
14
+ export interface Asking {
15
+ /** Told apart when one command is asked about on several rows. */
16
+ suffix?: string;
17
+ /** Where a person is sent once the command has run. */
18
+ back?: Back | null;
19
+ /** The button's colour: `error` for something that cannot be undone, `neutral` for the rest. */
20
+ tone?: "error" | "neutral";
21
+ /** The words for the button, when they are not the command's own. */
22
+ button?: string;
23
+ /** Whether a draft still being written is saved first (see browser/autosave.js). */
24
+ flush?: boolean;
25
+ }
26
+
27
+ /** The id of a command's dialog, with the suffix that tells rows apart. */
28
+ export const dialogId = (command: Command, suffix = ""): string => `confirm-${command.name}${suffix ? `-${suffix}` : ""}`;
29
+
30
+ /** The question a command asks, as a native dialog that posts to `action` when confirmed. */
31
+ export function confirmDialog(agg: Aggregate, command: Command, action: string, id: string, form?: string, asking: Asking = {}): string {
32
+ const dialog = dialogId(command, asking.suffix);
33
+ const what = esc(asking.button ?? commandLabel(agg, command));
34
+ const detail = command.goal ? `${esc(command.goal)}.` : `This applies to ${esc(id)}.`;
35
+ const owner = form ? ` form="${esc(form)}"` : "";
36
+ const tone = asking.tone === "neutral" ? "btn btn-neutral" : "btn btn-error";
37
+ const head = `<h2 id="${dialog}-title" class="text-xl">${what}?</h2>\n<p class="mt-2 text-muted">${detail}</p>`;
38
+ const dismiss = what === "Cancel" ? "Go back" : "Cancel";
39
+ const buttons = `<div class="modal-action"><button type="submit" class="btn btn-ghost" formmethod="dialog" formnovalidate>${dismiss}</button><button type="submit" class="${tone}"${owner}>${what}</button></div>`;
40
+ const box = form ? `${head}\n${buttons}` : `<form method="post" action="${esc(action)}"${asking.flush ? " data-flush-draft" : ""}>\n${backInput(asking.back ?? null)}${head}\n${buttons}\n</form>`;
41
+ const close = `<form method="dialog" class="modal-backdrop"><button aria-label="Close">close</button></form>`;
42
+ return `<dialog class="modal" id="${dialog}" aria-labelledby="${dialog}-title">\n<div class="modal-box">\n${box}\n</div>\n${close}\n</dialog>`;
43
+ }
@@ -1,80 +1,275 @@
1
1
  __BANNER__
2
2
 
3
- // The inputs of a form, derived from attributes. A value-object attribute is a nested fieldset of
4
- // its parts; a `list_of` attribute is a run of repeatable rows (the rows it holds, then one blank
5
- // row to fill); a lifecycle state is not an input at all. Each input is named by its dotted path
6
- // (`byline.name`, `body.blocks.0.text`), which input.ts reads back into the shape the command takes.
3
+ // The inputs of a form, derived from attributes. A value object is a group of its parts (one with a
4
+ // single part is just that input, named for the attribute); a `list_of` attribute is a run of
5
+ // repeatable rows (the rows it holds, then one blank row to fill, which the page's script lets the
6
+ // person add to, remove and reorder); a lifecycle state is not an input at all. Each input is named
7
+ // by its dotted path (`byline.name`, `body.blocks.0.text`), which input.ts reads back into the shape
8
+ // the command takes. Inputs are plain `input` elements, so a form posts with no script at all. A
9
+ // text a rule bounds above 200 characters is a writing box, a value object with a media key and a
10
+ // description is a picture (its picker opens from the group), and a list whose members hold a body,
11
+ // a picture or a list of their own is drawn as cards (see blocks.ts) so a new member is made from a
12
+ // template, not copied from the page.
7
13
  import type { Aggregate, Attr } from "../schema.ts";
8
14
  import { SCHEMA } from "../schema.ts";
9
15
  import { esc, href } from "./html.ts";
16
+ import { icon } from "./icons.ts";
10
17
  import { bodyToFields, bodyToHtml, emptyBody, fieldsToInputs } from "./body_model.js";
18
+ import { heavy } from "./block_list.js";
19
+ import { blockList } from "./blocks.ts";
20
+ import type { Problem } from "./blocks.ts";
21
+ import { isMoment, secondsOf } from "./moments.ts";
22
+ import { datalists, pickerControl } from "./pickers.ts";
23
+ import type { Choices } from "./pickers.ts";
24
+ import { label, limitsNote, singular } from "./words.ts";
11
25
 
12
26
  type Value = unknown;
13
27
 
14
- const label = (attr: Attr): string => attr.name.replace(/_/g, " ");
28
+ /** The field a refusal named and what it said, so the form can mark it and the script can focus it. */
29
+ export interface Invalid {
30
+ field: string;
31
+ message: string;
32
+ }
15
33
 
16
- const optionalNote = (attr: Attr): string => (attr.optional ? ` <small>(optional)</small>` : "");
34
+ /** What a run of inputs needs to know beyond the attribute: whether to mark required, which field is invalid. */
35
+ interface Context {
36
+ mark: boolean;
37
+ invalid?: Invalid | null;
38
+ top: boolean;
39
+ /** The row's own heading already names the input, so its label is for assistive technology only. */
40
+ quiet?: boolean;
41
+ /** What is on offer for each picker (see pickers.ts); a picker with nothing on offer is a text box. */
42
+ choices?: Choices;
43
+ /** The body attribute that is written as a draft: its editor saves by itself (see browser/autosave.js). */
44
+ draft?: string;
45
+ /** The blocks of a list of mixed blocks that a refusal named (see blocks.ts). */
46
+ problems?: Problem[];
47
+ /** A list is drawn as plain rows, whatever its members hold: it is inside a card's group, one level in. */
48
+ rows?: boolean;
49
+ }
17
50
 
18
51
  const inputType = (attr: Attr): string => (attr.kind === "integer" || attr.kind === "number" ? "number" : "text");
19
52
 
20
53
  const member = (value: Value, key: string): Value => (value && typeof value === "object" ? (value as Record<string, Value>)[key] : undefined);
21
54
 
22
- function scalar(attr: Attr, name: string, value: Value): string {
55
+ const idOf = (name: string): string => `f-${name.replace(/[^A-Za-z0-9]+/g, "-")}`;
56
+
57
+ function caption(attr: Attr, context: Context): string {
58
+ const text = esc(label(attr.name));
59
+ if (attr.optional) return `${text} <span class="ml-1 text-xs font-normal text-muted">optional</span>`;
60
+ return context.mark ? `${text} <span class="text-primary" title="Required" aria-hidden="true">*</span>` : text;
61
+ }
62
+
63
+ /**
64
+ * The words of the refusal that is about the field at `name`, or null: the one the command named for this
65
+ * attribute at the top of the form, or one that names this part of the value object it was about.
66
+ */
67
+ function refusalAt(attr: Attr, name: string, context: Context): string | null {
68
+ if (context.top && context.invalid?.field === attr.name) return context.invalid.message;
69
+ const named = context.problems?.find((one) => one.slots.some((part) => name === `${one.name}.${part}` || name.startsWith(`${one.name}.${part}.`)));
70
+ return named ? named.message : null;
71
+ }
72
+
73
+ const flagged = (attr: Attr, name: string, context: Context): boolean => refusalAt(attr, name, context) !== null;
74
+
75
+ /** The attributes of the control that is invalid: the state and the link to its message (and to its hint, when it has one). */
76
+ function invalidAttrs(name: string, attr: Attr, context: Context, hint = false): string {
77
+ const ids = [...(hint ? [`${idOf(name)}-hint`] : []), ...(flagged(attr, name, context) ? [`${idOf(name)}-error`] : [])];
78
+ const described = ids.length > 0 ? ` aria-describedby="${esc(ids.join(" "))}"` : "";
79
+ return `${flagged(attr, name, context) ? ` aria-invalid="true"` : ""}${described}`;
80
+ }
81
+
82
+ function problem(name: string, attr: Attr, context: Context): string {
83
+ const text = refusalAt(attr, name, context);
84
+ return text !== null ? `<p class="text-xs font-semibold text-error" id="${esc(idOf(name))}-error">${esc(text)}</p>` : "";
85
+ }
86
+
87
+ /** A moment's input: the seconds in a number input; the script swaps it for a native date input in the viewer's zone. */
88
+ function moment(attr: Attr, name: string, value: Value, context: Context): string {
89
+ const id = esc(idOf(name));
90
+ const seconds = secondsOf(value);
91
+ const classes = flagged(attr, name, context) ? "input input-error w-full max-w-xl" : "input w-full max-w-xl";
92
+ const required = context.mark && !attr.optional ? ` aria-required="true"` : "";
93
+ const input = `<input type="number" step="1" class="${classes}" id="${id}" name="${esc(name)}" value="${seconds ?? ""}" data-epoch-raw${required}${invalidAttrs(name, attr, context)}>`;
94
+ const note = `<p class="text-xs text-muted" data-epoch-note>Whole seconds since 1970, in UTC.</p>`;
95
+ return `<div class="my-4 grid gap-1" data-moment-field="${esc(attr.widget ?? "datetime")}"><label for="${id}" class="${context.quiet ? "sr-only" : "font-semibold"}">${caption(attr, context)}</label>${input}${note}${problem(name, attr, context)}</div>`;
96
+ }
97
+
98
+ /** The control a picker stands in for the text box, or null when the attribute has none. */
99
+ function picked(attr: Attr, name: string, value: Value, context: Context): string | null {
100
+ if (!attr.picker) return null;
101
+ const held = member(value, "value") ?? value ?? "";
102
+ const required = context.mark && !attr.optional ? ` aria-required="true"` : "";
103
+ const control = { id: idOf(name), name, value: String(held), optional: attr.optional, invalid: flagged(attr, name, context), extra: `${required}${invalidAttrs(name, attr, context)}` };
104
+ return pickerControl(attr.picker, context.choices ?? {}, control);
105
+ }
106
+
107
+ /** A closed set's members as a native select, with the held value chosen; an optional one may be left unchosen. */
108
+ function select(attr: Attr, name: string, value: Value, context: Context): string {
109
+ const held = String(member(value, "value") ?? value ?? "");
110
+ const id = esc(idOf(name));
111
+ const required = context.mark && !attr.optional ? ` aria-required="true"` : "";
112
+ const classes = flagged(attr, name, context) ? "select select-error w-full max-w-xl" : "select w-full max-w-xl";
113
+ const blank = attr.optional || held === "" ? `<option value=""${held === "" ? " selected" : ""}>${attr.optional ? "None" : "Choose"}</option>` : "";
114
+ const options = (attr.options ?? []).map((option) => `<option value="${esc(option)}"${option === held ? " selected" : ""}>${esc(label(option))}</option>`);
115
+ const input = `<select class="${classes}" id="${id}" name="${esc(name)}"${required}${invalidAttrs(name, attr, context)}>${blank}${options.join("")}</select>`;
116
+ return `<div class="my-4 grid gap-1"><label for="${id}" class="${context.quiet ? "sr-only" : "font-semibold"}">${caption(attr, context)}</label>${input}${problem(name, attr, context)}</div>`;
117
+ }
118
+
119
+ function scalar(attr: Attr, name: string, value: Value, context: Context): string {
120
+ if (isMoment(attr)) return moment(attr, name, value, context);
121
+ if (attr.options && !attr.list) return select(attr, name, value, context);
23
122
  const text = esc(member(value, "value") ?? value ?? "");
123
+ const id = esc(idOf(name));
24
124
  if (attr.kind === "boolean") {
25
- return `<label><input type="checkbox" name="${esc(name)}"${value === true ? " checked" : ""}> ${esc(label(attr))}${optionalNote(attr)}</label>`;
125
+ return `<div class="my-4 flex items-center gap-2"><input type="checkbox" class="checkbox checkbox-sm" id="${id}" name="${esc(name)}"${value === true ? " checked" : ""}><label for="${id}" class="font-semibold">${caption(attr, context)}</label></div>`;
26
126
  }
27
127
  const step = attr.kind === "number" ? ` step="any"` : "";
28
- return `<label>${esc(label(attr))}${optionalNote(attr)}<input type="${inputType(attr)}"${step} name="${esc(name)}" value="${text}"></label>`;
128
+ const required = context.mark && !attr.optional ? ` aria-required="true"` : "";
129
+ const bad = flagged(attr, name, context);
130
+ const limit = attr.maxLength ? ` maxlength="${attr.maxLength}"` : "";
131
+ if (attr.multiline && !attr.list && attr.kind === "text") {
132
+ const classes = bad ? "textarea textarea-error w-full max-w-3xl" : "textarea w-full max-w-3xl";
133
+ const hint = `<p class="text-xs text-muted" id="${id}-hint">Up to ${attr.maxLength} characters.</p>`;
134
+ const box = `<textarea class="${classes}" rows="5" id="${id}" name="${esc(name)}"${limit}${required}${invalidAttrs(name, attr, context, true)}>\n${text}</textarea>`;
135
+ return `<div class="my-4 grid gap-1"><label for="${id}" class="${context.quiet ? "sr-only" : "font-semibold"}">${caption(attr, context)}</label>${box}${hint}${problem(name, attr, context)}</div>`;
136
+ }
137
+ const classes = bad ? "input input-error w-full max-w-xl" : "input w-full max-w-xl";
138
+ const chosen = picked(attr, name, value, context);
139
+ const input = chosen ?? `<input type="${inputType(attr)}"${step} class="${classes}" id="${id}" name="${esc(name)}" value="${text}"${limit}${required}${invalidAttrs(name, attr, context)}>`;
140
+ return `<div class="my-4 grid gap-1"><label for="${id}" class="${context.quiet ? "sr-only" : "font-semibold"}">${caption(attr, context)}</label>${input}${problem(name, attr, context)}</div>`;
141
+ }
142
+
143
+ /** A value object's one plain part as the attribute that holds it: it takes the attribute's name, picker and moment. */
144
+ function plainOf(attr: Attr, part: Attr, name: string): Attr {
145
+ return { ...part, name, optional: attr.optional, widget: attr.widget ?? part.widget, picker: attr.picker ?? part.picker, options: attr.options ?? part.options };
146
+ }
147
+
148
+ function parts(agg: Aggregate, attr: Attr, name: string, value: Value, context: Context): string {
149
+ const members = agg.valueObjects[attr.type] ?? [];
150
+ const inside = { ...context, top: false, mark: context.mark && !attr.optional };
151
+ return members.map((part) => field(agg, part, member(value, part.name), `${name}.`, inside)).join("\n");
152
+ }
153
+
154
+ /** The words of the refusal about the value object at `name` as a whole, or about one of its parts by name. */
155
+ function groupRefusal(attr: Attr, name: string, context: Context): string | null {
156
+ return refusalAt(attr, name, context) ?? context.problems?.find((one) => one.name === name)?.message ?? null;
157
+ }
158
+
159
+ /** The control a picture's media picker is opened from, or nothing when the editor has no pictures. */
160
+ function pickButton(): string {
161
+ return SCHEMA.media ? `<button type="button" class="btn btn-sm w-fit" data-action="pick-picture" data-media="${esc(href("media"))}">${icon("image")}<span>Choose a picture</span></button>` : "";
29
162
  }
30
163
 
31
- function parts(agg: Aggregate, attr: Attr, name: string, value: Value): string {
164
+ /**
165
+ * A value object that is a picture: its file, its description (required wherever a picture is) and its
166
+ * caption as one group with a button that opens the picker, in whatever depth it sits; any other part
167
+ * it has is drawn after it as it is anywhere else.
168
+ */
169
+ function pictureField(agg: Aggregate, attr: Attr, name: string, value: Value, context: Context): string {
170
+ const picture = attr.picture as NonNullable<Attr["picture"]>;
32
171
  const members = agg.valueObjects[attr.type] ?? [];
33
- return members.map((part) => field(agg, part, member(value, part.name), `${name}.`)).join("\n");
172
+ const inside: Context = { ...context, top: false, mark: context.mark && !attr.optional };
173
+ const named = [picture.key, picture.alt, picture.caption].filter((part): part is string => part !== undefined);
174
+ const control = (part?: string): string => {
175
+ const found = members.find((one) => one.name === part);
176
+ return found ? field(agg, found, member(value, found.name), `${name}.`, inside) : "";
177
+ };
178
+ const others = members.filter((one) => !named.includes(one.name)).map((one) => field(agg, one, member(value, one.name), `${name}.`, inside));
179
+ const thumb = `<img class="size-20 shrink-0 border border-base-300 object-cover" alt="" width="80" height="80" data-picture-thumb hidden>`;
180
+ const said = groupRefusal(attr, name, context);
181
+ const bad = said !== null ? ` aria-invalid="true" aria-describedby="${esc(idOf(name))}-error"` : "";
182
+ const why = said !== null ? `<p class="text-xs font-semibold text-error" id="${esc(idOf(name))}-error">${esc(said)}</p>` : "";
183
+ return `<fieldset class="mt-6 [&>legend+*]:clear-both"${bad}><legend class="float-left mb-2 w-full border-t border-base-300 pt-4 text-lg font-semibold">${caption(attr, context)}</legend>
184
+ <div class="my-4 grid gap-1 border border-base-300 p-3" data-picture data-key="${esc(`${name}.${picture.key}`)}" data-alt="${esc(`${name}.${picture.alt}`)}">
185
+ <div class="flex flex-wrap items-start gap-3">${thumb}<div class="grid min-w-0 flex-1 gap-1 max-sm:basis-full">${control(picture.key)}${control(picture.alt)}${control(picture.caption)}${pickButton()}</div></div>
186
+ <p class="text-xs text-muted">A picture needs a description for people who cannot see it.</p>
187
+ </div>
188
+ ${others.join("\n")}${why}
189
+ </fieldset>`;
34
190
  }
35
191
 
36
- function one(agg: Aggregate, attr: Attr, name: string, value: Value): string {
37
- if (attr.kind !== "object") return scalar(attr, name, value);
38
- return `<fieldset><legend>${esc(label(attr))}${optionalNote(attr)}</legend>\n${parts(agg, attr, name, value)}\n</fieldset>`;
192
+ function one(agg: Aggregate, attr: Attr, name: string, value: Value, context: Context): string {
193
+ if (attr.kind !== "object") return scalar(attr, name, value, context);
194
+ const members = agg.valueObjects[attr.type] ?? [];
195
+ const hit = groupRefusal(attr, name, context);
196
+ if (members.length === 1 && members[0].kind !== "object" && !members[0].list) {
197
+ const own: Context = hit !== null ? { ...context, top: true, invalid: { field: attr.name, message: hit } } : context;
198
+ return scalar(plainOf(attr, members[0], attr.name), `${name}.${members[0].name}`, member(value, members[0].name), own);
199
+ }
200
+ if (attr.picture) return pictureField(agg, attr, name, value, context);
201
+ const invalid = hit !== null ? ` aria-invalid="true" aria-describedby="${esc(idOf(name))}-error"` : "";
202
+ const why = hit !== null ? `<p class="text-xs font-semibold text-error" id="${esc(idOf(name))}-error">${esc(hit)}</p>` : "";
203
+ return `<fieldset class="mt-6 [&>legend+*]:clear-both"${invalid}><legend class="float-left mb-2 w-full border-t border-base-300 pt-4 text-lg font-semibold">${caption(attr, context)}</legend>\n${parts(agg, attr, name, value, context)}\n${why}</fieldset>`;
39
204
  }
40
205
 
41
- function listed(agg: Aggregate, attr: Attr, name: string, value: Value): string {
206
+ function listed(agg: Aggregate, attr: Attr, name: string, value: Value, context: Context): string {
42
207
  const held = Array.isArray(value) ? value : [];
208
+ const inside = { ...context, mark: false, top: false };
209
+ const members = agg.valueObjects[attr.type] ?? [];
210
+ const only = attr.kind === "object" && members.length === 1 && members[0].kind !== "object" && !members[0].list ? members[0] : null;
211
+ const word = attr.name.replace(/([^aeiou])ies$/, "$1y").replace(/s$/, "");
43
212
  const rows = [...held, undefined].map((item, index) => {
44
- const body = attr.kind === "object" ? parts(agg, attr, `${name}.${index}`, item) : scalar({ ...attr, list: false }, `${name}.${index}`, item);
45
- return `<fieldset class="row"><legend>${esc(label(attr))} ${index + 1}</legend>\n${body}\n</fieldset>`;
213
+ const body = only
214
+ ? scalar({ ...plainOf(attr, only, word), optional: only.optional }, `${name}.${index}.${only.name}`, member(item, only.name), { ...inside, quiet: true })
215
+ : attr.kind === "object"
216
+ ? parts(agg, attr, `${name}.${index}`, item, inside)
217
+ : scalar({ ...attr, name: word, list: false, optional: false }, `${name}.${index}`, item, { ...inside, quiet: true });
218
+ return `<fieldset class="relative my-3 border-l-2 border-base-300 pl-4 [&>legend+*]:clear-both" data-row data-index="${index}"><legend class="float-left w-full text-xs font-semibold text-muted">${esc(label(word))} ${index + 1}</legend>\n${body}\n</fieldset>`;
46
219
  });
47
- return `<fieldset><legend>${esc(label(attr))}${optionalNote(attr)}</legend>\n${rows.join("\n")}\n</fieldset>`;
220
+ const noun = esc(singular(word));
221
+ const invalid = flagged(attr, name, context) ? ` aria-invalid="true" aria-describedby="${esc(idOf(name))}-error"` : "";
222
+ const max = attr.max ? ` data-max="${attr.max}"` : "";
223
+ const limits = limitsNote(attr.min, attr.max);
224
+ const note = limits ? `\n<p class="clear-both mb-2 text-xs text-muted">${esc(limits)}</p>` : "";
225
+ return `<fieldset class="mt-6 [&>legend+*]:clear-both" data-repeat data-path="${esc(name)}" data-noun="${noun}"${max}${invalid}><legend class="float-left mb-2 w-full border-t border-base-300 pt-4 text-lg font-semibold">${caption(attr, context)}</legend>${note}\n<div data-rows>\n${rows.join("\n")}\n</div>${problem(name, attr, context)}\n</fieldset>`;
48
226
  }
49
227
 
50
- // The rich-text widget. The server renders the body's current value as the surface's HTML and as the
51
- // hidden inputs the form posts (the same dotted-path fields every value object and list uses), so a
52
- // browser without the script still posts the body unchanged; body_widget.js then edits the surface
53
- // and rewrites those inputs from the tree it reads back.
54
- function body(attr: Attr, name: string, value: Value): string {
228
+ // The rich-text editor. The server renders the body as its read-only text and as the hidden inputs
229
+ // the form posts (the same dotted-path fields every value object and list uses), so a browser
230
+ // without the script still posts the body unchanged; the editor replaces the text with a writing
231
+ // surface and rewrites those inputs from the document as it is edited.
232
+ function body(attr: Attr, name: string, value: Value, context: Context): string {
55
233
  const tree = value && typeof value === "object" ? value : emptyBody();
56
234
  const inputs = fieldsToInputs(bodyToFields(tree, name));
57
235
  const media = SCHEMA.media ? ` data-media="${esc(href("media"))}"` : "";
58
- return `<fieldset><legend>${esc(label(attr))}${optionalNote(attr)}</legend>
59
- <div class="body-editor" data-body-editor data-name="${esc(name)}"${media}>
60
- <div class="body-toolbar" data-toolbar role="toolbar" aria-label="Formatting"></div>
61
- <div class="body-surface body" data-surface role="textbox" aria-multiline="true" aria-label="${esc(label(attr))}">${bodyToHtml(tree)}</div>
62
- <p class="note" data-notes role="status" aria-live="polite"></p>
236
+ const draft = context.draft === attr.name ? " data-draft" : "";
237
+ const said = groupRefusal(attr, name, context);
238
+ const bad = said !== null ? ` aria-invalid="true" aria-describedby="${esc(idOf(name))}-error"` : "";
239
+ const why = said !== null ? `\n<p class="mt-2 text-sm font-semibold text-error" id="${esc(idOf(name))}-error">${esc(said)}</p>` : "";
240
+ return `<fieldset class="mt-6 [&>legend+*]:clear-both"${bad}><legend class="float-left mb-2 w-full border-t border-base-300 pt-4 text-lg font-semibold">${caption(attr, context)}</legend>
241
+ <div class="max-w-3xl" data-body-editor${draft} data-name="${esc(name)}" data-label="${esc(label(attr.name))}" data-body="${esc(JSON.stringify(tree))}"${media}>
242
+ <div class="prose border border-base-300 bg-base-100 p-6" data-body-fallback>${bodyToHtml(tree, 2)}</div>
243
+ <p class="mt-2 text-xs text-muted" data-needs-script>Editing this text needs scripts turned on in the browser.</p>
63
244
  <div data-fields>
64
245
  ${inputs}
65
246
  </div>
66
- </div>
247
+ </div>${why}
67
248
  </fieldset>`;
68
249
  }
69
250
 
251
+ /** A list of mixed blocks as cards; each slot is drawn by `field`, so a part is entered as it is anywhere else. */
252
+ function blocks(agg: Aggregate, attr: Attr, name: string, value: Value, context: Context): string {
253
+ const slot = (part: Attr, prefix: string, held: Value, message?: string, required?: boolean, rows?: boolean): string => {
254
+ const own: Context = { ...context, top: message !== undefined, invalid: message ? { field: part.name, message } : null, mark: required === true, quiet: false, draft: undefined, rows };
255
+ return field(agg, required ? { ...part, optional: false } : part, held, prefix, own);
256
+ };
257
+ const message = groupRefusal(attr, name, context) ?? undefined;
258
+ const rendering = { slot, draft: context.draft === attr.name, problems: context.problems ?? [], caption: caption(attr, context), media: SCHEMA.media ? href("media") : undefined, message };
259
+ return blockList(agg, attr, name, value, rendering);
260
+ }
261
+
70
262
  /** One attribute's inputs, `prefix` being the dotted path of the object that holds it. */
71
- export function field(agg: Aggregate, attr: Attr, value: Value, prefix = ""): string {
263
+ export function field(agg: Aggregate, attr: Attr, value: Value, prefix = "", context: Context = { mark: true, top: true }): string {
72
264
  const name = `${prefix}${attr.name}`;
73
- if (attr.widget === "body") return body(attr, name, value);
74
- return attr.list ? listed(agg, attr, name, value) : one(agg, attr, name, value);
265
+ if (attr.widget === "body") return body(attr, name, value, context);
266
+ if (attr.blockList && attr.list) return blocks(agg, attr, name, value, context);
267
+ if (attr.list && attr.kind === "object" && !context.rows && (attr.picture !== undefined || heavy(agg.valueObjects, attr.type))) return blocks(agg, attr, name, value, context);
268
+ return attr.list ? listed(agg, attr, name, value, context) : one(agg, attr, name, value, context);
75
269
  }
76
270
 
77
271
  /** The inputs for `attrs`, filled from `values` (an instance's state or a refused submission). */
78
- export function fields(agg: Aggregate, attrs: Attr[], values: Record<string, Value> = {}): string {
79
- return attrs.map((attr) => field(agg, attr, values[attr.name])).join("\n");
272
+ export function fields(agg: Aggregate, attrs: Attr[], values: Record<string, Value> = {}, invalid: Invalid | null = null, mark = true, choices: Choices = {}, draft?: string, problems: Problem[] = []): string {
273
+ const inputs = attrs.map((attr) => field(agg, attr, values[attr.name], "", { mark, invalid, top: true, choices, draft, problems }));
274
+ return [...inputs, datalists(agg, attrs, choices)].filter((text) => text !== "").join("\n");
80
275
  }
@@ -1,9 +1,15 @@
1
1
  __BANNER__
2
2
 
3
- // The page frame and the small helpers every page shares. Pages are plain strings built on the
4
- // server; every value that came from the domain or the person is escaped on its way in.
3
+ // The small helpers every page shares. Pages are plain strings built on the server; every value that
4
+ // came from the domain or the person is escaped on its way in, so no text becomes markup.
5
+ //
6
+ // Every class name below is written out whole. The stylesheet is built by Tailwind, which finds
7
+ // classes by reading these files as text, so a class made by joining strings would not be found and
8
+ // would have no style.
5
9
  import { EDITOR } from "../config.ts";
6
- import { SCHEMA } from "../schema.ts";
10
+ import type { Aggregate } from "../schema.ts";
11
+ import { icon } from "./icons.ts";
12
+ import { label } from "./words.ts";
7
13
 
8
14
  /** Escapes text for an HTML body or a double-quoted attribute. */
9
15
  export function esc(value: unknown): string {
@@ -15,88 +21,41 @@ export function href(...segments: string[]): string {
15
21
  return [EDITOR.basePath, ...segments.map(encodeURIComponent)].join("/");
16
22
  }
17
23
 
18
- const STYLE = `
19
- :root { color-scheme: light dark; --edge: #8886; --accent: #2457c5; }
20
- body { font: 16px/1.5 system-ui, sans-serif; margin: 0; }
21
- header, main { max-width: 64rem; margin: 0 auto; padding: 0.75rem 1rem; }
22
- header { display: flex; flex-wrap: wrap; gap: 0.5rem 1.25rem; border-bottom: 1px solid var(--edge); align-items: baseline; }
23
- header a.brand { font-weight: 700; }
24
- a { color: var(--accent); }
25
- table { border-collapse: collapse; width: 100%; }
26
- th, td { text-align: left; padding: 0.35rem 0.5rem; border-bottom: 1px solid var(--edge); vertical-align: top; }
27
- fieldset { border: 1px solid var(--edge); border-radius: 6px; margin: 0.5rem 0; padding: 0.5rem 0.75rem; }
28
- label { display: block; margin: 0.4rem 0; }
29
- label small { opacity: 0.7; }
30
- input:not([type=checkbox]), select, textarea { display: block; width: 100%; max-width: 32rem; font: inherit; padding: 0.3rem; box-sizing: border-box; }
31
- button { font: inherit; padding: 0.35rem 0.9rem; cursor: pointer; }
32
- .badge { display: inline-block; padding: 0 0.6rem; border-radius: 1rem; border: 1px solid var(--edge); font-size: 0.85rem; }
33
- .refusal { border: 1px solid #c33; border-radius: 6px; padding: 0.5rem 0.75rem; margin: 0.75rem 0; }
34
- .outline { border-left: 3px solid var(--edge); margin: 0.25rem 0 0.25rem 0.25rem; padding-left: 0.75rem; }
35
- .outline ul { margin: 0.2rem 0; padding-left: 1.25rem; }
36
- .note { opacity: 0.7; font-size: 0.85rem; }
37
- .body { overflow-wrap: anywhere; }
38
- .body p, .body h1, .body h2, .body h3, .body h4, .body blockquote, .body li { white-space: pre-wrap; }
39
- .body p, .body h1, .body h2, .body h3, .body h4, .body blockquote, .body ul, .body ol, .body figure { margin: 0.4rem 0; }
40
- .body blockquote { border-left: 3px solid var(--edge); padding-left: 0.75rem; }
41
- .body [data-align=left] { text-align: left; }
42
- .body [data-align=center] { text-align: center; }
43
- .body [data-align=right] { text-align: right; }
44
- .body [data-align=justify] { text-align: justify; }
45
- .body [data-indent="1"] { margin-left: 2rem; } .body [data-indent="2"] { margin-left: 4rem; } .body [data-indent="3"] { margin-left: 6rem; }
46
- .body [data-indent="4"] { margin-left: 8rem; } .body [data-indent="5"] { margin-left: 10rem; } .body [data-indent="6"] { margin-left: 12rem; }
47
- .body [data-indent="7"] { margin-left: 14rem; } .body [data-indent="8"] { margin-left: 16rem; }
48
- .body li[data-depth="1"] { margin-left: 1.5rem; } .body li[data-depth="2"] { margin-left: 3rem; } .body li[data-depth="3"] { margin-left: 4.5rem; }
49
- .body li[data-depth="4"] { margin-left: 6rem; } .body li[data-depth="5"] { margin-left: 7.5rem; }
50
- .body .body-media { display: inline-block; border: 1px dashed var(--edge); padding: 0.2rem 0.5rem; }
51
- .body-editor { max-width: 48rem; }
52
- .body-toolbar { display: flex; flex-wrap: wrap; gap: 0.25rem; margin-bottom: 0.4rem; }
53
- .body-toolbar button, .body-toolbar select { font-size: 0.85rem; padding: 0.15rem 0.5rem; width: auto; display: inline-block; }
54
- .body-surface { min-height: 8rem; border: 1px solid var(--edge); border-radius: 6px; padding: 0.4rem 0.75rem; }
55
- .body-surface:focus { outline: 2px solid var(--accent); }
56
- .body-surface figure { cursor: pointer; }
57
- .body-picker { border: 1px solid var(--edge); border-radius: 6px; padding: 0.5rem 0.75rem; margin-bottom: 0.5rem; }
58
- .picker-list { display: flex; flex-wrap: wrap; gap: 0.5rem; list-style: none; margin: 0.25rem 0; padding: 0; max-height: 12rem; overflow: auto; }
59
- .picker-list button { display: flex; flex-direction: column; align-items: center; max-width: 6rem; font-size: 0.8rem; }
60
- .picker-list button[aria-pressed=true] { outline: 2px solid var(--accent); }
61
- .picker-list img { object-fit: cover; }
62
- `;
24
+ /**
25
+ * The inline message a refused command shows above its form. The page's script moves focus to the
26
+ * first field marked invalid, or to this block when no field is named.
27
+ */
28
+ export function refusal(message: string | null): string {
29
+ return message ? `<div class="alert alert-error alert-soft mb-4 items-start" tabindex="-1" data-refusal>${icon("alert")}<p class="font-medium">${esc(message)}</p></div>` : "";
30
+ }
63
31
 
64
- /** True when a page body holds the rich-text widget, which is the only page that needs its script. */
65
- const hasWidget = (body: string): boolean => body.includes("data-body-editor");
32
+ /** A badge's classes and mark by tone. Colour is never the only cue: each tone has its own shape and the state's words. */
33
+ const TONES: Record<string, [classes: string, mark: string]> = {
34
+ neutral: ["badge badge-soft gap-1 font-semibold", "dot"],
35
+ ok: ["badge badge-soft badge-success gap-1 font-semibold", "success"],
36
+ info: ["badge badge-soft badge-info gap-1 font-semibold", "info"],
37
+ warn: ["badge badge-soft badge-warning gap-1 font-semibold", "alert"],
38
+ danger: ["badge badge-soft badge-error gap-1 font-semibold", "alert"],
39
+ };
40
+
41
+ /** A status as a badge: its words, a shape and a colour. */
42
+ export function badge(status: unknown, tone = "neutral"): string {
43
+ if (!status) return "";
44
+ const [classes, mark] = TONES[tone] ?? TONES.neutral;
45
+ return `<span class="${classes}">${icon(mark)}${esc(label(String(status)))}</span>`;
46
+ }
66
47
 
67
- /** The frame of every page: the editor's name, a link to each aggregate, and `body`. */
68
- export function page(title: string, body: string): string {
69
- const nav = SCHEMA.aggregates.map((agg) => `<a href="${esc(href(agg.name))}">${esc(agg.name)}</a>`).join("\n ");
70
- return `<!doctype html>
71
- <html lang="en">
72
- <head>
73
- <meta charset="utf-8">
74
- <meta name="viewport" content="width=device-width, initial-scale=1">
75
- <title>${esc(title)} - ${esc(EDITOR.title)}</title>
76
- <style>${STYLE}</style>
77
- </head>
78
- <body>
79
- <header>
80
- <a class="brand" href="${esc(EDITOR.basePath)}">${esc(EDITOR.title)}</a>
81
- <nav>
82
- ${nav}
83
- </nav>
84
- </header>
85
- <main>
86
- ${body}
87
- </main>
88
- ${hasWidget(body) ? `<script type="module" src="${esc(href("assets", "body_widget.js"))}"></script>` : ""}
89
- </body>
90
- </html>
91
- `;
48
+ /** The badge of an aggregate's lifecycle state, in the tone the schema gives that state. */
49
+ export function statusBadge(agg: Aggregate, status: unknown): string {
50
+ return badge(status, agg.lifecycle?.tones[String(status)] ?? "neutral");
92
51
  }
93
52
 
94
- /** The inline message a refused command shows above its form. */
95
- export function refusal(message: string | null): string {
96
- return message ? `<div class="refusal" role="alert">${esc(message)}</div>` : "";
53
+ /** A button that copies `value` to the clipboard (the page's script shows it and wires it; without a script it stays hidden). */
54
+ export function copyButton(value: string, what: string): string {
55
+ return `<button type="button" class="btn btn-ghost btn-square btn-xs text-muted" data-copy="${esc(value)}" title="Copy ${esc(what)}" aria-label="Copy ${esc(what)}" hidden>${icon("copy")}</button>`;
97
56
  }
98
57
 
99
- /** A lifecycle state as a badge. */
100
- export function badge(status: unknown): string {
101
- return status ? `<span class="badge">${esc(status)}</span>` : "";
58
+ /** A value with a button that copies it. */
59
+ export function copyable(value: string, what: string): string {
60
+ return `<span class="inline-flex items-center gap-1"><span>${esc(value)}</span>${copyButton(value, what)}</span>`;
102
61
  }
@@ -0,0 +1,15 @@
1
+ __BANNER__
2
+
3
+ // The editor's icons: one SVG sprite written into every page, and `icon(name)` to draw one. The
4
+ // symbols are the project's single source of icons (the generator's own list), so the browser code
5
+ // draws the same ones with `<use href="#i-name">` and nothing is fetched.
6
+
7
+ /** The sprite, placed once at the top of every page. It takes no room and is hidden from readers. */
8
+ export const SPRITE = `<svg class="absolute size-0 overflow-hidden" xmlns="http://www.w3.org/2000/svg" width="0" height="0" focusable="false" aria-hidden="true">
9
+ __ICONS__
10
+ </svg>`;
11
+
12
+ /** An icon that decorates a label next to it. */
13
+ export function icon(name: string): string {
14
+ return `<svg class="icon" aria-hidden="true" focusable="false"><use href="#i-${name}"/></svg>`;
15
+ }
@@ -3,18 +3,24 @@ __BANNER__
3
3
  // A submitted form turned back into what a command takes. The form's inputs are named by dotted
4
4
  // path (see fields.ts); this reads them against the same attributes, so a nested value object comes
5
5
  // back nested and a run of repeatable rows comes back as a list. A row left entirely blank is not a
6
- // row, and an optional part left blank is left out; a required text part left blank is sent as
7
- // empty text so that the domain's own rule refuses it in its own words. A rich-text body is read the
6
+ // row, and an optional part left blank is left out, as is an optional group with nothing in it; a
7
+ // required text part left blank in a group that has something in it is sent as empty text so that
8
+ // the domain's own rule refuses it in its own words. A rich-text body is read the
8
9
  // same way: the widget posts its tree as the same dotted-path fields (`body.blocks.0.spans.1.text`),
9
10
  // so there is no second encoding.
10
11
  import type { Aggregate, Attr } from "../schema.ts";
11
12
 
12
13
  export type Form = Map<string, string>;
13
14
 
14
- /** The fields of an `application/x-www-form-urlencoded` body, the last of a repeated name winning. */
15
+ /**
16
+ * The fields of an `application/x-www-form-urlencoded` body, the last of a repeated name winning.
17
+ * A browser sends a line break in a field as CR LF; it is read back as the single LF the domain
18
+ * holds, or a line break in a body would grow by a character each time the body is saved and
19
+ * opened again.
20
+ */
15
21
  export function parseForm(body: string): Form {
16
22
  const form: Form = new Map();
17
- for (const [key, value] of new URLSearchParams(body)) form.set(key, value);
23
+ for (const [key, value] of new URLSearchParams(body)) form.set(key, value.replace(/\r\n?/g, "\n"));
18
24
  return form;
19
25
  }
20
26
 
@@ -42,12 +48,23 @@ function text(attr: Attr, form: Form, path: string, optional: boolean): unknown
42
48
  return raw;
43
49
  }
44
50
 
51
+ /**
52
+ * A value object from its fields. One that is optional, or sits in an optional one, and was left
53
+ * entirely blank is left out; one with anything in it is read again with its required parts counted
54
+ * as required, so a part left blank is sent empty and the domain's own rule refuses it in its own words.
55
+ */
45
56
  function object(agg: Aggregate, attr: Attr, form: Form, path: string, optional: boolean): unknown {
46
- const built: Record<string, unknown> = {};
47
- for (const part of agg.valueObjects[attr.type] ?? []) {
48
- const value = build(agg, part, form, `${path}.`, optional);
49
- if (value !== undefined) built[part.name] = value;
50
- }
57
+ const read = (lenient: boolean): Record<string, unknown> => {
58
+ const built: Record<string, unknown> = {};
59
+ for (const part of agg.valueObjects[attr.type] ?? []) {
60
+ const value = build(agg, part, form, `${path}.`, lenient);
61
+ if (value !== undefined) built[part.name] = value;
62
+ }
63
+ return built;
64
+ };
65
+ const lenient = read(optional);
66
+ if (optional && blank(lenient)) return undefined;
67
+ const built = optional ? read(false) : lenient;
51
68
  return Object.keys(built).length > 0 ? built : undefined;
52
69
  }
53
70
 
@@ -64,9 +81,9 @@ export function build(agg: Aggregate, attr: Attr, form: Form, prefix: string, op
64
81
  return rows.filter((row) => !blank(row));
65
82
  }
66
83
 
67
- /** The arguments a command takes, from a submitted form. */
68
- export function commandArguments(agg: Aggregate, attrs: Attr[], form: Form): Record<string, unknown> {
69
- const args: Record<string, unknown> = {};
84
+ /** The arguments a command takes, from a submitted form; `empty` names the list arguments sent empty. */
85
+ export function commandArguments(agg: Aggregate, attrs: Attr[], form: Form, empty: string[] = []): Record<string, unknown> {
86
+ const args: Record<string, unknown> = Object.fromEntries(empty.map((name) => [name, []]));
70
87
  for (const attr of attrs) {
71
88
  const value = build(agg, attr, form, "");
72
89
  if (value !== undefined) args[attr.name] = value;