hecks 3.10.0 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/bluebook/pizzas.bluebook +29 -0
  3. data/examples/pizzas/bluebook/pizzas.hecksagon +7 -0
  4. data/exe/hecks +4 -4
  5. data/lib/hecks/adapters/driven/d1.rb +2 -10
  6. data/lib/hecks/adapters/driven/heki.adapter +1 -1
  7. data/lib/hecks/adapters/driven/heki.rb +2 -12
  8. data/lib/hecks/adapters/driven/lambda.adapter +1 -1
  9. data/lib/hecks/adapters/driven/lambda.rb +3 -18
  10. data/lib/hecks/adapters/driven/postgres.rb +4 -11
  11. data/lib/hecks/adapters/driven/sqlite.adapter +1 -1
  12. data/lib/hecks/adapters/driven/sqlite.rb +2 -10
  13. data/lib/hecks/adapters/driving/admission.rb +46 -0
  14. data/lib/hecks/adapters/driving/cli/nesting.rb +36 -0
  15. data/lib/hecks/adapters/driving/cli/shorthand.rb +88 -0
  16. data/lib/hecks/adapters/driving/cli.rb +127 -0
  17. data/lib/hecks/adapters/driving/cli_runner/answers.rb +160 -0
  18. data/lib/hecks/adapters/driving/cli_runner/resolution.rb +155 -0
  19. data/lib/hecks/adapters/driving/cli_runner/settling.rb +120 -0
  20. data/lib/hecks/adapters/driving/cli_runner/streaming.rb +93 -0
  21. data/lib/hecks/adapters/driving/cli_runner/suggestions.rb +47 -0
  22. data/lib/hecks/adapters/driving/cli_runner.rb +69 -0
  23. data/lib/hecks/adapters/driving/command_request.rb +121 -0
  24. data/lib/hecks/adapters/driving/handle.rb +214 -0
  25. data/lib/hecks/adapters/driving/json.rb +136 -0
  26. data/lib/hecks/adapters/driving/launcher_options.rb +267 -0
  27. data/lib/hecks/adapters/driving/mcp/properties.rb +49 -0
  28. data/lib/hecks/adapters/driving/mcp/tool_calls.rb +116 -0
  29. data/lib/hecks/adapters/driving/mcp/tool_definitions/auditing.rb +53 -0
  30. data/lib/hecks/adapters/driving/mcp/tool_definitions/calling.rb +71 -0
  31. data/lib/hecks/adapters/driving/mcp/tool_definitions/reading.rb +99 -0
  32. data/lib/hecks/adapters/driving/mcp/tool_definitions.rb +18 -0
  33. data/lib/hecks/adapters/driving/mcp.rb +159 -0
  34. data/lib/hecks/adapters/driving/mcp_guide_cache.rb +78 -0
  35. data/lib/hecks/adapters/driving/mcp_scope/argument_policy.rb +97 -0
  36. data/lib/hecks/adapters/driving/mcp_scope/env_problems.rb +69 -0
  37. data/lib/hecks/adapters/driving/mcp_scope/presenter.rb +83 -0
  38. data/lib/hecks/adapters/driving/mcp_scope.rb +213 -0
  39. data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +171 -0
  40. data/lib/hecks/adapters/driving/ruby/chapter.rb +91 -0
  41. data/lib/hecks/adapters/driving/ruby.rb +83 -0
  42. data/lib/hecks/adapters/driving/usage_cache.rb +144 -0
  43. data/lib/hecks/{doors.rb → adapters/driving.rb} +10 -6
  44. data/lib/hecks/behaviors/expectations/boot.rb +1 -1
  45. data/lib/hecks/bench/ruby_runner.rb +1 -1
  46. data/lib/hecks/bluebook/ask_resolution.rb +130 -0
  47. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +8 -2
  48. data/lib/hecks/bluebook/assembly/contracts/rules.rb +4 -2
  49. data/lib/hecks/bluebook/assembly/contracts/structure.rb +3 -1
  50. data/lib/hecks/bluebook/assembly/marks.rb +1 -0
  51. data/lib/hecks/bluebook/attribute.rb +1 -1
  52. data/lib/hecks/bluebook/behaviour/command.rb +13 -8
  53. data/lib/hecks/bluebook/behaviour/policy.rb +30 -0
  54. data/lib/hecks/bluebook/capabilities/attribute_values.rb +72 -0
  55. data/lib/hecks/bluebook/capabilities.rb +91 -9
  56. data/lib/hecks/bluebook/domain_port.rb +16 -4
  57. data/lib/hecks/bluebook/dsl/adapter_builder.rb +18 -6
  58. data/lib/hecks/bluebook/dsl/binding_proxy.rb +13 -0
  59. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/computed_sources.rb +98 -0
  60. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/fan_out_rows.rb +42 -0
  61. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/projected_fields.rb +14 -2
  62. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/provisions.rb +69 -3
  63. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/with_projections.rb +17 -10
  64. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +5 -0
  65. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
  66. data/lib/hecks/bluebook/dsl/command_builder/mutations.rb +8 -1
  67. data/lib/hecks/bluebook/dsl/command_builder/operands.rb +121 -0
  68. data/lib/hecks/bluebook/dsl/command_builder.rb +2 -0
  69. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +10 -1
  70. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +45 -2
  71. data/lib/hecks/bluebook/dsl/policy_builder.rb +47 -10
  72. data/lib/hecks/bluebook/dsl/rule_reference.rb +7 -4
  73. data/lib/hecks/bluebook/dsl/world_builder.rb +1 -1
  74. data/lib/hecks/bluebook/expression/ast_json/emitters.rb +6 -0
  75. data/lib/hecks/bluebook/expression/ast_json.rb +9 -6
  76. data/lib/hecks/bluebook/expression/ast_reader.rb +14 -9
  77. data/lib/hecks/bluebook/expression/projection.json +36 -0
  78. data/lib/hecks/bluebook/expression/resolver/operations.rb +31 -0
  79. data/lib/hecks/bluebook/expression/resolver/parsing.rb +38 -5
  80. data/lib/hecks/bluebook/expression/resolver/scanning.rb +22 -5
  81. data/lib/hecks/bluebook/expression/resolver/values.rb +10 -0
  82. data/lib/hecks/bluebook/expression/resolver.rb +20 -1
  83. data/lib/hecks/bluebook/hecksagon.rb +23 -3
  84. data/lib/hecks/bluebook/lifecycle.rb +22 -3
  85. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +13 -1
  86. data/lib/hecks/bluebook/meta_validator/modes.rb +4 -1
  87. data/lib/hecks/bluebook/meta_validator/readings.rb +18 -1
  88. data/lib/hecks/bluebook/meta_validator/reconstruction/heads.rb +2 -1
  89. data/lib/hecks/bluebook/meta_validator/shapes.rb +5 -0
  90. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +1 -1
  91. data/lib/hecks/bluebook/meta_validator.rb +12 -12
  92. data/lib/hecks/bluebook/model_check/ask_checks.rb +71 -0
  93. data/lib/hecks/bluebook/model_check/policy_checks.rb +2 -1
  94. data/lib/hecks/bluebook/model_check/reference_checks.rb +70 -0
  95. data/lib/hecks/bluebook/model_check.rb +8 -2
  96. data/lib/hecks/bluebook/policy.rb +4 -2
  97. data/lib/hecks/bluebook/smoke_test.rb +1 -1
  98. data/lib/hecks/bluebook/synthesizer.rb +2 -0
  99. data/lib/hecks/bluebook.rb +1 -0
  100. data/lib/hecks/cli/command_table.rb +1 -1
  101. data/lib/hecks/cli/console.rb +2 -1
  102. data/lib/hecks/cli/document.rb +1 -1
  103. data/lib/hecks/cli/launcher_words.rb +4 -4
  104. data/lib/hecks/cli/mcp.rb +4 -4
  105. data/lib/hecks/cli/project_cli/launcher_source.rb +2 -2
  106. data/lib/hecks/cli/project_cli/templates.rb +5 -5
  107. data/lib/hecks/cli/project_cli.rb +9 -6
  108. data/lib/hecks/cli/run.rb +2 -2
  109. data/lib/hecks/cli/smoke_http/checks.rb +4 -1
  110. data/lib/hecks/cli/smoke_http/transport.rb +3 -3
  111. data/lib/hecks/cli/smoke_http.rb +3 -0
  112. data/lib/hecks/codemod.rb +5 -2
  113. data/lib/hecks/corpus/routes.rb +7 -0
  114. data/lib/hecks/corpus/rust.rb +3 -2
  115. data/lib/hecks/deploy/bluebook/deploy.bluebook +18 -2
  116. data/lib/hecks/doc/reference/rendering.rb +6 -3
  117. data/lib/hecks/forms/app/commands.rb +8 -7
  118. data/lib/hecks/forms/app/queries.rb +2 -1
  119. data/lib/hecks/forms/app.rb +1 -1
  120. data/lib/hecks/forms/record_renderer.rb +1 -1
  121. data/lib/hecks/framework/bluebook/privacy.bluebook +11 -0
  122. data/lib/hecks/fuzzing/concurrent_dispatch.rb +4 -1
  123. data/lib/hecks/fuzzing/failure_triage.rb +2 -1
  124. data/lib/hecks/fuzzing/mutation/run.rb +11 -4
  125. data/lib/hecks/fuzzing/properties/catalog.rb +6 -4
  126. data/lib/hecks/fuzzing/properties/guaranteed_by_construction.rb +18 -11
  127. data/lib/hecks/fuzzing/properties/guards.rb +1 -1
  128. data/lib/hecks/fuzzing/self_consistency/saga_redelivery.rb +1 -1
  129. data/lib/hecks/fuzzing/self_consistency.rb +6 -2
  130. data/lib/hecks/fuzzing/shrinker.rb +3 -1
  131. data/lib/hecks/fuzzing/value_generator.rb +19 -5
  132. data/lib/hecks/gate/stages.yml +4 -10
  133. data/lib/hecks/grammar/evolve.rb +1 -1
  134. data/lib/hecks/grammar/expression_operators.json +372 -0
  135. data/lib/hecks/grammar/operators.rb +3 -1
  136. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +10 -10
  137. data/lib/hecks/hecks/adapters/codebase/inspection/rubycritic.rb +35 -0
  138. data/lib/hecks/hecks/adapters/codebase/inspection.rb +104 -0
  139. data/lib/hecks/hecks/adapters/codebase/publishing.rb +2 -1
  140. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +12 -2
  141. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +2 -1
  142. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +1 -1
  143. data/lib/hecks/hecks/adapters/cost_explorer.rb +8 -5
  144. data/lib/hecks/hecks/adapters/deploy_toolchain/statuses.rb +2 -1
  145. data/lib/hecks/hecks/adapters/finding_github.rb +13 -6
  146. data/lib/hecks/hecks/adapters/first_admin.rb +2 -2
  147. data/lib/hecks/hecks/adapters/github_rulesets.rb +6 -3
  148. data/lib/hecks/hecks/adapters/in_process_boot.rb +1 -1
  149. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +1 -1
  150. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +10 -20
  151. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +28 -5
  152. data/lib/hecks/hecks/adapters/local_files.rb +2 -2
  153. data/lib/hecks/hecks/adapters/rehearsal/audit_report.rb +107 -0
  154. data/lib/hecks/hecks/adapters/rehearsal/census.rb +71 -0
  155. data/lib/hecks/hecks/adapters/rehearsal/declaration.rb +133 -0
  156. data/lib/hecks/hecks/adapters/rehearsal/environment.rb +83 -0
  157. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +142 -0
  158. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +96 -0
  159. data/lib/hecks/hecks/adapters/rehearsal/postgres_census.rb +100 -0
  160. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +69 -0
  161. data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +129 -0
  162. data/lib/hecks/hecks/adapters/rehearsal/rows.rb +41 -0
  163. data/lib/hecks/hecks/adapters/rehearsal/rust_audit.rb +83 -0
  164. data/lib/hecks/hecks/adapters/rehearsal/subject.rb +104 -0
  165. data/lib/hecks/hecks/adapters/rehearsal.rb +54 -0
  166. data/lib/hecks/hecks/adapters/rehearsal_store.adapter +7 -0
  167. data/lib/hecks/hecks/adapters/rehearsal_store.rb +81 -0
  168. data/lib/hecks/hecks/adapters/terminal.rb +13 -11
  169. data/lib/hecks/hecks/codebase.behaviors +13 -1
  170. data/lib/hecks/hecks/codebase.bluebook +239 -5
  171. data/lib/hecks/hecks/custodian.behaviors +24 -6
  172. data/lib/hecks/hecks/custodian.bluebook +6 -1227
  173. data/lib/hecks/hecks/deploy.behaviors +1 -1
  174. data/lib/hecks/hecks/deploy.world +1 -3
  175. data/lib/hecks/hecks/hecks.behaviors +1 -1
  176. data/lib/hecks/hecks/hecks.hecksagon +53 -18
  177. data/lib/hecks/hecks/hecks.world +10 -10
  178. data/lib/hecks/hecks/rehearsal.bluebook +477 -0
  179. data/lib/hecks/hecks/site.behaviors +1 -1
  180. data/lib/hecks/hecks/site.world +0 -1
  181. data/lib/hecks/hecks/tooling.bluebook +1238 -0
  182. data/lib/hecks/indifferent_key.rb +20 -0
  183. data/lib/hecks/language/adapter.bluebook +13 -3
  184. data/lib/hecks/language/bluebook/aggregate.bluebook +23 -0
  185. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -0
  186. data/lib/hecks/language/bluebook/entity.bluebook +18 -0
  187. data/lib/hecks/language/bluebook/policy.bluebook +9 -1
  188. data/lib/hecks/language/bluebook/query.bluebook +1 -1
  189. data/lib/hecks/language/bluebook/vocabulary.bluebook +11 -4
  190. data/lib/hecks/language/hecksagon/domain_port.bluebook +1 -1
  191. data/lib/hecks/language/hecksagon/hecksagon.bluebook +22 -0
  192. data/lib/hecks/language/oidc.json +10 -0
  193. data/lib/hecks/language/port.bluebook +4 -4
  194. data/lib/hecks/literal.rb +12 -3
  195. data/lib/hecks/ports/key_vault.rb +7 -2
  196. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +21 -10
  197. data/lib/hecks/ports/persistence/plugins/era/postgres_era/boot.rb +4 -2
  198. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +2 -1
  199. data/lib/hecks/ports/persistence/repository_factory.rb +6 -3
  200. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +29 -0
  201. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +1 -1
  202. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  203. data/lib/hecks/projections/deploy/shared.rb +5 -2
  204. data/lib/hecks/projections/model/deviations.rb +2 -2
  205. data/lib/hecks/projections/model.rb +2 -2
  206. data/lib/hecks/projections/oidc.rb +13 -1
  207. data/lib/hecks/projections/shape.rb +5 -2
  208. data/lib/hecks/projections/site/cms_editor/attributes.rb +26 -6
  209. data/lib/hecks/projections/site/cms_editor/blocks.rb +157 -0
  210. data/lib/hecks/projections/site/cms_editor/checks.rb +14 -1
  211. data/lib/hecks/projections/site/cms_editor/clearing.rb +70 -0
  212. data/lib/hecks/projections/site/cms_editor/color.rb +124 -0
  213. data/lib/hecks/projections/site/cms_editor/composition.rb +42 -0
  214. data/lib/hecks/projections/site/cms_editor/destructive.rb +61 -0
  215. data/lib/hecks/projections/site/cms_editor/drafts.rb +102 -0
  216. data/lib/hecks/projections/site/cms_editor/hints.rb +74 -0
  217. data/lib/hecks/projections/site/cms_editor/icons.rb +86 -0
  218. data/lib/hecks/projections/site/cms_editor/key_kinds.rb +31 -0
  219. data/lib/hecks/projections/site/cms_editor/limits.rb +72 -0
  220. data/lib/hecks/projections/site/cms_editor/media.rb +1 -1
  221. data/lib/hecks/projections/site/cms_editor/moment.rb +34 -0
  222. data/lib/hecks/projections/site/cms_editor/palette.rb +115 -0
  223. data/lib/hecks/projections/site/cms_editor/pickers.rb +85 -0
  224. data/lib/hecks/projections/site/cms_editor/preview.rb +91 -0
  225. data/lib/hecks/projections/site/cms_editor/relations.rb +92 -0
  226. data/lib/hecks/projections/site/cms_editor/scale_checks.rb +66 -0
  227. data/lib/hecks/projections/site/cms_editor/scheduling.rb +150 -0
  228. data/lib/hecks/projections/site/cms_editor/schema.rb +96 -19
  229. data/lib/hecks/projections/site/cms_editor/setting.rb +52 -9
  230. data/lib/hecks/projections/site/cms_editor/targets.rb +86 -0
  231. data/lib/hecks/projections/site/cms_editor/templates/package.json.tmpl +18 -1
  232. data/lib/hecks/projections/site/cms_editor/templates/src/app.ts.tmpl +251 -57
  233. data/lib/hecks/projections/site/cms_editor/templates/src/assets.ts.tmpl +98 -0
  234. data/lib/hecks/projections/site/cms_editor/templates/src/auth/membership.ts.tmpl +32 -24
  235. data/lib/hecks/projections/site/cms_editor/templates/src/browser/app.css.tmpl +412 -0
  236. data/lib/hecks/projections/site/cms_editor/templates/src/browser/autosave.js.tmpl +144 -0
  237. data/lib/hecks/projections/site/cms_editor/templates/src/browser/blocks.js.tmpl +500 -0
  238. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_doc.js.tmpl +175 -0
  239. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_editor.js.tmpl +418 -0
  240. data/lib/hecks/projections/site/cms_editor/templates/src/browser/epoch.js.tmpl +61 -0
  241. data/lib/hecks/projections/site/cms_editor/templates/src/browser/forms.js.tmpl +210 -0
  242. data/lib/hecks/projections/site/cms_editor/templates/src/browser/lists.js.tmpl +40 -0
  243. data/lib/hecks/projections/site/cms_editor/templates/src/browser/main.js.tmpl +26 -0
  244. data/lib/hecks/projections/site/cms_editor/templates/src/browser/media_picker.js.tmpl +232 -0
  245. data/lib/hecks/projections/site/cms_editor/templates/src/browser/moments.js.tmpl +73 -0
  246. data/lib/hecks/projections/site/cms_editor/templates/src/browser/pickers.js.tmpl +152 -0
  247. data/lib/hecks/projections/site/cms_editor/templates/src/browser/preview.js.tmpl +98 -0
  248. data/lib/hecks/projections/site/cms_editor/templates/src/browser/shell.js.tmpl +140 -0
  249. data/lib/hecks/projections/site/cms_editor/templates/src/browser/toast.js.tmpl +66 -0
  250. data/lib/hecks/projections/site/cms_editor/templates/src/choices.ts.tmpl +99 -0
  251. data/lib/hecks/projections/site/cms_editor/templates/src/commands.ts.tmpl +78 -24
  252. data/lib/hecks/projections/site/cms_editor/templates/src/config.ts.tmpl +6 -0
  253. data/lib/hecks/projections/site/cms_editor/templates/src/flash.ts.tmpl +52 -0
  254. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +19 -2
  255. data/lib/hecks/projections/site/cms_editor/templates/src/media/handler.ts.tmpl +31 -8
  256. data/lib/hecks/projections/site/cms_editor/templates/src/names.ts.tmpl +36 -0
  257. data/lib/hecks/projections/site/cms_editor/templates/src/schema.ts.tmpl +131 -2
  258. data/lib/hecks/projections/site/cms_editor/templates/src/ui/availability.ts.tmpl +15 -0
  259. data/lib/hecks/projections/site/cms_editor/templates/src/ui/block_list.js.tmpl +213 -0
  260. data/lib/hecks/projections/site/cms_editor/templates/src/ui/blocks.ts.tmpl +275 -0
  261. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_model.js.tmpl +8 -5
  262. data/lib/hecks/projections/site/cms_editor/templates/src/ui/detail.ts.tmpl +88 -0
  263. data/lib/hecks/projections/site/cms_editor/templates/src/ui/dialogs.ts.tmpl +43 -0
  264. data/lib/hecks/projections/site/cms_editor/templates/src/ui/fields.ts.tmpl +229 -34
  265. data/lib/hecks/projections/site/cms_editor/templates/src/ui/html.ts.tmpl +40 -81
  266. data/lib/hecks/projections/site/cms_editor/templates/src/ui/icons.ts.tmpl +15 -0
  267. data/lib/hecks/projections/site/cms_editor/templates/src/ui/input.ts.tmpl +29 -12
  268. data/lib/hecks/projections/site/cms_editor/templates/src/ui/links.ts.tmpl +100 -0
  269. data/lib/hecks/projections/site/cms_editor/templates/src/ui/moments.ts.tmpl +44 -0
  270. data/lib/hecks/projections/site/cms_editor/templates/src/ui/outline.ts.tmpl +53 -17
  271. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pages.ts.tmpl +178 -88
  272. data/lib/hecks/projections/site/cms_editor/templates/src/ui/paging.ts.tmpl +82 -0
  273. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pickers.ts.tmpl +110 -0
  274. data/lib/hecks/projections/site/cms_editor/templates/src/ui/preview.ts.tmpl +87 -0
  275. data/lib/hecks/projections/site/cms_editor/templates/src/ui/related.ts.tmpl +97 -0
  276. data/lib/hecks/projections/site/cms_editor/templates/src/ui/schedule.ts.tmpl +160 -0
  277. data/lib/hecks/projections/site/cms_editor/templates/src/ui/shell.ts.tmpl +211 -0
  278. data/lib/hecks/projections/site/cms_editor/templates/src/ui/table.ts.tmpl +151 -0
  279. data/lib/hecks/projections/site/cms_editor/templates/src/ui/theme.js.tmpl +16 -0
  280. data/lib/hecks/projections/site/cms_editor/templates/src/ui/values.ts.tmpl +69 -0
  281. data/lib/hecks/projections/site/cms_editor/templates/src/ui/words.ts.tmpl +97 -0
  282. data/lib/hecks/projections/site/cms_editor/templates/src/ui/world.ts.tmpl +35 -0
  283. data/lib/hecks/projections/site/cms_editor/templates/tsconfig.json.tmpl +2 -1
  284. data/lib/hecks/projections/site/cms_editor/theme.rb +29 -0
  285. data/lib/hecks/projections/site/cms_editor/theme_checks.rb +57 -0
  286. data/lib/hecks/projections/site/cms_editor.rb +49 -23
  287. data/lib/hecks/projections/site/role_probe.rb +2 -0
  288. data/lib/hecks/projector/cli_audience.rb +2 -1
  289. data/lib/hecks/projector/cli_projector/listing.rb +1 -1
  290. data/lib/hecks/projector/cli_projector/option_specs.rb +1 -1
  291. data/lib/hecks/projector/docs_projector/closing.rb +1 -1
  292. data/lib/hecks/projector/docs_projector.rb +1 -1
  293. data/lib/hecks/projector/exporter/bindings.rb +40 -10
  294. data/lib/hecks/projector/exporter/capability_values.rb +55 -0
  295. data/lib/hecks/projector/narrate_projector/reactions.rb +1 -1
  296. data/lib/hecks/projector/target.rb +11 -0
  297. data/lib/hecks/projector.rb +32 -5
  298. data/lib/hecks/quality_control/adapters/github_checks.rb +12 -8
  299. data/lib/hecks/quality_control/adapters/github_issues.rb +8 -4
  300. data/lib/hecks/quality_control/cli/qa_open_pr.rb +2 -2
  301. data/lib/hecks/quality_control/cli/qa_sweep/checks/shrinking.rb +1 -1
  302. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +1 -1
  303. data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
  304. data/lib/hecks/quality_control/quality_control.bluebook +6 -2
  305. data/lib/hecks/release/approval.rb +64 -0
  306. data/lib/hecks/router/{door.rb → aggregate_reader.rb} +5 -4
  307. data/lib/hecks/router/namespace_installer.rb +16 -16
  308. data/lib/hecks/router.rb +1 -1
  309. data/lib/hecks/runtime/best_effort.rb +22 -0
  310. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -2
  311. data/lib/hecks/runtime/command_interpreter.rb +3 -2
  312. data/lib/hecks/runtime/command_rules/emission.rb +1 -1
  313. data/lib/hecks/runtime/dispatcher.rb +4 -4
  314. data/lib/hecks/runtime/entity_element/location.rb +1 -1
  315. data/lib/hecks/runtime/event.rb +5 -0
  316. data/lib/hecks/runtime/identity.rb +7 -6
  317. data/lib/hecks/runtime/interpreting.rb +2 -1
  318. data/lib/hecks/runtime/invocation_builder.rb +1 -1
  319. data/lib/hecks/runtime/loader/boot_steps.rb +8 -5
  320. data/lib/hecks/runtime/loader.rb +10 -10
  321. data/lib/hecks/runtime/policy_interpreter/arguments.rb +7 -4
  322. data/lib/hecks/runtime/policy_interpreter/fan_out.rb +3 -3
  323. data/lib/hecks/runtime/policy_interpreter.rb +6 -6
  324. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -1
  325. data/lib/hecks/runtime/reaction_invocation.rb +1 -1
  326. data/lib/hecks/runtime/rebuild_sweep.rb +11 -1
  327. data/lib/hecks/runtime/registry/ask_verification.rb +35 -0
  328. data/lib/hecks/runtime/registry/hecksagon_rules.rb +10 -0
  329. data/lib/hecks/runtime/registry/merging.rb +1 -1
  330. data/lib/hecks/runtime/registry/providers.rb +16 -0
  331. data/lib/hecks/runtime/registry/read_repository.rb +6 -4
  332. data/lib/hecks/runtime/registry/verification.rb +4 -1
  333. data/lib/hecks/runtime/registry/world_defaults.rb +29 -0
  334. data/lib/hecks/runtime/registry.rb +1 -1
  335. data/lib/hecks/runtime/saga_interpreter/compensation.rb +1 -1
  336. data/lib/hecks/runtime/saga_interpreter/delivery.rb +4 -4
  337. data/lib/hecks/runtime/saga_interpreter/transitions.rb +1 -1
  338. data/lib/hecks/runtime/saga_interpreter.rb +3 -3
  339. data/lib/hecks/runtime/tenant_check.rb +5 -4
  340. data/lib/hecks/runtime/value/coercion.rb +2 -2
  341. data/lib/hecks/runtime/value/validation.rb +16 -4
  342. data/lib/hecks/runtime.rb +9 -9
  343. data/lib/hecks/rust_build/project_rust/target_ir.rb +1 -1
  344. data/lib/hecks/rust_build/wasm.rb +1 -1
  345. data/lib/hecks/rust_build/wasm_browser.rb +1 -1
  346. data/lib/hecks/rust_build.rb +11 -0
  347. data/lib/hecks/served_options.rb +53 -0
  348. data/lib/hecks/site/bluebook/site.bluebook +1 -0
  349. data/lib/hecks/storehouse/audit.rb +1 -1
  350. data/lib/hecks/storehouse/dispatching.rb +8 -6
  351. data/lib/hecks/storehouse/introspection.rb +2 -2
  352. data/lib/hecks/storehouse/querying.rb +6 -4
  353. data/lib/hecks/storehouse.rb +4 -4
  354. data/lib/hecks/tools/comment_style/source_file.rb +4 -1
  355. data/lib/hecks/tools/deploy_recipe_lint/fixtures.rb +4 -1
  356. data/lib/hecks/tools/lanes/rows.rb +6 -0
  357. data/lib/hecks/tools/oidc_manifests.rb +4 -5
  358. data/lib/hecks/tools/regeneration_run.rb +11 -4
  359. data/lib/hecks/tools/site_routes/editor_files.rb +12 -5
  360. data/lib/hecks/tools/tenant_provisioning.rb +2 -2
  361. data/lib/hecks/tools/tools_doc.rb +4 -4
  362. data/lib/hecks/version.rb +1 -1
  363. data/lib/hecks/vocabulary.rb +4 -4
  364. data/lib/hecks.rb +15 -15
  365. data/lib/rubocop/cop/hecks/env_dump.rb +69 -0
  366. data/lib/rubocop/cop/hecks/monotonic_duration.rb +53 -0
  367. data/lib/rubocop/cop/hecks/no_wall_clock_in_runtime.rb +34 -0
  368. data/lib/rubocop/cop/hecks/raw_shell_interpolation.rb +79 -0
  369. data/lib/rubocop/cop/hecks/sleep_in_spec.rb +34 -0
  370. data/lib/rubocop/cop/hecks/swallowed_rescue.rb +37 -0
  371. data/lib/rubocop/cop/hecks/symbol_string_key_mix.rb +50 -0
  372. data/lib/rubocop/cop/hecks/untyped_raise.rb +37 -0
  373. data/prebuilt/05c358d38d661a3774f813d49c66f5b68939af94432d66257a56f49ee2598994.marshal +0 -0
  374. data/prebuilt/26c40c61f09b09b11651c64668eda3ceb180f83cf2a3802d3033d2fd99ac841b.marshal +0 -0
  375. data/prebuilt/verdicts-230ac18c25abfd3d92ede032bee2661a4c2cd2773ab0cf0e2ae52546ba2f7af9.json +1 -0
  376. data/rust/.gitignore +6 -0
  377. data/rust/Cargo.toml +6 -0
  378. data/rust/build/src/binding_pass.rs +103 -0
  379. data/rust/build/src/cargo_sync.rs +6 -0
  380. data/rust/build/src/lineage_pass.rs +3 -3
  381. data/rust/build/src/main.rs +17 -5
  382. data/rust/build/src/pipeline.rs +48 -5
  383. data/rust/build/src/seams_pass.rs +123 -0
  384. data/rust/build/src/translation_pass.rs +167 -0
  385. data/rust/codegen/src/asks.rs +103 -0
  386. data/rust/codegen/src/commands.rs +13 -8
  387. data/rust/codegen/src/constraints.rs +25 -0
  388. data/rust/codegen/src/domain_generator.rs +2 -0
  389. data/rust/codegen/src/expr_emitter.rs +13 -0
  390. data/rust/codegen/src/json_codec.rs +54 -15
  391. data/rust/codegen/src/main.rs +1 -0
  392. data/rust/codegen/src/mutations.rs +128 -3
  393. data/rust/codegen/src/queries.rs +11 -2
  394. data/rust/codegen/src/reactions.rs +1 -1
  395. data/rust/codegen/src/types.rs +166 -8
  396. data/rust/host/HECKS_RELEASE +1 -1
  397. data/rust/host/src/api.rs +4 -4
  398. data/rust/host/src/bin/validate_harness.rs +43 -0
  399. data/rust/host/src/boundary_fuzz/expr_json.rs +2 -2
  400. data/rust/host/src/boundary_fuzz/ir.rs +1 -0
  401. data/rust/host/src/boundary_fuzz/server.rs +1 -1
  402. data/rust/host/src/checkout.rs +35 -19
  403. data/rust/host/src/commerce_ir.rs +408 -2
  404. data/rust/host/src/expr_json.rs +118 -0
  405. data/rust/host/src/mint.rs +1 -1
  406. data/rust/host/src/payments/tests.rs +27 -1
  407. data/rust/host/src/payments.rs +12 -4
  408. data/rust/host/src/query_step.rs +22 -4
  409. data/rust/host/src/reference_validate.rs +653 -108
  410. data/rust/host/src/resend.rs +2 -0
  411. data/rust/host/src/server.rs +19 -6
  412. data/rust/host/src/ui_schema.rs +132 -5
  413. data/rust/host/src/web/newsletter.rs +7 -8
  414. data/rust/host/src/web/newsletter_send.rs +38 -13
  415. data/rust/host/src/web/registrations.rs +229 -26
  416. data/rust/host/src/web.rs +3 -3
  417. data/rust/parser/src/emit.rs +38 -6
  418. data/rust/parser/src/expr/ast_json.rs +7 -1
  419. data/rust/parser/src/expr/resolver.rs +171 -6
  420. data/rust/parser/src/ir.rs +7 -0
  421. data/rust/parser/src/keywords.rs +24 -0
  422. data/rust/parser/src/main.rs +3 -0
  423. data/rust/parser/src/parse/aggregate.rs +2 -1
  424. data/rust/parser/src/parse/chapter.rs +2 -2
  425. data/rust/parser/src/parse/command.rs +2 -0
  426. data/rust/parser/src/parse/hecksagon.rs +5 -2
  427. data/rust/parser/src/parse/lifecycle.rs +81 -0
  428. data/rust/parser/src/parse/mod.rs +15 -1
  429. data/rust/parser/src/parse/policy.rs +13 -2
  430. data/rust/parser/src/parse/value_object.rs +13 -8
  431. data/rust/parser/src/ruby_value.rs +1 -1
  432. data/rust/src/exemplar/commands.rs +4 -4
  433. data/rust/src/exemplar/constraints.rs +1 -1
  434. data/rust/src/exemplar/json.rs +43 -1
  435. data/rust/src/kernel/cli.rs +7 -5
  436. data/rust/src/kernel/dispatch.rs +2 -2
  437. data/rust/src/kernel/dispatch_guard_tests.rs +1 -1
  438. data/rust/src/kernel/expr.rs +66 -3
  439. data/rust/src/kernel/expression_operators/arithmetic.rs +130 -5
  440. data/rust/src/kernel/expression_operators/text.rs +26 -2
  441. data/rust/src/kernel/json.rs +15 -0
  442. data/rust/src/kernel/mod.rs +1 -1
  443. data/rust/src/kernel/orchestrate.rs +54 -5
  444. data/rust/src/kernel/orchestrate_guard_tests.rs +67 -0
  445. data/rust/src/kernel/pattern.rs +60 -0
  446. data/rust/src/kernel/reference_lookup.rs +90 -7
  447. metadata +140 -38
  448. data/lib/hecks/doors/cli_door/nesting.rb +0 -34
  449. data/lib/hecks/doors/cli_door/shorthand.rb +0 -85
  450. data/lib/hecks/doors/cli_door.rb +0 -124
  451. data/lib/hecks/doors/cli_runner/answers.rb +0 -138
  452. data/lib/hecks/doors/cli_runner/resolution.rb +0 -146
  453. data/lib/hecks/doors/cli_runner/settling.rb +0 -115
  454. data/lib/hecks/doors/cli_runner/streaming.rb +0 -90
  455. data/lib/hecks/doors/cli_runner/suggestions.rb +0 -43
  456. data/lib/hecks/doors/cli_runner.rb +0 -66
  457. data/lib/hecks/doors/command_request.rb +0 -119
  458. data/lib/hecks/doors/handle.rb +0 -208
  459. data/lib/hecks/doors/json_door.rb +0 -132
  460. data/lib/hecks/doors/launcher_options.rb +0 -247
  461. data/lib/hecks/doors/mcp_door/properties.rb +0 -47
  462. data/lib/hecks/doors/mcp_door/tool_calls.rb +0 -113
  463. data/lib/hecks/doors/mcp_door/tool_definitions/auditing.rb +0 -51
  464. data/lib/hecks/doors/mcp_door/tool_definitions/calling.rb +0 -69
  465. data/lib/hecks/doors/mcp_door/tool_definitions/reading.rb +0 -97
  466. data/lib/hecks/doors/mcp_door/tool_definitions.rb +0 -15
  467. data/lib/hecks/doors/mcp_door.rb +0 -156
  468. data/lib/hecks/doors/mcp_door_scope/argument_policy.rb +0 -92
  469. data/lib/hecks/doors/mcp_door_scope/env_problems.rb +0 -55
  470. data/lib/hecks/doors/mcp_door_scope/presenter.rb +0 -78
  471. data/lib/hecks/doors/mcp_door_scope.rb +0 -201
  472. data/lib/hecks/doors/mcp_guide_cache.rb +0 -75
  473. data/lib/hecks/doors/ruby_door/aggregate_door.rb +0 -162
  474. data/lib/hecks/doors/ruby_door/chapter.rb +0 -89
  475. data/lib/hecks/doors/ruby_door.rb +0 -80
  476. data/lib/hecks/doors/usage_cache.rb +0 -139
  477. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_widget.js.tmpl +0 -282
  478. data/lib/hecks/projections/site/cms_editor/templates/src/ui/media_picker.js.tmpl +0 -109
  479. data/prebuilt/c28d6d9db14eea3e17161e9d9c262efee30b256cf97693a7cfd552bce24b2f7f.marshal +0 -0
  480. data/prebuilt/cb375cba61ca0dbe8ddb9739c306510fe05e5f4b7a9b6955caff8224c63ea0e0.marshal +0 -0
  481. data/prebuilt/verdicts-d6d584ad43b5f3cebf88aafbbdbf024c38574acf18e06adffbc555f7786b1c2f.json +0 -1
