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,100 @@
1
+ __BANNER__
2
+
3
+ // How one record reaches another. A record is named to the rest of the domain by a plain key of the
4
+ // form `<kind>:<slug>`, where the kind is its aggregate's snake-case name (`post:first-light`), so
5
+ // this file makes that key, fills an input named by dotted path with it, and keeps the way back: a
6
+ // page reached from a record's page carries `_back` (an address under the editor), and the form on it
7
+ // posts `__back`, so a save returns to the record the person came from.
8
+ import { EDITOR } from "../config.ts";
9
+ import type { Aggregate, Attr } from "../schema.ts";
10
+ import { esc } from "./html.ts";
11
+ import { label } from "./words.ts";
12
+
13
+ /** A record's names, as a key and its parts: `post:first-light` is kind `post`, slug `first-light`. */
14
+ export interface Parts {
15
+ key: string;
16
+ kind: string;
17
+ slug: string;
18
+ id: string;
19
+ }
20
+
21
+ /** The snake-case name a kind is spelled: "MediaItem" is "media_item". */
22
+ export const snake = (name: string): string => name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
23
+
24
+ /**
25
+ * A record's key and its parts. An identity that already reads `<kind>:<slug>` is that key; any
26
+ * other identity is the slug of a key whose kind is its aggregate's.
27
+ */
28
+ export function partsOf(agg: Aggregate, id: string): Parts {
29
+ const given = /^([a-z][a-z0-9_]*):(.+)$/.exec(id);
30
+ if (given) return { key: id, kind: given[1], slug: given[2], id };
31
+ const kind = snake(agg.name);
32
+ return { key: `${kind}:${id}`, kind, slug: id, id };
33
+ }
34
+
35
+ /** The value object's one plain part that an attribute is entered as, or null when it has several. */
36
+ function onlyPart(agg: Aggregate, attr: Attr): Attr | null {
37
+ const parts = attr.kind === "object" ? (agg.valueObjects[attr.type] ?? []) : [];
38
+ return parts.length === 1 && parts[0].kind !== "object" && !parts[0].list ? parts[0] : null;
39
+ }
40
+
41
+ /** The name of the input that holds an attribute of one plain value, as a form posts it. */
42
+ export function plainName(agg: Aggregate, attr: Attr): string {
43
+ const part = onlyPart(agg, attr);
44
+ return part ? `${attr.name}.${part.name}` : attr.name;
45
+ }
46
+
47
+ /** `text` as a value of the attribute, wrapped in its value object when it has one. */
48
+ export function plainValue(agg: Aggregate, attr: Attr, text: string): unknown {
49
+ const part = onlyPart(agg, attr);
50
+ return part ? { [part.name]: text } : text;
51
+ }
52
+
53
+ /** The text an attribute of one plain value holds. */
54
+ export function plainText(value: unknown): string {
55
+ if (value && typeof value === "object") return plainText(Object.values(value as object).find((part) => part !== null && part !== undefined));
56
+ return value === null || value === undefined ? "" : String(value);
57
+ }
58
+
59
+ /** Where a page was reached from: the address to go back to, and the words for the link. */
60
+ export interface Back {
61
+ path: string;
62
+ label: string;
63
+ }
64
+
65
+ /**
66
+ * The way back a request names, or null. Only an address under the editor's base path is one, so a
67
+ * link written by anyone cannot send a person off the editor; a control character or a backslash is
68
+ * refused as well.
69
+ */
70
+ export function backOf(raw: string | null | undefined): Back | null {
71
+ if (!raw || raw.length > 400 || /[\u0000-\u001f\\]/.test(raw)) return null;
72
+ if (raw !== EDITOR.basePath && !raw.startsWith(`${EDITOR.basePath}/`)) return null;
73
+ const segments = raw.split(/[?#]/)[0].split("/").filter(Boolean);
74
+ const at = segments.indexOf("id");
75
+ const named = at >= 0 && segments[at + 1] ? segments[at + 1] : (segments.at(-1) ?? "");
76
+ return { path: raw, label: decoded(named) };
77
+ }
78
+
79
+ function decoded(text: string): string {
80
+ try {
81
+ return decodeURIComponent(text);
82
+ } catch {
83
+ return text;
84
+ }
85
+ }
86
+
87
+ /** `path` with the way back added to its query. */
88
+ export const withBack = (path: string, back: string): string => `${path}${path.includes("?") ? "&" : "?"}_back=${encodeURIComponent(back)}`;
89
+
90
+ /** The hidden input a form posts so that a save returns to `back`. */
91
+ export const backInput = (back: Back | null): string => (back ? `<input type="hidden" name="__back" value="${esc(back.path)}">` : "");
92
+
93
+ /** The link at the top of a page that leads back to the record it was reached from. */
94
+ export function backLink(back: Back | null): string {
95
+ if (!back) return "";
96
+ return `<p class="mb-3"><a class="btn btn-ghost btn-sm" href="${esc(back.path)}">Back to ${esc(back.label)}</a></p>`;
97
+ }
98
+
99
+ /** What a record is called in a heading, such as "Search wording" or "Pictures", by the aggregate's own words. */
100
+ export const titled = (agg: Aggregate): string => label(agg.name);
@@ -0,0 +1,44 @@
1
+ __BANNER__
2
+
3
+ // Moments in time. An integer the bluebook names as a date or a time is whole seconds since 1970 UTC.
4
+ // The server does not know the viewer's time zone, so it writes the moment in UTC inside a `time`
5
+ // element that carries the seconds, and the page's script (browser/moments.js) rewrites it in the
6
+ // viewer's own zone with how long ago or from now it is. A form carries the seconds in a plain
7
+ // number input that works without a script; the script replaces it with a native date or
8
+ // date-and-time input and keeps the seconds in step.
9
+ import type { Attr } from "../schema.ts";
10
+ import { esc } from "./html.ts";
11
+
12
+ /** Whether the attribute is a moment. */
13
+ export const isMoment = (attr: Attr): boolean => attr.widget === "date" || attr.widget === "datetime";
14
+
15
+ /** The whole seconds a stored value holds: the number itself, or the single part of a value object. */
16
+ export function secondsOf(value: unknown): number | null {
17
+ const held = value && typeof value === "object" && !Array.isArray(value) ? Object.values(value as object)[0] : value;
18
+ if (held === null || held === undefined || held === "" || typeof held === "boolean") return null;
19
+ const seconds = Number(held);
20
+ return Number.isFinite(seconds) && Math.abs(seconds) <= 8_640_000_000_000 ? Math.trunc(seconds) : null;
21
+ }
22
+
23
+ /** A moment in UTC, for a page read with no script: `YYYY-MM-DD`, or `YYYY-MM-DD HH:MM UTC` with a time. */
24
+ export function utcText(seconds: number, mode: string): string {
25
+ const iso = new Date(seconds * 1000).toISOString();
26
+ return mode === "date" ? iso.slice(0, 10) : `${iso.slice(0, 10)} ${iso.slice(11, 16)} UTC`;
27
+ }
28
+
29
+ /** A stored moment as a `time` element the script restates in the viewer's zone. */
30
+ export function momentTime(seconds: number, mode: string): string {
31
+ const iso = new Date(seconds * 1000).toISOString();
32
+ return `<time class="whitespace-nowrap" datetime="${esc(iso)}" data-moment="${esc(mode)}" data-epoch="${seconds}">${esc(utcText(seconds, mode))}</time>`;
33
+ }
34
+
35
+ /** A stored value (or a list of them) of a moment attribute, shown read-only. */
36
+ export function momentValue(attr: Attr, value: unknown): string {
37
+ const mode = attr.widget ?? "datetime";
38
+ const seconds = (item: unknown): string => {
39
+ const held = secondsOf(item);
40
+ return held === null ? `<span class="text-muted">Not set</span>` : momentTime(held, mode);
41
+ };
42
+ if (Array.isArray(value)) return value.length === 0 ? `<span class="text-muted">None</span>` : value.map(seconds).join(", ");
43
+ return seconds(value);
44
+ }
@@ -1,27 +1,63 @@
1
1
  __BANNER__
2
2
 
3
- // A value shown read-only, whatever its shape: a scalar as text, a list as a list, a value object
4
- // as its named parts. A rich-text body is not shown this way; pages.ts renders it with bodyToHtml.
3
+ // A value shown read-only, by its shape: a scalar as text, a value object as a list of its named
4
+ // parts, a list of scalars as chips and a list of value objects as a small table. A rich-text body
5
+ // is not shown this way; detail.ts renders it with bodyToHtml.
5
6
  import { esc } from "./html.ts";
7
+ import { label } from "./words.ts";
6
8
 
7
9
  const blank = (value: unknown): boolean => value === null || value === undefined || value === "";
8
10
 
9
- /** `value` as a nested read-only outline. */
11
+ /** The value inside a value object that has only a `value` part, so "Ann" is not `{value: "Ann"}`. */
12
+ function plain(value: unknown): unknown {
13
+ if (value && typeof value === "object" && !Array.isArray(value)) {
14
+ const entries = Object.entries(value as Record<string, unknown>);
15
+ if (entries.length === 1 && entries[0][0] === "value") return plain(entries[0][1]);
16
+ }
17
+ return value;
18
+ }
19
+
20
+ const isObject = (value: unknown): value is Record<string, unknown> => !!value && typeof value === "object" && !Array.isArray(value);
21
+
22
+ const none = (text: string): string => `<span class="text-muted">${text}</span>`;
23
+
24
+ function chips(items: unknown[]): string {
25
+ return `<ul class="flex flex-wrap gap-2">${items.map((item) => `<li class="rounded-full border border-base-300 px-3 py-0.5">${outline(item)}</li>`).join("")}</ul>`;
26
+ }
27
+
28
+ function subtable(rows: Record<string, unknown>[]): string {
29
+ const keys = [...new Set(rows.flatMap((row) => Object.keys(row)))];
30
+ const head = keys.map((key) => `<th scope="col" class="text-xs text-muted">${esc(label(key))}</th>`).join("");
31
+ const lines = rows.map((row) => `<tr>${keys.map((key) => `<td class="align-top">${outline(row[key])}</td>`).join("")}</tr>`);
32
+ return `<div class="overflow-x-auto"><table class="table table-sm"><thead><tr>${head}</tr></thead><tbody>${lines.join("")}</tbody></table></div>`;
33
+ }
34
+
35
+ function parts(value: Record<string, unknown>): string {
36
+ const rows = Object.entries(value).map(([key, part]) => `<div class="grid gap-0 sm:grid-cols-[8rem_minmax(0,1fr)] sm:gap-3"><dt class="text-muted">${esc(label(key))}</dt><dd>${outline(part)}</dd></div>`);
37
+ return `<dl class="grid gap-2">${rows.join("")}</dl>`;
38
+ }
39
+
40
+ /** `value` as read-only markup. */
10
41
  export function outline(value: unknown): string {
11
- if (blank(value)) return `<em>empty</em>`;
12
- if (Array.isArray(value)) return value.length === 0 ? `<em>none</em>` : `<ul>${value.map((item) => `<li>${outline(item)}</li>`).join("")}</ul>`;
13
- if (typeof value !== "object") return esc(value);
14
- const entries = Object.entries(value as Record<string, unknown>);
15
- if (entries.length === 1 && entries[0][0] === "value") return outline(entries[0][1]);
16
- const rows = entries.map(([key, part]) => `<div><strong>${esc(key)}</strong>: ${outline(part)}</div>`);
17
- return `<div class="outline">${rows.join("")}</div>`;
42
+ const shown = plain(value);
43
+ if (blank(shown)) return none("Not set");
44
+ if (Array.isArray(shown)) {
45
+ if (shown.length === 0) return none("None");
46
+ return shown.every((item) => isObject(plain(item))) ? subtable(shown.map((item) => plain(item) as Record<string, unknown>)) : chips(shown);
47
+ }
48
+ if (isObject(shown)) return parts(shown);
49
+ if (typeof shown === "boolean") return shown ? "Yes" : "No";
50
+ return esc(shown);
18
51
  }
19
52
 
20
- /** A one-line summary of a value, for a table cell: single-part objects show the part, lists their size. */
21
- export function summary(value: unknown): string {
22
- if (blank(value)) return "";
23
- if (Array.isArray(value)) return `${value.length} ${value.length === 1 ? "item" : "items"}`;
24
- if (typeof value !== "object") return esc(value);
25
- const parts = Object.values(value as Record<string, unknown>).filter((part) => !blank(part) && !Array.isArray(part));
26
- return esc(parts.join(", "));
53
+ /** A value as one line of plain text: single-part objects show the part, lists their size. */
54
+ export function summaryText(value: unknown): string {
55
+ const shown = plain(value);
56
+ if (blank(shown)) return "";
57
+ if (Array.isArray(shown)) return `${shown.length} ${shown.length === 1 ? "item" : "items"}`;
58
+ if (isObject(shown)) return Object.values(shown).map(plain).filter((part) => !blank(part) && !Array.isArray(part)).join(", ");
59
+ return typeof shown === "boolean" ? (shown ? "Yes" : "No") : String(shown);
27
60
  }
61
+
62
+ /** A one-line summary of a value, for a table cell, escaped. */
63
+ export const summary = (value: unknown): string => esc(summaryText(value));
@@ -1,123 +1,213 @@
1
1
  __BANNER__
2
2
 
3
- // The pages: a nav of aggregates, a list per aggregate (from a query when it has one), a detail page
4
- // per instance, and a form per command generated from the command's attributes. Lifecycle state is a
5
- // badge, and a lifecycle move is offered only from the states it applies in.
3
+ // The pages: an overview of what the domain holds, a list per aggregate (table.ts), one instance
4
+ // (detail.ts), and a form per command generated from the command's attributes. Every page is built
5
+ // into the frame in shell.ts. Lifecycle state is a badge, and a lifecycle move is offered only from
6
+ // the states it applies in. An editor of several chapters groups the overview and the navigation by
7
+ // chapter and puts the chapter in each address.
8
+ import { EDITOR } from "../config.ts";
9
+ import { at, keyOf } from "../names.ts";
6
10
  import type { Aggregate, Attr, Command, Query } from "../schema.ts";
7
- import { bodyToHtml } from "./body_model.js";
8
- import { fields } from "./fields.ts";
9
- import { badge, esc, href, page, refusal } from "./html.ts";
10
- import { outline, summary } from "./outline.ts";
11
11
  import { SCHEMA } from "../schema.ts";
12
+ import { detailPage as detail } from "./detail.ts";
13
+ import type { Submitted } from "./detail.ts";
14
+ import { confirmDialog } from "./dialogs.ts";
15
+ import { fields } from "./fields.ts";
16
+ import type { Invalid } from "./fields.ts";
17
+ import { esc, href, refusal } from "./html.ts";
18
+ import { backInput, backLink, withBack } from "./links.ts";
19
+ import type { Back } from "./links.ts";
20
+ import type { View } from "./paging.ts";
21
+ import type { Choices } from "./pickers.ts";
22
+ import { previewTools } from "./preview.ts";
23
+ import { scheduledTable } from "./schedule.ts";
24
+ import { SCHEDULED, countMark, groupOf, groups, onlyFor, page } from "./shell.ts";
25
+ import type { Crumb } from "./shell.ts";
26
+ import { listPage as list } from "./table.ts";
27
+ import type { World } from "./world.ts";
28
+ import { commandLabel, label, plural } from "./words.ts";
12
29
 
13
30
  type State = Record<string, unknown>;
14
31
  type Row = [string, State];
15
32
 
16
- /** What a submitted command left behind: the refusal to show, and the values to put back. */
17
- export interface Submitted {
18
- command: string;
19
- message: string | null;
20
- values: State;
21
- }
33
+ export type { Submitted };
34
+
35
+ const OVERVIEW: Crumb = ["Overview", href()];
36
+
37
+ const head = (title: string, note: string, extra = ""): string =>
38
+ `<div class="mb-6 flex flex-wrap items-start justify-between gap-3"><div><h1 class="text-3xl">${esc(title)}</h1><p class="mt-1 max-w-prose text-muted">${esc(note)}</p></div>${extra}</div>`;
22
39
 
23
- const label = (name: string): string => name.replace(/_/g, " ");
40
+ /** The trail to an aggregate's list: the overview, then the chapter when there are several. */
41
+ const trail = (agg: Aggregate): Crumb[] => [OVERVIEW, ...(agg.chapter ? [[label(agg.chapter)] as Crumb] : []), [plural(agg.name), href(...at(agg))]];
24
42
 
25
- /** The home page: every aggregate the domain declares. */
43
+ /** The words a page of an aggregate carries in its title: the plural, and the chapter when it could be mistaken for another. */
44
+ const titleOf = (agg: Aggregate): string => (agg.chapter ? `${plural(agg.name)} (${label(agg.chapter)})` : plural(agg.name));
45
+
46
+ function overviewItems(aggregates: Aggregate[]): string {
47
+ const items = aggregates.map((agg) => {
48
+ const note = agg.description ? `<span class="text-muted">${esc(agg.description)}</span>` : "";
49
+ return onlyFor(agg, `<li class="flex items-center justify-between gap-4"><a class="grid flex-1 gap-0.5 p-4 hover:bg-base-200/60" href="${esc(href(...at(agg)))}"><span class="text-base font-semibold">${esc(plural(agg.name))}</span>${note}</a><span class="pr-4 tabular-nums text-muted">${countMark(keyOf(agg))}</span></li>`);
50
+ });
51
+ return `<ul class="divide-y divide-base-300 border-y border-base-300 bg-base-100">\n${items.join("\n")}\n</ul>`;
52
+ }
53
+
54
+ /** The overview: every aggregate the domain declares, with what it is and how much of it there is. */
26
55
  export function home(): string {
27
- const items = SCHEMA.aggregates.map(
28
- (agg) => `<li><a href="${esc(href(agg.name))}">${esc(agg.name)}</a>${agg.description ? ` <span class="note">${esc(agg.description)}</span>` : ""}</li>`,
29
- );
30
- return page("Home", `<h1>${esc(SCHEMA.domain)}</h1>\n<ul>\n${items.join("\n")}\n</ul>`);
56
+ const sections = groups().map(([chapter, aggregates]) => {
57
+ if (chapter === null) return overviewItems(aggregates);
58
+ return groupOf(chapter, `<h2 class="mb-2 mt-8 text-lg">${esc(label(chapter))}</h2>\n${overviewItems(aggregates)}`);
59
+ });
60
+ const body = `${head(SCHEMA.chapters ? EDITOR.brand : SCHEMA.domain, "Choose what to edit.")}
61
+ ${sections.join("\n")}`;
62
+ return page("Overview", body, { crumbs: [["Overview"]] });
31
63
  }
32
64
 
33
- /** The page for a path that names nothing. */
34
- export function notFound(what: string): string {
35
- return page("Not found", `<h1>Not found</h1>\n<p>${esc(what)}</p>`);
65
+ /**
66
+ * The page for a path that names nothing, or, with a `title`, for another reason the person is
67
+ * sent back (the domain not answering, a chapter closed to their role): the heading and the tab
68
+ * say that reason, not "Not found".
69
+ */
70
+ export function notFound(what: string, title = "Not found"): string {
71
+ const body = `${head(title, what)}
72
+ <p><a class="btn" href="${esc(href())}">Back to the overview</a></p>`;
73
+ return page(title, body, { crumbs: [OVERVIEW, [title]] });
36
74
  }
37
75
 
38
- /** A form for one command; `to` adds the id input a command acting on another aggregate needs. */
39
- export function commandForm(agg: Aggregate, command: Command, action: string, shown: Submitted | null, state: State = {}, to = false): string {
40
- const own = shown && shown.command === command.name ? shown : null;
41
- const note = command.goal ? `<p class="note">${esc(command.goal)}</p>` : "";
42
- const target = to ? `<label>${esc(command.on ?? "")} id<input type="text" name="__to" value="${esc(own?.values.__to ?? "")}"></label>` : "";
43
- return `<form method="post" action="${esc(action)}">
44
- ${refusal(own?.message ?? null)}${note}
45
- ${target}
46
- ${fields(agg, command.attributes, own ? own.values : state)}
47
- <button type="submit">${esc(label(command.name))}</button>
48
- </form>`;
76
+ /** An aggregate's list: one page of its rows, with the filters in `view`. */
77
+ export function listPage(agg: Aggregate, rows: Row[], query: Query | null, error: string | null, asked: boolean, view: View, choices: Choices = {}): string {
78
+ return page(titleOf(agg), list(agg, rows, query, error, asked, view, choices), { active: keyOf(agg), crumbs: [OVERVIEW, ...(agg.chapter ? [[label(agg.chapter)] as Crumb] : []), [plural(agg.name)]] });
49
79
  }
50
80
 
51
- /** The page of one creating command (or a command that acts on another aggregate's instance). */
52
- export function commandPage(agg: Aggregate, command: Command, action: string, shown: Submitted | null): string {
53
- return page(command.name, `<h1>${esc(label(command.name))} <small>${esc(agg.name)}</small></h1>\n${commandForm(agg, command, action, shown, {}, command.on !== null && command.on !== agg.name)}`);
81
+ /** One instance, with the records and actions that go with it and its preview. */
82
+ export function detailPage(agg: Aggregate, id: string, values: State, shown: Submitted | null, world: World): string {
83
+ const crumbs: Crumb[] = [...trail(agg), [id]];
84
+ const tools = previewTools(agg, id);
85
+ return page(id, detail(agg, id, values, shown, world, tools), { active: keyOf(agg), crumbs, drawer: tools.drawer });
54
86
  }
55
87
 
56
- const columns = (agg: Aggregate) => agg.attributes.filter((attr) => !attr.list && attr.widget !== "body").slice(0, 4);
88
+ /** Every action waiting to run, soonest first. */
89
+ export function scheduledPage(world: World): string {
90
+ const body = `${head("Scheduled", "What is waiting to run, soonest first. These are saved here and run by something outside this editor.")}
91
+ ${scheduledTable(world, href("scheduled"))}`;
92
+ return page("Scheduled", body, { active: SCHEDULED, crumbs: [OVERVIEW, ["Scheduled"]] });
93
+ }
57
94
 
58
- function table(agg: Aggregate, rows: Row[]): string {
59
- const cols = columns(agg);
60
- const head = ["id", ...cols.map((attr) => label(attr.name)), ...(agg.lifecycle ? ["status"] : [])];
61
- const lines = rows.map(([id, state]) => {
62
- const cells = cols.map((attr) => `<td>${summary(state[attr.name])}</td>`);
63
- const status = agg.lifecycle ? `<td>${badge(state[agg.lifecycle.field])}</td>` : "";
64
- return `<tr><td><a href="${esc(href(agg.name, "id", id))}">${esc(id)}</a></td>${cells.join("")}${status}</tr>`;
65
- });
66
- const empty = rows.length === 0 ? `<tr><td colspan="${head.length}"><em>nothing here yet</em></td></tr>` : lines.join("\n");
67
- return `<table>\n<tr>${head.map((name) => `<th>${esc(name)}</th>`).join("")}</tr>\n${empty}\n</table>`;
95
+ /**
96
+ * What a command's form starts from: the instance's values, and for a working copy the instance has
97
+ * not saved yet (a draft not yet saved) the live content it stands for, and for a rich-text body or a
98
+ * block list the one it holds of the same shape, so an edit begins from the current content and not
99
+ * an empty page.
100
+ */
101
+ function startingValues(agg: Aggregate, command: Command, state: State): State {
102
+ const values = { ...state };
103
+ const unset = (value: unknown): boolean => value == null || (Array.isArray(value) && value.length === 0);
104
+ const drafts = agg.drafts;
105
+ if (drafts && drafts.save === command.name && unset(values[drafts.attribute]) && !unset(state[drafts.live])) values[drafts.attribute] = state[drafts.live];
106
+ for (const attr of command.attributes.filter((candidate: Attr) => (candidate.widget === "body" || candidate.blockList) && unset(values[candidate.name]))) {
107
+ const held = agg.attributes.find((other) => other.type === attr.type && other.name !== attr.name && !unset(state[other.name]));
108
+ if (held) values[attr.name] = state[held.name];
109
+ }
110
+ return values;
68
111
  }
69
112
 
70
- function queryLinks(agg: Aggregate, current: Query | null): string {
71
- const links = agg.queries.map((query) => `<a href="${esc(`${href(agg.name)}?query=${encodeURIComponent(query.name)}`)}">${esc(label(query.name))}</a>`);
72
- const all = `<a href="${esc(href(agg.name))}">all</a>`;
73
- return `<p>${[all, ...links].join(" | ")}${current ? ` <span class="note">showing ${esc(current.name)}</span>` : ""}</p>`;
113
+ const FORM = "command-form";
114
+
115
+ function invalidOf(own: Submitted | null): Invalid | null {
116
+ return own?.field && own.message ? { field: own.field, message: own.message.replace(/^.*?: /s, "") } : null;
74
117
  }
75
118
 
76
- function queryForm(agg: Aggregate, query: Query | null): string {
77
- if (!query || query.attributes.length === 0) return "";
78
- const action = esc(href(agg.name));
79
- return `<form method="get" action="${action}"><input type="hidden" name="query" value="${esc(query.name)}">
80
- ${fields(agg, query.attributes)}
81
- <button type="submit">Ask</button></form>`;
119
+ /** The buttons of a command's form: the command itself, and for one that makes something, the same and then another. */
120
+ function submits(agg: Aggregate, command: Command): string {
121
+ const text = esc(commandLabel(agg, command));
122
+ if (command.destructive) return `<button type="submit" class="btn btn-error" data-busy data-confirm="confirm-${esc(command.name)}">${text}</button>`;
123
+ const again = command.creates ? `<button type="submit" class="btn" name="__again" value="1" data-busy>Save and add another</button>` : "";
124
+ return `${again}<button type="submit" class="btn btn-neutral" data-busy>${text}</button>`;
82
125
  }
83
126
 
84
- /** An aggregate's list: the rows of the query asked, or of everything it holds. */
85
- export function listPage(agg: Aggregate, rows: Row[], query: Query | null, error: string | null): string {
86
- const creating = agg.commands.filter((command) => command.creates);
87
- const actions = creating.map((command) => `<a href="${esc(href(agg.name, "new", command.name))}">${esc(label(command.name))}</a>`).join(" | ");
88
- const elsewhere = agg.commands.filter((command) => !command.creates && command.on !== agg.name);
89
- const others = elsewhere.map((command) => `<a href="${esc(href(agg.name, "run", command.name))}">${esc(label(command.name))}</a>`).join(" | ");
90
- const body = `<h1>${esc(agg.name)}</h1>\n${agg.description ? `<p class="note">${esc(agg.description)}</p>` : ""}
91
- ${actions ? `<p>New: ${actions}</p>` : ""}${others ? `<p>Other: ${others}</p>` : ""}
92
- ${queryLinks(agg, query)}${queryForm(agg, query)}${refusal(error)}
93
- ${table(agg, rows)}`;
94
- return page(agg.name, body);
127
+ /** What a command's form may be given beyond the command: the instance's values, the choices on offer and the way back. */
128
+ interface Extras {
129
+ state?: State;
130
+ /** Whether the form names the instance to act on, for a command that acts on another aggregate's. */
131
+ to?: boolean;
132
+ cancel?: string;
133
+ choices?: Choices;
134
+ back?: Back | null;
135
+ /** The instance the form acts on, which a draft's buttons need. */
136
+ id?: string;
95
137
  }
96
138
 
97
- function available(agg: Aggregate, command: Command, state: State): boolean {
98
- const move = agg.lifecycle?.transitions.find((transition) => transition.verb === command.name);
99
- return !move || !agg.lifecycle || move.from.includes(String(state[agg.lifecycle.field]));
139
+ /**
140
+ * The status line, and the buttons that publish or throw away the draft, on the page of the command
141
+ * that saves one. The question each asks is a dialog after the form; the publish dialog saves what is
142
+ * still unsaved first (`data-flush-draft`, see browser/autosave.js).
143
+ */
144
+ function draftTools(agg: Aggregate, command: Command, extras: Extras): { bar: string; dialogs: string } {
145
+ const drafts = agg.drafts;
146
+ if (!drafts || drafts.save !== command.name || !extras.id) return { bar: "", dialogs: "" };
147
+ const id = extras.id;
148
+ const back = extras.back ?? null;
149
+ const saved = (extras.state ?? {})[drafts.attribute];
150
+ const held = saved != null && !(Array.isArray(saved) && saved.length === 0);
151
+ const make = (name?: string): Command | undefined => agg.commands.find((candidate) => candidate.name === name && candidate.on === agg.name && candidate.attributes.length === 0);
152
+ const ask = (command: Command | undefined, words: string, classes: string, tone: "neutral" | "error", flush: boolean, hidden: boolean): [string, string] => {
153
+ if (!command) return ["", ""];
154
+ const to = href(...at(agg), "id", id, command.name);
155
+ const link = back ? withBack(to, back.path) : to;
156
+ const button = `<a class="${classes}" href="${esc(link)}" data-confirm="confirm-${esc(command.name)}"${hidden ? " hidden data-draft-discard" : ""}>${esc(words)}</a>`;
157
+ return [button, confirmDialog(agg, command, to, id, undefined, { back, tone, button: words, flush })];
158
+ };
159
+ const publish = ask(make(drafts.publish), "Publish draft", "btn btn-neutral", "neutral", true, false);
160
+ const discard = ask(make(drafts.discard), "Discard draft", "btn btn-error btn-outline", "error", false, !held);
161
+ const live = esc(label(drafts.live).toLowerCase());
162
+ const note = `${held ? "A draft is saved." : "No draft is saved yet."} The live ${live} changes only when the draft is published.`;
163
+ const bar = `<div class="mb-4 flex flex-wrap items-center gap-2" data-draft-bar><p class="mr-auto text-sm text-muted" role="status" aria-live="polite" data-draft-status>${note}</p>${discard[0]}${publish[0]}</div>`;
164
+ return { bar, dialogs: `${publish[1]}\n${discard[1]}` };
100
165
  }
101
166
 
102
- /** An attribute's value read-only: a rich-text body as escaped HTML, anything else as an outline. */
103
- function shown(attr: Attr, value: unknown): string {
104
- if (attr.widget !== "body") return outline(value);
105
- return value && typeof value === "object" ? `<div class="body">${bodyToHtml(value)}</div>` : `<em>empty</em>`;
167
+ /** A form for one command; `to` adds the id input a command acting on another aggregate needs. */
168
+ export function commandForm(agg: Aggregate, command: Command, action: string, shown: Submitted | null, extras: Extras = {}): string {
169
+ const { state = {}, to = false, choices = {}, back = null } = extras;
170
+ const own = shown && shown.command === command.name ? shown : null;
171
+ const note = command.goal ? `<p class="mb-4 max-w-prose text-muted">${esc(command.goal)}.</p>` : "";
172
+ const target = to
173
+ ? `<div class="my-4 grid gap-1"><label for="f-to" class="font-semibold">${esc(command.on ?? "")} id <span class="text-primary" aria-hidden="true">*</span></label><input type="text" class="input w-full max-w-xl" id="f-to" name="__to" value="${esc(own?.values.__to ?? "")}"></div>`
174
+ : "";
175
+ const dialog = command.destructive ? confirmDialog(agg, command, action, "", FORM, { back }) : "";
176
+ const draft = draftTools(agg, command, extras);
177
+ const saved = command.attributes.find((attr) => attr.name === agg.drafts?.attribute);
178
+ const whole = saved !== undefined && saved.widget !== "body" && saved.blockList === undefined;
179
+ const mode = draft.bar ? ` data-draft="${esc(agg.drafts?.attribute)}"${whole ? " data-draft-form" : ""}` : "";
180
+ const leave = back?.path ?? extras.cancel ?? href(...at(agg));
181
+ return `<form class="max-w-3xl" id="${FORM}" method="post" action="${esc(action)}" data-form${mode}>
182
+ ${backInput(back)}${refusal(own?.message ?? null)}${note}
183
+ ${draft.bar}
184
+ ${target}
185
+ ${fields(agg, command.attributes, own ? own.values : startingValues(agg, command, state), invalidOf(own), true, choices, draft.bar ? agg.drafts?.attribute : undefined, own?.problems ?? [])}
186
+ <div data-actionbar class="sticky bottom-0 z-10 -mx-4 mt-8 flex items-center justify-end gap-2 border-t border-base-300 bg-base-100 px-4 pt-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] shadow-[0_-8px_16px_-12px_rgb(0_0_0/0.25)]"><p class="mr-auto text-xs text-muted"><span class="text-primary" aria-hidden="true">*</span> Required</p><a class="btn btn-ghost" href="${esc(leave)}">Cancel</a>${submits(agg, command)}</div>
187
+ </form>
188
+ ${dialog}${draft.dialogs}`;
106
189
  }
107
190
 
108
- function state(agg: Aggregate, values: State): string {
109
- const rows = agg.attributes.map((attr) => `<tr><th>${esc(label(attr.name))}</th><td>${shown(attr, values[attr.name])}</td></tr>`);
110
- return `<table>\n${rows.join("\n")}\n</table>`;
191
+ /** The page of one creating command (or a command that acts on another aggregate's instance); `state` fills it, `back` is where saving returns to. */
192
+ export function commandPage(agg: Aggregate, command: Command, action: string, shown: Submitted | null, choices: Choices = {}, extras: Extras = {}): string {
193
+ const crumbs: Crumb[] = [...trail(agg), [commandLabel(agg, command)]];
194
+ const form = commandForm(agg, command, action, shown, { ...extras, to: command.on !== null && command.on !== agg.name, choices });
195
+ return page(commandLabel(agg, command), `${backLink(extras.back ?? null)}${head(commandLabel(agg, command), label(agg.name))}\n${form}`, { active: keyOf(agg), crumbs });
111
196
  }
112
197
 
113
- /** One instance: its status, its values read-only, and a form for each command that applies to it. */
114
- export function detailPage(agg: Aggregate, id: string, values: State, shown: Submitted | null): string {
115
- const acting = agg.commands.filter((command) => command.on === agg.name && available(agg, command, values));
116
- const forms = acting.map((command) => {
117
- const open = command.attributes.length > 0 ? ` ${shown && shown.command === command.name ? "open" : ""}` : "";
118
- const body = commandForm(agg, command, href(agg.name, "id", id, command.name), shown, values);
119
- return command.attributes.length > 0 ? `<details${open}><summary>${esc(label(command.name))}</summary>\n${body}\n</details>` : body;
120
- });
121
- const status = agg.lifecycle ? badge(values[agg.lifecycle.field]) : "";
122
- return page(id, `<h1>${esc(id)} ${status}</h1>\n<p><a href="${esc(href(agg.name))}">${esc(agg.name)}</a></p>\n${state(agg, values)}\n<h2>Change</h2>\n${forms.join("\n")}`);
198
+ /** The page of a command that acts on one instance: its form, or the question a destructive one asks. */
199
+ export function instanceCommandPage(agg: Aggregate, id: string, command: Command, values: State, shown: Submitted | null, choices: Choices = {}, back: Back | null = null): string {
200
+ const action = href(...at(agg), "id", id, command.name);
201
+ const crumbs: Crumb[] = [...trail(agg), [id, href(...at(agg), "id", id)], [commandLabel(agg, command)]];
202
+ const tools = previewTools(agg, id);
203
+ const title = `${backLink(back)}${head(commandLabel(agg, command), `${label(agg.name)} ${id}`, tools.button)}`;
204
+ const cancel = back?.path ?? href(...at(agg), "id", id);
205
+ const frame = { active: keyOf(agg), crumbs, drawer: tools.drawer };
206
+ if (command.destructive && command.attributes.length === 0) {
207
+ const own = shown && shown.command === command.name ? shown.message : null;
208
+ const ask = `<form class="max-w-3xl" method="post" action="${esc(action)}" data-form>${backInput(back)}${refusal(own)}<p>${esc(command.goal ?? `This applies to ${id}`)}. Choose ${esc(commandLabel(agg, command))} to go ahead.</p>
209
+ <div class="mt-6 flex justify-end gap-2"><a class="btn btn-ghost" href="${esc(cancel)}">Cancel</a><button type="submit" class="btn btn-error" data-busy>${esc(commandLabel(agg, command))}</button></div></form>`;
210
+ return page(commandLabel(agg, command), `${title}\n${ask}`, frame);
211
+ }
212
+ return page(commandLabel(agg, command), `${title}\n${commandForm(agg, command, action, shown, { state: values, cancel, choices, back, id })}`, frame);
123
213
  }