@@ -0,0 +1,152 @@
1
+ __BANNER__
2
+
3
+ // What a script does with pickers (see ui/pickers.ts). A text box that suggests keys is checked as it
4
+ // is typed, so the form cannot be sent with a key that is not on offer. A `<kind>:<key>` value is
5
+ // built from a kind and a record, two controls that write the one input the form posts. The server
6
+ // checks every key again, whatever the script did.
7
+
8
+ /** How many choices a native select holds before a text box with suggestions takes over (as in the page). */
9
+ const SELECT_MAX = 30;
10
+
11
+ /** The keys a text box's list offers. */
12
+ const offered = (input) => [...(document.getElementById(input.getAttribute("list"))?.options ?? [])].map((option) => option.value);
13
+
14
+ /** Whether `input` names something on offer, or nothing, or what it named when the page was made. */
15
+ function valid(input) {
16
+ if (input.dataset.open !== undefined || input.value === "" || input.value === input.dataset.initial) return true;
17
+ return offered(input).includes(input.value);
18
+ }
19
+
20
+ /** Marks a strict box invalid, with the words the browser shows, until it names something on offer. */
21
+ function check(input) {
22
+ input.setCustomValidity(valid(input) ? "" : `Nothing is called "${input.value}". Choose one of the suggestions.`);
23
+ }
24
+
25
+ const words = (option) => (option.label && option.label !== option.key ? `${option.label} (${option.key})` : option.key);
26
+
27
+ let counter = 0;
28
+
29
+ /** A text box for a record, with its list of suggestions when there are some. */
30
+ function suggesting(options, strict, value) {
31
+ const input = document.createElement("input");
32
+ input.type = "text";
33
+ input.className = "input w-full";
34
+ input.setAttribute("aria-label", "Record");
35
+ input.autocomplete = "off";
36
+ input.value = value;
37
+ const holder = document.createElement("span");
38
+ holder.className = "flex min-w-0 flex-1";
39
+ holder.append(input);
40
+ if (options.length > 0) {
41
+ const list = document.createElement("datalist");
42
+ list.id = `kind-choices-${(counter += 1)}`;
43
+ for (const option of options) list.append(Object.assign(document.createElement("option"), { value: option.key, label: words(option) }));
44
+ input.setAttribute("list", list.id);
45
+ holder.append(list);
46
+ }
47
+ if (strict && options.length > 0) {
48
+ input.dataset.strict = "";
49
+ input.dataset.initial = value;
50
+ check(input);
51
+ }
52
+ return holder;
53
+ }
54
+
55
+ /** A native select of a kind's records. */
56
+ function choosing(options, value) {
57
+ const select = document.createElement("select");
58
+ select.className = "select min-w-0 flex-1";
59
+ select.setAttribute("aria-label", "Record");
60
+ select.append(new Option("Choose...", ""));
61
+ for (const option of options) select.append(new Option(words(option), option.key, false, option.key === value));
62
+ if (value !== "" && !options.some((option) => option.key === value)) select.append(new Option(`${value} (not on offer)`, value, false, true));
63
+ return select;
64
+ }
65
+
66
+ /** The record's control for a kind: a select for a few records, a box that suggests for more, a plain box for none. */
67
+ function control(kind, value) {
68
+ const options = kind?.options ?? [];
69
+ return kind?.strict && options.length > 0 && options.length <= SELECT_MAX ? choosing(options, value) : suggesting(options, Boolean(kind?.strict), value);
70
+ }
71
+
72
+ const fieldOf = (element) => (element.matches("input, select") ? element : element.querySelector("input"));
73
+
74
+ /** The `<kind>:<key>` the pair of controls stand for, or empty when either is. */
75
+ function sync(box) {
76
+ const kind = box.querySelector("[data-kind]");
77
+ const record = fieldOf(box.querySelector("[data-record]"));
78
+ box.querySelector("input[type=hidden]").value = kind.value !== "" && record.value !== "" ? `${kind.value}:${record.value}` : "";
79
+ }
80
+
81
+ /** Puts a record control for the chosen kind in place of the one there. */
82
+ function place(box, value) {
83
+ box.querySelector("[data-record]")?.remove();
84
+ const kinds = JSON.parse(box.dataset.kinds);
85
+ const element = control(kinds.find((kind) => kind.kind === box.querySelector("[data-kind]").value), value);
86
+ element.dataset.record = "";
87
+ box.querySelector("[data-pair]").append(element);
88
+ }
89
+
90
+ /** Builds the kind and record controls of one `<kind>:<key>` input. */
91
+ function mount(box) {
92
+ if (box.dataset.mounted) return;
93
+ const kinds = JSON.parse(box.dataset.kinds);
94
+ const input = box.querySelector("input");
95
+ const [first, ...rest] = input.value.split(":");
96
+ const known = kinds.some((kind) => kind.kind === first);
97
+ const kind = document.createElement("select");
98
+ kind.className = "select";
99
+ kind.id = input.id;
100
+ kind.dataset.kind = "";
101
+ kind.setAttribute("aria-label", "Kind");
102
+ kind.append(new Option("Kind...", ""));
103
+ for (const each of kinds) kind.append(new Option(each.kind, each.kind, false, known && each.kind === first));
104
+ const pair = document.createElement("div");
105
+ pair.className = "flex flex-wrap gap-2";
106
+ pair.dataset.pair = "";
107
+ pair.append(kind);
108
+ input.type = "hidden";
109
+ input.removeAttribute("id");
110
+ box.append(pair);
111
+ box.dataset.mounted = "true";
112
+ place(box, known ? rest.join(":") : "");
113
+ sync(box);
114
+ }
115
+
116
+ /** Mounts every `<kind>:<key>` input under `root` that has not been; a copied row is first undone so it can be. */
117
+ export function mountPickers(root = document) {
118
+ for (const box of root.querySelectorAll("[data-key-picker]")) mount(box);
119
+ }
120
+
121
+ /** Undoes a copied `<kind>:<key>` row's controls, so the row can be mounted as a new one. */
122
+ export function unmountPickers(root) {
123
+ for (const box of root.querySelectorAll("[data-key-picker][data-mounted]")) {
124
+ const hidden = box.querySelector("input[type=hidden]");
125
+ const id = box.querySelector("select[data-kind]")?.id;
126
+ box.querySelector("[data-pair]")?.remove();
127
+ hidden.type = "text";
128
+ hidden.value = "";
129
+ if (id) hidden.id = id;
130
+ delete box.dataset.mounted;
131
+ }
132
+ }
133
+
134
+ export function enhancePickers() {
135
+ for (const input of document.querySelectorAll("input[data-strict]")) {
136
+ input.dataset.initial = input.value;
137
+ check(input);
138
+ }
139
+ document.addEventListener("input", (event) => {
140
+ const target = event.target;
141
+ if (target.matches?.("input[data-strict]")) check(target);
142
+ const box = target.closest?.("[data-key-picker]");
143
+ if (box && !target.matches("[data-kind]")) sync(box);
144
+ });
145
+ document.addEventListener("change", (event) => {
146
+ const box = event.target.closest?.("[data-key-picker]");
147
+ if (!box) return;
148
+ if (event.target.matches("[data-kind]")) place(box, "");
149
+ sync(box);
150
+ });
151
+ mountPickers();
152
+ }
@@ -0,0 +1,98 @@
1
+ __BANNER__
2
+
3
+ // The preview drawer (see ui/preview.ts): the site's own page for a record, framed beside the editor.
4
+ // The button is a plain link to the address (it opens a new tab with no script); here it opens the
5
+ // drawer instead and loads the frame, which is not loaded until then. The width buttons only change
6
+ // the frame's width. A successful draft save (browser/autosave.js says so with `editor:saved`) reloads
7
+ // the frame, and a drawer that was open when a form was sent is open again on the page that follows, so
8
+ // a save or a publish is seen as the site shows it. Nothing is sent into the frame.
9
+
10
+ const SIZES = { phone: "w-[375px]", tablet: "w-[768px]", desktop: "w-full" };
11
+ const ALL = Object.values(SIZES);
12
+ const KEY = "editor-preview";
13
+
14
+ /** The path the drawer was open on, which survives one page change in this tab. */
15
+ function remembered() {
16
+ try {
17
+ return sessionStorage.getItem(KEY);
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ function remember(path) {
24
+ try {
25
+ if (path) sessionStorage.setItem(KEY, path);
26
+ else sessionStorage.removeItem(KEY);
27
+ } catch {
28
+ // Storage may be blocked; the drawer then simply starts closed on the next page.
29
+ }
30
+ }
31
+
32
+ /** Moves focus to `element`, trying again for a moment while the drawer is still becoming visible (a hidden element takes none). */
33
+ function focusInto(element, tries = 20) {
34
+ element.focus();
35
+ if (tries > 0 && document.activeElement !== element) setTimeout(() => focusInto(element, tries - 1), 30);
36
+ }
37
+
38
+ const FOCUSABLE = "a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex='-1'])";
39
+
40
+ export function enhancePreview() {
41
+ const root = document.querySelector("[data-preview]");
42
+ if (!root) return;
43
+ const toggle = root.querySelector(".drawer-toggle");
44
+ const frame = root.querySelector("[data-preview-frame]");
45
+ const opener = document.querySelector("[data-preview-open]");
46
+ const load = () => {
47
+ frame.src = frame.dataset.src;
48
+ };
49
+ const show = () => {
50
+ toggle.checked = true;
51
+ if (!frame.getAttribute("src")) load();
52
+ remember(location.pathname);
53
+ focusInto(root.querySelector("[data-preview-close]"));
54
+ };
55
+ const hide = () => {
56
+ toggle.checked = false;
57
+ remember(null);
58
+ opener?.focus();
59
+ };
60
+ opener?.addEventListener("click", (event) => {
61
+ event.preventDefault();
62
+ show();
63
+ });
64
+ toggle.addEventListener("change", () => remember(toggle.checked ? location.pathname : null));
65
+ const section = root.querySelector("[role='dialog']");
66
+ document.addEventListener("keydown", (event) => {
67
+ if (!toggle.checked) return;
68
+ if (event.key === "Escape") return hide();
69
+ if (event.key !== "Tab") return;
70
+ // The drawer is modal: Tab and Shift+Tab go round its own controls, not on to the page behind it.
71
+ const items = [...section.querySelectorAll(FOCUSABLE)].filter((item) => item.getClientRects().length > 0);
72
+ const first = items[0];
73
+ const last = items.at(-1);
74
+ if (!section.contains(document.activeElement)) {
75
+ event.preventDefault();
76
+ first?.focus();
77
+ } else if (event.shiftKey && document.activeElement === first) {
78
+ event.preventDefault();
79
+ last.focus();
80
+ } else if (!event.shiftKey && document.activeElement === last) {
81
+ event.preventDefault();
82
+ first.focus();
83
+ }
84
+ });
85
+ root.querySelector("[data-preview-close]").addEventListener("click", hide);
86
+ for (const button of root.querySelectorAll("[data-preview-width]")) {
87
+ button.addEventListener("click", () => {
88
+ frame.classList.remove(...ALL);
89
+ frame.classList.add(SIZES[button.dataset.previewWidth]);
90
+ for (const other of root.querySelectorAll("[data-preview-width]")) other.setAttribute("aria-pressed", String(other === button));
91
+ });
92
+ }
93
+ root.querySelector("[data-preview-refresh]").addEventListener("click", load);
94
+ document.addEventListener("editor:saved", () => {
95
+ if (toggle.checked) load();
96
+ });
97
+ if (remembered() === location.pathname) show();
98
+ }
@@ -0,0 +1,140 @@
1
+ __BANNER__
2
+
3
+ // What the frame and the lists do with a script: the menu button, the theme toggle, the copy
4
+ // buttons, and a list's filter and sort. Every page works without any of it.
5
+ import { showToast } from "./toast.js";
6
+
7
+ const icon = (name) => `<svg class="icon" aria-hidden="true" focusable="false"><use href="#i-${name}"/></svg>`;
8
+
9
+ /** The drawer: a checkbox is the state, so the page needs no script to open it; this adds the button, Escape and focus. */
10
+ function navigation() {
11
+ const toggle = document.getElementById("nav-toggle");
12
+ const button = document.querySelector("[data-nav-toggle]");
13
+ if (!toggle || !button) return;
14
+ const set = (open) => {
15
+ toggle.checked = open;
16
+ button.setAttribute("aria-expanded", String(open));
17
+ button.setAttribute("aria-label", open ? "Close the menu" : "Open the menu");
18
+ if (open) document.querySelector("#rail a")?.focus();
19
+ };
20
+ button.addEventListener("click", () => set(!toggle.checked));
21
+ document.addEventListener("keydown", (event) => {
22
+ if (event.key === "Escape" && toggle.checked) {
23
+ set(false);
24
+ button.focus();
25
+ }
26
+ });
27
+ document.querySelector(".drawer-overlay")?.addEventListener("click", () => set(false));
28
+ }
29
+
30
+ const dark = () => {
31
+ const named = document.documentElement.getAttribute("data-theme");
32
+ return named ? named === "editor-dark" : window.matchMedia("(prefers-color-scheme: dark)").matches;
33
+ };
34
+
35
+ /** The theme button: switches between light and dark, and remembers the choice where storage allows. */
36
+ function theme() {
37
+ const button = document.querySelector("[data-theme-toggle]");
38
+ if (!button) return;
39
+ const show = () => {
40
+ button.innerHTML = icon(dark() ? "sun" : "moon");
41
+ button.setAttribute("aria-label", dark() ? "Switch to the light theme" : "Switch to the dark theme");
42
+ button.title = button.getAttribute("aria-label");
43
+ };
44
+ button.addEventListener("click", () => {
45
+ const next = dark() ? "light" : "dark";
46
+ document.documentElement.setAttribute("data-theme", `editor-${next}`);
47
+ try {
48
+ window.localStorage.setItem("editor-theme", next);
49
+ } catch (error) {
50
+ /* storage is blocked: the choice lasts for this page only */
51
+ }
52
+ show();
53
+ });
54
+ window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", show);
55
+ show();
56
+ }
57
+
58
+ /** Copy buttons: shown only when a script can make them work. */
59
+ function copying() {
60
+ for (const button of document.querySelectorAll("[data-copy]")) {
61
+ button.hidden = false;
62
+ button.addEventListener("click", async () => {
63
+ try {
64
+ await navigator.clipboard.writeText(button.dataset.copy);
65
+ showToast("success", "Copied.", 2500);
66
+ } catch (error) {
67
+ showToast("error", "The browser did not allow copying. Select the text and copy it.");
68
+ }
69
+ });
70
+ }
71
+ }
72
+
73
+ const cellOf = (row, at) => row.cells[at];
74
+ const valueOf = (cell) => cell?.dataset.value ?? cell?.textContent ?? "";
75
+
76
+ /** A list's sort: each heading becomes a button that orders the rows by its column, and says which way. */
77
+ function sorting(table) {
78
+ const body = table.tBodies[0];
79
+ const heads = [...table.tHead.rows[0].cells];
80
+ heads.forEach((head, at) => {
81
+ const label = head.textContent;
82
+ const button = document.createElement("button");
83
+ button.type = "button";
84
+ button.className = "inline-flex items-center gap-1 font-semibold";
85
+ button.innerHTML = `<span>${label}</span><span class="opacity-60">${icon("sort")}</span>`;
86
+ head.replaceChildren(button);
87
+ button.addEventListener("click", () => {
88
+ const ascending = head.getAttribute("aria-sort") !== "ascending";
89
+ const numbers = head.dataset.sort === "number";
90
+ const rows = [...body.rows].sort((a, b) => {
91
+ const left = valueOf(cellOf(a, at));
92
+ const right = valueOf(cellOf(b, at));
93
+ const order = numbers ? Number(left) - Number(right) : left.localeCompare(right, undefined, { numeric: true, sensitivity: "base" });
94
+ return ascending ? order : -order;
95
+ });
96
+ body.append(...rows);
97
+ heads.forEach((other) => other.removeAttribute("aria-sort"));
98
+ head.setAttribute("aria-sort", ascending ? "ascending" : "descending");
99
+ button.lastChild.innerHTML = icon(ascending ? "chevron-up" : "chevron-down");
100
+ });
101
+ });
102
+ }
103
+
104
+ /**
105
+ * A list's search box, as it is typed: hides the rows of this page that do not hold the words, and
106
+ * says so in the count. Sending the form (Enter) searches the whole list on the server.
107
+ */
108
+ function filtering(input) {
109
+ const table = document.querySelector("table[data-sortable]");
110
+ const count = document.querySelector("[data-count]");
111
+ if (!table || !count) return;
112
+ const rows = [...table.tBodies[0].rows];
113
+ input.addEventListener("input", () => {
114
+ const wanted = input.value.trim().toLowerCase();
115
+ let shown = 0;
116
+ for (const row of rows) {
117
+ const match = wanted === "" || row.textContent.toLowerCase().includes(wanted);
118
+ row.hidden = !match;
119
+ if (match) shown += 1;
120
+ }
121
+ count.textContent = wanted === "" ? count.dataset.text : `${shown} of ${rows.length} on this page`;
122
+ });
123
+ }
124
+
125
+ /** After a command ran, the part of the page it changed settles into view with a short highlight. */
126
+ function highlight() {
127
+ if (!document.querySelector('[data-toast][role="status"]')) return;
128
+ for (const changed of document.querySelectorAll("aside .badge")) changed.parentElement?.classList.add("just-changed");
129
+ }
130
+
131
+ export function enhanceShell() {
132
+ navigation();
133
+ theme();
134
+ copying();
135
+ highlight();
136
+ const table = document.querySelector("table[data-sortable]");
137
+ if (table) sorting(table);
138
+ const filter = document.querySelector("[data-filter]");
139
+ if (filter) filtering(filter);
140
+ }
@@ -0,0 +1,66 @@
1
+ __BANNER__
2
+
3
+ // Notices. The server renders the one a redirect carried ("Published.") into the page; this adds a
4
+ // close button and lets it go after a few seconds, and shows others the page's own script raises
5
+ // ("Copied."). A notice that reports an error stays until it is closed, and is announced at once
6
+ // (`role="alert"`); the rest wait their turn (`role="status"`). The classes are written whole so the
7
+ // stylesheet build finds them.
8
+ const KINDS = {
9
+ success: ["alert alert-success alert-soft", "success", "status"],
10
+ error: ["alert alert-error alert-soft", "alert", "alert"],
11
+ info: ["alert alert-info alert-soft", "info", "status"],
12
+ };
13
+
14
+ const LIFETIME = 6000;
15
+
16
+ const svg = (name) => `<svg class="icon" aria-hidden="true" focusable="false"><use href="#i-${name}"/></svg>`;
17
+
18
+ function container() {
19
+ let found = document.querySelector("[data-toasts]");
20
+ if (!found) {
21
+ found = document.createElement("div");
22
+ found.className = "toast toast-top toast-center z-50 mt-14";
23
+ found.dataset.toasts = "";
24
+ document.body.append(found);
25
+ }
26
+ return found;
27
+ }
28
+
29
+ /** Closes a notice. */
30
+ function dismiss(notice) {
31
+ notice.remove();
32
+ }
33
+
34
+ /** Gives a notice a close button and a lifetime. */
35
+ function adopt(notice, lifetime) {
36
+ if (notice.querySelector("[data-close]")) return;
37
+ const close = document.createElement("button");
38
+ close.type = "button";
39
+ close.className = "btn btn-ghost btn-square btn-xs";
40
+ close.setAttribute("aria-label", "Dismiss");
41
+ close.dataset.close = "";
42
+ close.innerHTML = svg("close");
43
+ close.addEventListener("click", () => dismiss(notice));
44
+ notice.append(close);
45
+ if (lifetime > 0) setTimeout(() => dismiss(notice), lifetime);
46
+ }
47
+
48
+ /** Shows a notice: `kind` is `success`, `error` or `info`; `message` is shown as text, never as markup. */
49
+ export function showToast(kind, message, lifetime) {
50
+ const [classes, mark, role] = KINDS[kind] ?? KINDS.info;
51
+ const notice = document.createElement("div");
52
+ notice.className = classes;
53
+ notice.setAttribute("role", role);
54
+ notice.dataset.toast = "";
55
+ notice.innerHTML = svg(mark);
56
+ const text = document.createElement("p");
57
+ text.textContent = message;
58
+ notice.append(text);
59
+ container().append(notice);
60
+ adopt(notice, lifetime ?? (kind === "error" ? 0 : LIFETIME));
61
+ }
62
+
63
+ /** Gives the notices the server rendered a close button and a lifetime. */
64
+ export function adoptToasts() {
65
+ for (const notice of document.querySelectorAll("[data-toast]")) adopt(notice, notice.getAttribute("role") === "alert" ? 0 : LIFETIME);
66
+ }
@@ -0,0 +1,99 @@
1
+ __BANNER__
2
+
3
+ // What a picker offers, and whether what was chosen is there. The offer is the target's own listing
4
+ // query (or every instance when it declares none), read from the host as any other page is, and a
5
+ // target the host cannot answer for is left without one, so its input stays a text box. A key is
6
+ // checked against every instance the target holds, not only those on offer, so a retired record a
7
+ // form already names is still a record that exists. Nothing here asks the domain to know about
8
+ // pickers: the host answers the queries it already has.
9
+ import { DomainRefusal, DomainUnavailable } from "@hecks/client";
10
+ import type { HostClient } from "@hecks/client";
11
+
12
+ import { leaf } from "./commands.ts";
13
+ import { rowsAnswered } from "./host.ts";
14
+ import type { State } from "./commands.ts";
15
+ import type { Aggregate, Attr, Picker } from "./schema.ts";
16
+ import { summaryText } from "./ui/outline.ts";
17
+ import { OFFER_MAX, choiceText, targetKey, targetsOf } from "./ui/pickers.ts";
18
+ import type { Choice, Choices, Target } from "./ui/pickers.ts";
19
+ import { singular } from "./ui/words.ts";
20
+
21
+ /** The words that name an instance: its label attribute when it has one that says more than the key. */
22
+ function choiceOf(target: Target, row: State): Choice | null {
23
+ const key = leaf(row[target.key ?? ""]);
24
+ if (key === null) return null;
25
+ const words = target.label && target.label !== target.key ? summaryText(row[target.label]) : "";
26
+ return { key, label: words === "" ? key : words };
27
+ }
28
+
29
+ /** What `target` offers, or null when the host could not say. */
30
+ async function offered(client: HostClient, target: Target): Promise<Choice[] | null> {
31
+ const name = targetKey(target);
32
+ try {
33
+ const rows = target.listing
34
+ ? rowsAnswered(await client.query(`${name}.${target.listing}`, {}), `${name}.${target.listing}`)
35
+ : client.instancesOf(await client.read(), name).map(([, state]) => state);
36
+ const found = rows.flatMap((row) => choiceOf(target, row) ?? []);
37
+ return found.sort((a, b) => choiceText(a).localeCompare(choiceText(b), undefined, { sensitivity: "base", numeric: true })).slice(0, OFFER_MAX);
38
+ } catch (err) {
39
+ if (err instanceof DomainRefusal || err instanceof DomainUnavailable) return null;
40
+ throw err;
41
+ }
42
+ }
43
+
44
+ /** What each picker among `attrs` offers, by target; a target the host could not answer for is absent. */
45
+ export async function choicesFor(client: HostClient, agg: Aggregate, attrs: Attr[]): Promise<Choices> {
46
+ const targets = targetsOf(agg, attrs);
47
+ const found = await Promise.all(targets.map(async (target) => [targetKey(target), await offered(client, target)] as const));
48
+ return Object.fromEntries(found.filter((entry): entry is readonly [string, Choice[]] => entry[1] !== null));
49
+ }
50
+
51
+ /** A key that is not there, and the attribute of the form to mark. */
52
+ export interface Unknown {
53
+ field: string;
54
+ message: string;
55
+ }
56
+
57
+ /** What `picker` makes of `key`: the target it must be an instance of, or a problem to say. */
58
+ function resolve(picker: Picker, key: string): { target: Target; key: string } | string | null {
59
+ if (!picker.kinds) return picker.strict ? { target: picker, key } : null;
60
+ const colon = key.indexOf(":");
61
+ const kinds = picker.kinds.map((kind) => kind.kind);
62
+ const kind = picker.kinds.find((candidate) => candidate.kind === key.slice(0, Math.max(colon, 0)));
63
+ if (!kind) return `Start with one of ${kinds.join(", ")} and a colon, as in ${kinds[0]}:key.`;
64
+ return kind.target && kind.strict ? { target: kind, key: key.slice(colon + 1) } : null;
65
+ }
66
+
67
+ /** The keys the host holds for `target`, read once. */
68
+ function holder(client: HostClient): (target: Target) => Promise<Set<string>> {
69
+ const answer = client.read();
70
+ return async (target) => new Set(client.instancesOf(await answer, targetKey(target)).map(([id]) => id));
71
+ }
72
+
73
+ /** Every key a form names that a strict picker's target does not hold, the first one found; null when all are there. */
74
+ export async function unknownKey(client: HostClient, agg: Aggregate, attrs: Attr[], args: State): Promise<Unknown | null> {
75
+ const wanted: { field: string; target: Target; key: string }[] = [];
76
+ const problems: Unknown[] = [];
77
+ const walk = (list: Attr[], values: State, top?: string): void => {
78
+ for (const attr of list) {
79
+ const field = top ?? attr.name;
80
+ for (const item of [values[attr.name]].flat()) {
81
+ const key = leaf(item);
82
+ if (attr.options && key !== null && key !== "") {
83
+ if (!attr.options.includes(key)) problems.push({ field, message: `Choose one of ${attr.options.join(", ")}.` });
84
+ } else if (attr.picker && key !== null && key !== "") {
85
+ const next = resolve(attr.picker, key);
86
+ if (typeof next === "string") problems.push({ field, message: next });
87
+ else if (next) wanted.push({ field, ...next });
88
+ } else if (attr.kind === "object" && item && typeof item === "object") walk(agg.valueObjects[attr.type] ?? [], item as State, field);
89
+ }
90
+ }
91
+ };
92
+ walk(attrs, args);
93
+ if (problems.length > 0) return problems[0];
94
+ const held = wanted.length > 0 ? holder(client) : null;
95
+ for (const { field, target, key } of wanted) {
96
+ if (!(await held?.(target))?.has(key)) return { field, message: `No ${singular(target.target ?? "record")} has the key "${key}".` };
97
+ }
98
+ return null;
99
+ }