hecks 3.9.1 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (504) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/bluebook/pizzas.bluebook +29 -0
  3. data/examples/pizzas/bluebook/pizzas.hecksagon +7 -0
  4. data/exe/hecks +4 -4
  5. data/lib/hecks/adapters/driven/d1.rb +2 -10
  6. data/lib/hecks/adapters/driven/heki.adapter +1 -1
  7. data/lib/hecks/adapters/driven/heki.rb +2 -12
  8. data/lib/hecks/adapters/driven/lambda.adapter +1 -1
  9. data/lib/hecks/adapters/driven/lambda.rb +3 -18
  10. data/lib/hecks/adapters/driven/postgres.rb +4 -11
  11. data/lib/hecks/adapters/driven/sqlite.adapter +1 -1
  12. data/lib/hecks/adapters/driven/sqlite.rb +2 -10
  13. data/lib/hecks/adapters/driving/admission.rb +46 -0
  14. data/lib/hecks/adapters/driving/cli/nesting.rb +36 -0
  15. data/lib/hecks/adapters/driving/cli/shorthand.rb +88 -0
  16. data/lib/hecks/adapters/driving/cli.rb +127 -0
  17. data/lib/hecks/adapters/driving/cli_runner/answers.rb +160 -0
  18. data/lib/hecks/adapters/driving/cli_runner/resolution.rb +155 -0
  19. data/lib/hecks/adapters/driving/cli_runner/settling.rb +120 -0
  20. data/lib/hecks/adapters/driving/cli_runner/streaming.rb +93 -0
  21. data/lib/hecks/adapters/driving/cli_runner/suggestions.rb +47 -0
  22. data/lib/hecks/adapters/driving/cli_runner.rb +69 -0
  23. data/lib/hecks/adapters/driving/command_request.rb +121 -0
  24. data/lib/hecks/adapters/driving/handle.rb +214 -0
  25. data/lib/hecks/adapters/driving/json.rb +136 -0
  26. data/lib/hecks/adapters/driving/launcher_options.rb +267 -0
  27. data/lib/hecks/adapters/driving/mcp/properties.rb +49 -0
  28. data/lib/hecks/adapters/driving/mcp/tool_calls.rb +116 -0
  29. data/lib/hecks/adapters/driving/mcp/tool_definitions/auditing.rb +53 -0
  30. data/lib/hecks/adapters/driving/mcp/tool_definitions/calling.rb +71 -0
  31. data/lib/hecks/adapters/driving/mcp/tool_definitions/reading.rb +99 -0
  32. data/lib/hecks/adapters/driving/mcp/tool_definitions.rb +18 -0
  33. data/lib/hecks/adapters/driving/mcp.rb +159 -0
  34. data/lib/hecks/adapters/driving/mcp_guide_cache.rb +78 -0
  35. data/lib/hecks/adapters/driving/mcp_scope/argument_policy.rb +97 -0
  36. data/lib/hecks/adapters/driving/mcp_scope/env_problems.rb +69 -0
  37. data/lib/hecks/adapters/driving/mcp_scope/presenter.rb +83 -0
  38. data/lib/hecks/adapters/driving/mcp_scope.rb +213 -0
  39. data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +171 -0
  40. data/lib/hecks/adapters/driving/ruby/chapter.rb +91 -0
  41. data/lib/hecks/adapters/driving/ruby.rb +83 -0
  42. data/lib/hecks/adapters/driving/usage_cache.rb +144 -0
  43. data/lib/hecks/{doors.rb → adapters/driving.rb} +10 -6
  44. data/lib/hecks/behaviors/expectations/boot.rb +1 -1
  45. data/lib/hecks/bench/ruby_runner.rb +1 -1
  46. data/lib/hecks/bluebook/ask_resolution.rb +130 -0
  47. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +8 -2
  48. data/lib/hecks/bluebook/assembly/contracts/rules.rb +4 -2
  49. data/lib/hecks/bluebook/assembly/contracts/structure.rb +3 -1
  50. data/lib/hecks/bluebook/assembly/marks.rb +1 -0
  51. data/lib/hecks/bluebook/attribute.rb +1 -1
  52. data/lib/hecks/bluebook/behaviour/command.rb +13 -8
  53. data/lib/hecks/bluebook/behaviour/policy.rb +30 -0
  54. data/lib/hecks/bluebook/capabilities/attribute_values.rb +72 -0
  55. data/lib/hecks/bluebook/capabilities.rb +91 -9
  56. data/lib/hecks/bluebook/domain_port.rb +16 -4
  57. data/lib/hecks/bluebook/dsl/adapter_builder.rb +18 -6
  58. data/lib/hecks/bluebook/dsl/binding_proxy.rb +13 -0
  59. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/computed_sources.rb +98 -0
  60. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/fan_out_rows.rb +42 -0
  61. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/projected_fields.rb +14 -2
  62. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/provisions.rb +69 -3
  63. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/with_projections.rb +17 -10
  64. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +5 -0
  65. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
  66. data/lib/hecks/bluebook/dsl/command_builder/mutations.rb +8 -1
  67. data/lib/hecks/bluebook/dsl/command_builder/operands.rb +121 -0
  68. data/lib/hecks/bluebook/dsl/command_builder.rb +2 -0
  69. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +10 -1
  70. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +45 -2
  71. data/lib/hecks/bluebook/dsl/policy_builder.rb +47 -10
  72. data/lib/hecks/bluebook/dsl/rule_reference.rb +7 -4
  73. data/lib/hecks/bluebook/dsl/world_builder.rb +1 -1
  74. data/lib/hecks/bluebook/expression/ast_json/emitters.rb +6 -0
  75. data/lib/hecks/bluebook/expression/ast_json.rb +9 -6
  76. data/lib/hecks/bluebook/expression/ast_reader.rb +14 -9
  77. data/lib/hecks/bluebook/expression/projection.json +36 -0
  78. data/lib/hecks/bluebook/expression/resolver/operations.rb +31 -0
  79. data/lib/hecks/bluebook/expression/resolver/parsing.rb +38 -5
  80. data/lib/hecks/bluebook/expression/resolver/scanning.rb +22 -5
  81. data/lib/hecks/bluebook/expression/resolver/values.rb +10 -0
  82. data/lib/hecks/bluebook/expression/resolver.rb +20 -1
  83. data/lib/hecks/bluebook/hecksagon.rb +23 -3
  84. data/lib/hecks/bluebook/lifecycle.rb +22 -3
  85. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +13 -1
  86. data/lib/hecks/bluebook/meta_validator/modes.rb +4 -1
  87. data/lib/hecks/bluebook/meta_validator/readings.rb +18 -1
  88. data/lib/hecks/bluebook/meta_validator/reconstruction/heads.rb +2 -1
  89. data/lib/hecks/bluebook/meta_validator/shapes.rb +5 -0
  90. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +1 -1
  91. data/lib/hecks/bluebook/meta_validator.rb +12 -12
  92. data/lib/hecks/bluebook/model_check/ask_checks.rb +71 -0
  93. data/lib/hecks/bluebook/model_check/policy_checks.rb +2 -1
  94. data/lib/hecks/bluebook/model_check/reference_checks.rb +70 -0
  95. data/lib/hecks/bluebook/model_check.rb +8 -2
  96. data/lib/hecks/bluebook/policy.rb +4 -2
  97. data/lib/hecks/bluebook/smoke_test.rb +1 -1
  98. data/lib/hecks/bluebook/synthesizer.rb +2 -0
  99. data/lib/hecks/bluebook.rb +1 -0
  100. data/lib/hecks/cli/command_table.rb +1 -1
  101. data/lib/hecks/cli/console.rb +2 -1
  102. data/lib/hecks/cli/document.rb +1 -1
  103. data/lib/hecks/cli/launcher_words.rb +4 -4
  104. data/lib/hecks/cli/mcp.rb +4 -4
  105. data/lib/hecks/cli/project_cli/launcher_source.rb +2 -2
  106. data/lib/hecks/cli/project_cli/templates.rb +5 -5
  107. data/lib/hecks/cli/project_cli.rb +9 -6
  108. data/lib/hecks/cli/run.rb +2 -2
  109. data/lib/hecks/cli/smoke_http/checks.rb +4 -1
  110. data/lib/hecks/cli/smoke_http/transport.rb +3 -3
  111. data/lib/hecks/cli/smoke_http.rb +3 -0
  112. data/lib/hecks/codemod.rb +5 -2
  113. data/lib/hecks/corpus/routes.rb +7 -0
  114. data/lib/hecks/corpus/rust.rb +3 -2
  115. data/lib/hecks/deploy/bluebook/deploy.bluebook +124 -2
  116. data/lib/hecks/deploy/bluebook/deploy.hecksagon +8 -0
  117. data/lib/hecks/deploy/oidc.json +16 -3
  118. data/lib/hecks/doc/reference/rendering.rb +6 -3
  119. data/lib/hecks/forms/app/commands.rb +8 -7
  120. data/lib/hecks/forms/app/queries.rb +2 -1
  121. data/lib/hecks/forms/app.rb +1 -1
  122. data/lib/hecks/forms/record_renderer.rb +1 -1
  123. data/lib/hecks/framework/bluebook/privacy.bluebook +11 -0
  124. data/lib/hecks/fuzzing/concurrent_dispatch.rb +4 -1
  125. data/lib/hecks/fuzzing/failure_triage.rb +2 -1
  126. data/lib/hecks/fuzzing/mutation/run.rb +11 -4
  127. data/lib/hecks/fuzzing/properties/catalog.rb +6 -4
  128. data/lib/hecks/fuzzing/properties/guaranteed_by_construction.rb +18 -11
  129. data/lib/hecks/fuzzing/properties/guards.rb +1 -1
  130. data/lib/hecks/fuzzing/self_consistency/saga_redelivery.rb +1 -1
  131. data/lib/hecks/fuzzing/self_consistency.rb +6 -2
  132. data/lib/hecks/fuzzing/shrinker.rb +3 -1
  133. data/lib/hecks/fuzzing/value_generator.rb +19 -5
  134. data/lib/hecks/gate/stages.yml +4 -10
  135. data/lib/hecks/grammar/evolve.rb +1 -1
  136. data/lib/hecks/grammar/expression_operators.json +372 -0
  137. data/lib/hecks/grammar/operators.rb +3 -1
  138. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +10 -10
  139. data/lib/hecks/hecks/adapters/codebase/inspection/rubycritic.rb +35 -0
  140. data/lib/hecks/hecks/adapters/codebase/inspection.rb +104 -0
  141. data/lib/hecks/hecks/adapters/codebase/publishing.rb +2 -1
  142. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +12 -2
  143. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +2 -1
  144. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +1 -1
  145. data/lib/hecks/hecks/adapters/cost_explorer.rb +8 -5
  146. data/lib/hecks/hecks/adapters/deploy_toolchain/clearing.rb +51 -0
  147. data/lib/hecks/hecks/adapters/deploy_toolchain/statuses.rb +2 -1
  148. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +2 -0
  149. data/lib/hecks/hecks/adapters/finding_github.rb +13 -6
  150. data/lib/hecks/hecks/adapters/first_admin.rb +2 -2
  151. data/lib/hecks/hecks/adapters/github_rulesets.rb +6 -3
  152. data/lib/hecks/hecks/adapters/in_process_boot.rb +1 -1
  153. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +1 -1
  154. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +10 -20
  155. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +28 -5
  156. data/lib/hecks/hecks/adapters/local_files.rb +2 -2
  157. data/lib/hecks/hecks/adapters/rehearsal/audit_report.rb +107 -0
  158. data/lib/hecks/hecks/adapters/rehearsal/census.rb +71 -0
  159. data/lib/hecks/hecks/adapters/rehearsal/declaration.rb +133 -0
  160. data/lib/hecks/hecks/adapters/rehearsal/environment.rb +83 -0
  161. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +142 -0
  162. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +96 -0
  163. data/lib/hecks/hecks/adapters/rehearsal/postgres_census.rb +100 -0
  164. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +69 -0
  165. data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +129 -0
  166. data/lib/hecks/hecks/adapters/rehearsal/rows.rb +41 -0
  167. data/lib/hecks/hecks/adapters/rehearsal/rust_audit.rb +83 -0
  168. data/lib/hecks/hecks/adapters/rehearsal/subject.rb +104 -0
  169. data/lib/hecks/hecks/adapters/rehearsal.rb +54 -0
  170. data/lib/hecks/hecks/adapters/rehearsal_store.adapter +7 -0
  171. data/lib/hecks/hecks/adapters/rehearsal_store.rb +81 -0
  172. data/lib/hecks/hecks/adapters/site_toolchain/role_prober.rb +78 -0
  173. data/lib/hecks/hecks/adapters/site_toolchain.rb +29 -4
  174. data/lib/hecks/hecks/adapters/terminal.rb +13 -11
  175. data/lib/hecks/hecks/adapters/world_clearing.rb +65 -0
  176. data/lib/hecks/hecks/codebase.behaviors +13 -1
  177. data/lib/hecks/hecks/codebase.bluebook +239 -5
  178. data/lib/hecks/hecks/custodian.behaviors +24 -6
  179. data/lib/hecks/hecks/custodian.bluebook +6 -1227
  180. data/lib/hecks/hecks/deploy.behaviors +1 -1
  181. data/lib/hecks/hecks/deploy.world +1 -3
  182. data/lib/hecks/hecks/hecks.behaviors +1 -1
  183. data/lib/hecks/hecks/hecks.hecksagon +61 -18
  184. data/lib/hecks/hecks/hecks.world +11 -10
  185. data/lib/hecks/hecks/rehearsal.bluebook +477 -0
  186. data/lib/hecks/hecks/site.behaviors +1 -1
  187. data/lib/hecks/hecks/site.world +0 -1
  188. data/lib/hecks/hecks/tooling.bluebook +1238 -0
  189. data/lib/hecks/indifferent_key.rb +20 -0
  190. data/lib/hecks/language/adapter.bluebook +13 -3
  191. data/lib/hecks/language/bluebook/aggregate.bluebook +23 -0
  192. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -0
  193. data/lib/hecks/language/bluebook/entity.bluebook +18 -0
  194. data/lib/hecks/language/bluebook/policy.bluebook +9 -1
  195. data/lib/hecks/language/bluebook/query.bluebook +1 -1
  196. data/lib/hecks/language/bluebook/vocabulary.bluebook +11 -4
  197. data/lib/hecks/language/hecksagon/domain_port.bluebook +1 -1
  198. data/lib/hecks/language/hecksagon/hecksagon.bluebook +22 -0
  199. data/lib/hecks/language/oidc.json +10 -0
  200. data/lib/hecks/language/port.bluebook +4 -4
  201. data/lib/hecks/literal.rb +12 -3
  202. data/lib/hecks/ports/key_vault.rb +7 -2
  203. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +21 -10
  204. data/lib/hecks/ports/persistence/plugins/era/postgres_era/boot.rb +4 -2
  205. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +2 -1
  206. data/lib/hecks/ports/persistence/repository_factory.rb +6 -3
  207. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +29 -0
  208. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +1 -1
  209. data/lib/hecks/projections/deploy/fargate/settings/tuning.rb +15 -0
  210. data/lib/hecks/projections/deploy/fargate/settings.rb +3 -1
  211. data/lib/hecks/projections/deploy/fargate/stack/readers.rb +1 -0
  212. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  213. data/lib/hecks/projections/deploy/shared.rb +5 -2
  214. data/lib/hecks/projections/deploy/templates/fargate/Makefile.tmpl +7 -4
  215. data/lib/hecks/projections/model/deviations.rb +2 -2
  216. data/lib/hecks/projections/model.rb +2 -2
  217. data/lib/hecks/projections/oidc.rb +13 -1
  218. data/lib/hecks/projections/shape.rb +5 -2
  219. data/lib/hecks/projections/site/cms_editor/attributes.rb +95 -0
  220. data/lib/hecks/projections/site/cms_editor/blocks.rb +157 -0
  221. data/lib/hecks/projections/site/cms_editor/checks.rb +77 -0
  222. data/lib/hecks/projections/site/cms_editor/clearing.rb +70 -0
  223. data/lib/hecks/projections/site/cms_editor/color.rb +124 -0
  224. data/lib/hecks/projections/site/cms_editor/composition.rb +42 -0
  225. data/lib/hecks/projections/site/cms_editor/destructive.rb +61 -0
  226. data/lib/hecks/projections/site/cms_editor/drafts.rb +102 -0
  227. data/lib/hecks/projections/site/cms_editor/hints.rb +74 -0
  228. data/lib/hecks/projections/site/cms_editor/icons.rb +86 -0
  229. data/lib/hecks/projections/site/cms_editor/key_kinds.rb +31 -0
  230. data/lib/hecks/projections/site/cms_editor/limits.rb +72 -0
  231. data/lib/hecks/projections/site/cms_editor/media.rb +85 -0
  232. data/lib/hecks/projections/site/cms_editor/moment.rb +34 -0
  233. data/lib/hecks/projections/site/cms_editor/palette.rb +115 -0
  234. data/lib/hecks/projections/site/cms_editor/pickers.rb +85 -0
  235. data/lib/hecks/projections/site/cms_editor/pretty.rb +40 -0
  236. data/lib/hecks/projections/site/cms_editor/preview.rb +91 -0
  237. data/lib/hecks/projections/site/cms_editor/relations.rb +92 -0
  238. data/lib/hecks/projections/site/cms_editor/scale_checks.rb +66 -0
  239. data/lib/hecks/projections/site/cms_editor/scheduling.rb +150 -0
  240. data/lib/hecks/projections/site/cms_editor/schema.rb +161 -0
  241. data/lib/hecks/projections/site/cms_editor/setting.rb +130 -0
  242. data/lib/hecks/projections/site/cms_editor/targets.rb +86 -0
  243. data/lib/hecks/projections/site/cms_editor/templates/package.json.tmpl +36 -0
  244. data/lib/hecks/projections/site/cms_editor/templates/src/app.ts.tmpl +370 -0
  245. data/lib/hecks/projections/site/cms_editor/templates/src/assets.ts.tmpl +98 -0
  246. data/lib/hecks/projections/site/cms_editor/templates/src/auth/membership.ts.tmpl +87 -0
  247. data/lib/hecks/projections/site/cms_editor/templates/src/auth/session.ts.tmpl +36 -0
  248. data/lib/hecks/projections/site/cms_editor/templates/src/auth/sso.ts.tmpl +20 -0
  249. data/lib/hecks/projections/site/cms_editor/templates/src/browser/app.css.tmpl +412 -0
  250. data/lib/hecks/projections/site/cms_editor/templates/src/browser/autosave.js.tmpl +144 -0
  251. data/lib/hecks/projections/site/cms_editor/templates/src/browser/blocks.js.tmpl +500 -0
  252. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_doc.js.tmpl +175 -0
  253. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_editor.js.tmpl +418 -0
  254. data/lib/hecks/projections/site/cms_editor/templates/src/browser/epoch.js.tmpl +61 -0
  255. data/lib/hecks/projections/site/cms_editor/templates/src/browser/forms.js.tmpl +210 -0
  256. data/lib/hecks/projections/site/cms_editor/templates/src/browser/lists.js.tmpl +40 -0
  257. data/lib/hecks/projections/site/cms_editor/templates/src/browser/main.js.tmpl +26 -0
  258. data/lib/hecks/projections/site/cms_editor/templates/src/browser/media_picker.js.tmpl +232 -0
  259. data/lib/hecks/projections/site/cms_editor/templates/src/browser/moments.js.tmpl +73 -0
  260. data/lib/hecks/projections/site/cms_editor/templates/src/browser/pickers.js.tmpl +152 -0
  261. data/lib/hecks/projections/site/cms_editor/templates/src/browser/preview.js.tmpl +98 -0
  262. data/lib/hecks/projections/site/cms_editor/templates/src/browser/shell.js.tmpl +140 -0
  263. data/lib/hecks/projections/site/cms_editor/templates/src/browser/toast.js.tmpl +66 -0
  264. data/lib/hecks/projections/site/cms_editor/templates/src/choices.ts.tmpl +99 -0
  265. data/lib/hecks/projections/site/cms_editor/templates/src/commands.ts.tmpl +133 -0
  266. data/lib/hecks/projections/site/cms_editor/templates/src/config.ts.tmpl +29 -0
  267. data/lib/hecks/projections/site/cms_editor/templates/src/flash.ts.tmpl +52 -0
  268. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +43 -0
  269. data/lib/hecks/projections/site/cms_editor/templates/src/media/handler.ts.tmpl +177 -0
  270. data/lib/hecks/projections/site/cms_editor/templates/src/media/multipart.ts.tmpl +53 -0
  271. data/lib/hecks/projections/site/cms_editor/templates/src/media/sniff.ts.tmpl +94 -0
  272. data/lib/hecks/projections/site/cms_editor/templates/src/media/storage.ts.tmpl +50 -0
  273. data/lib/hecks/projections/site/cms_editor/templates/src/names.ts.tmpl +36 -0
  274. data/lib/hecks/projections/site/cms_editor/templates/src/schema.ts.tmpl +214 -0
  275. data/lib/hecks/projections/site/cms_editor/templates/src/server.ts.tmpl +65 -0
  276. data/lib/hecks/projections/site/cms_editor/templates/src/ui/availability.ts.tmpl +15 -0
  277. data/lib/hecks/projections/site/cms_editor/templates/src/ui/block_list.js.tmpl +213 -0
  278. data/lib/hecks/projections/site/cms_editor/templates/src/ui/blocks.ts.tmpl +275 -0
  279. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_model.js.tmpl +123 -0
  280. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_parse.js.tmpl +320 -0
  281. data/lib/hecks/projections/site/cms_editor/templates/src/ui/detail.ts.tmpl +88 -0
  282. data/lib/hecks/projections/site/cms_editor/templates/src/ui/dialogs.ts.tmpl +43 -0
  283. data/lib/hecks/projections/site/cms_editor/templates/src/ui/fields.ts.tmpl +275 -0
  284. data/lib/hecks/projections/site/cms_editor/templates/src/ui/html.ts.tmpl +61 -0
  285. data/lib/hecks/projections/site/cms_editor/templates/src/ui/icons.ts.tmpl +15 -0
  286. data/lib/hecks/projections/site/cms_editor/templates/src/ui/input.ts.tmpl +92 -0
  287. data/lib/hecks/projections/site/cms_editor/templates/src/ui/links.ts.tmpl +100 -0
  288. data/lib/hecks/projections/site/cms_editor/templates/src/ui/moments.ts.tmpl +44 -0
  289. data/lib/hecks/projections/site/cms_editor/templates/src/ui/outline.ts.tmpl +63 -0
  290. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pages.ts.tmpl +213 -0
  291. data/lib/hecks/projections/site/cms_editor/templates/src/ui/paging.ts.tmpl +82 -0
  292. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pickers.ts.tmpl +110 -0
  293. data/lib/hecks/projections/site/cms_editor/templates/src/ui/preview.ts.tmpl +87 -0
  294. data/lib/hecks/projections/site/cms_editor/templates/src/ui/related.ts.tmpl +97 -0
  295. data/lib/hecks/projections/site/cms_editor/templates/src/ui/schedule.ts.tmpl +160 -0
  296. data/lib/hecks/projections/site/cms_editor/templates/src/ui/shell.ts.tmpl +211 -0
  297. data/lib/hecks/projections/site/cms_editor/templates/src/ui/table.ts.tmpl +151 -0
  298. data/lib/hecks/projections/site/cms_editor/templates/src/ui/theme.js.tmpl +16 -0
  299. data/lib/hecks/projections/site/cms_editor/templates/src/ui/values.ts.tmpl +69 -0
  300. data/lib/hecks/projections/site/cms_editor/templates/src/ui/words.ts.tmpl +97 -0
  301. data/lib/hecks/projections/site/cms_editor/templates/src/ui/world.ts.tmpl +35 -0
  302. data/lib/hecks/projections/site/cms_editor/templates/tsconfig.json.tmpl +17 -0
  303. data/lib/hecks/projections/site/cms_editor/theme.rb +29 -0
  304. data/lib/hecks/projections/site/cms_editor/theme_checks.rb +57 -0
  305. data/lib/hecks/projections/site/cms_editor.rb +133 -0
  306. data/lib/hecks/projections/site/role_probe.rb +74 -0
  307. data/lib/hecks/projector/cli_audience.rb +2 -1
  308. data/lib/hecks/projector/cli_projector/listing.rb +1 -1
  309. data/lib/hecks/projector/cli_projector/option_specs.rb +1 -1
  310. data/lib/hecks/projector/docs_projector/closing.rb +1 -1
  311. data/lib/hecks/projector/docs_projector.rb +1 -1
  312. data/lib/hecks/projector/exporter/bindings.rb +40 -10
  313. data/lib/hecks/projector/exporter/capability_values.rb +55 -0
  314. data/lib/hecks/projector/narrate_projector/reactions.rb +1 -1
  315. data/lib/hecks/projector/target.rb +11 -0
  316. data/lib/hecks/projector.rb +32 -5
  317. data/lib/hecks/quality_control/adapters/github_checks.rb +12 -8
  318. data/lib/hecks/quality_control/adapters/github_issues.rb +8 -4
  319. data/lib/hecks/quality_control/cli/qa_open_pr.rb +2 -2
  320. data/lib/hecks/quality_control/cli/qa_sweep/checks/shrinking.rb +1 -1
  321. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +1 -1
  322. data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
  323. data/lib/hecks/quality_control/quality_control.bluebook +6 -2
  324. data/lib/hecks/release/approval.rb +64 -0
  325. data/lib/hecks/router/{door.rb → aggregate_reader.rb} +5 -4
  326. data/lib/hecks/router/namespace_installer.rb +16 -16
  327. data/lib/hecks/router.rb +1 -1
  328. data/lib/hecks/runtime/best_effort.rb +22 -0
  329. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -2
  330. data/lib/hecks/runtime/command_interpreter.rb +3 -2
  331. data/lib/hecks/runtime/command_rules/emission.rb +1 -1
  332. data/lib/hecks/runtime/dispatcher.rb +4 -4
  333. data/lib/hecks/runtime/entity_element/location.rb +1 -1
  334. data/lib/hecks/runtime/event.rb +5 -0
  335. data/lib/hecks/runtime/identity.rb +7 -6
  336. data/lib/hecks/runtime/interpreting.rb +2 -1
  337. data/lib/hecks/runtime/invocation_builder.rb +1 -1
  338. data/lib/hecks/runtime/loader/boot_steps.rb +8 -5
  339. data/lib/hecks/runtime/loader.rb +10 -10
  340. data/lib/hecks/runtime/policy_interpreter/arguments.rb +7 -4
  341. data/lib/hecks/runtime/policy_interpreter/fan_out.rb +3 -3
  342. data/lib/hecks/runtime/policy_interpreter.rb +6 -6
  343. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -1
  344. data/lib/hecks/runtime/reaction_invocation.rb +1 -1
  345. data/lib/hecks/runtime/rebuild_sweep.rb +11 -1
  346. data/lib/hecks/runtime/registry/ask_verification.rb +35 -0
  347. data/lib/hecks/runtime/registry/hecksagon_rules.rb +10 -0
  348. data/lib/hecks/runtime/registry/merging.rb +1 -1
  349. data/lib/hecks/runtime/registry/providers.rb +16 -0
  350. data/lib/hecks/runtime/registry/read_repository.rb +6 -4
  351. data/lib/hecks/runtime/registry/verification.rb +4 -1
  352. data/lib/hecks/runtime/registry/world_defaults.rb +29 -0
  353. data/lib/hecks/runtime/registry.rb +1 -1
  354. data/lib/hecks/runtime/saga_interpreter/compensation.rb +1 -1
  355. data/lib/hecks/runtime/saga_interpreter/delivery.rb +4 -4
  356. data/lib/hecks/runtime/saga_interpreter/transitions.rb +1 -1
  357. data/lib/hecks/runtime/saga_interpreter.rb +3 -3
  358. data/lib/hecks/runtime/tenant_check.rb +5 -4
  359. data/lib/hecks/runtime/value/coercion.rb +2 -2
  360. data/lib/hecks/runtime/value/validation.rb +16 -4
  361. data/lib/hecks/runtime.rb +9 -9
  362. data/lib/hecks/rust_build/project_rust/target_ir.rb +1 -1
  363. data/lib/hecks/rust_build/wasm.rb +1 -1
  364. data/lib/hecks/rust_build/wasm_browser.rb +1 -1
  365. data/lib/hecks/rust_build.rb +11 -0
  366. data/lib/hecks/served_options.rb +53 -0
  367. data/lib/hecks/site/bluebook/site.bluebook +29 -1
  368. data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
  369. data/lib/hecks/storehouse/audit.rb +1 -1
  370. data/lib/hecks/storehouse/dispatching.rb +8 -6
  371. data/lib/hecks/storehouse/introspection.rb +2 -2
  372. data/lib/hecks/storehouse/querying.rb +6 -4
  373. data/lib/hecks/storehouse.rb +4 -4
  374. data/lib/hecks/tools/comment_style/source_file.rb +4 -1
  375. data/lib/hecks/tools/deploy_recipe_lint/fixtures.rb +4 -1
  376. data/lib/hecks/tools/lanes/rows.rb +6 -0
  377. data/lib/hecks/tools/oidc_manifests.rb +4 -5
  378. data/lib/hecks/tools/regeneration_run.rb +11 -4
  379. data/lib/hecks/tools/site_routes/cli.rb +1 -1
  380. data/lib/hecks/tools/site_routes/editor_files.rb +45 -0
  381. data/lib/hecks/tools/site_routes.rb +14 -7
  382. data/lib/hecks/tools/tenant_provisioning.rb +2 -2
  383. data/lib/hecks/tools/tools_doc.rb +4 -4
  384. data/lib/hecks/version.rb +1 -1
  385. data/lib/hecks/vocabulary.rb +4 -4
  386. data/lib/hecks.rb +15 -15
  387. data/lib/rubocop/cop/hecks/env_dump.rb +69 -0
  388. data/lib/rubocop/cop/hecks/monotonic_duration.rb +53 -0
  389. data/lib/rubocop/cop/hecks/no_wall_clock_in_runtime.rb +34 -0
  390. data/lib/rubocop/cop/hecks/raw_shell_interpolation.rb +79 -0
  391. data/lib/rubocop/cop/hecks/sleep_in_spec.rb +34 -0
  392. data/lib/rubocop/cop/hecks/swallowed_rescue.rb +37 -0
  393. data/lib/rubocop/cop/hecks/symbol_string_key_mix.rb +50 -0
  394. data/lib/rubocop/cop/hecks/untyped_raise.rb +37 -0
  395. data/prebuilt/05c358d38d661a3774f813d49c66f5b68939af94432d66257a56f49ee2598994.marshal +0 -0
  396. data/prebuilt/26c40c61f09b09b11651c64668eda3ceb180f83cf2a3802d3033d2fd99ac841b.marshal +0 -0
  397. data/prebuilt/verdicts-230ac18c25abfd3d92ede032bee2661a4c2cd2773ab0cf0e2ae52546ba2f7af9.json +1 -0
  398. data/rust/.gitignore +6 -0
  399. data/rust/Cargo.toml +9 -1
  400. data/rust/build/src/binding_pass.rs +103 -0
  401. data/rust/build/src/cargo_sync.rs +6 -0
  402. data/rust/build/src/lineage_pass.rs +3 -3
  403. data/rust/build/src/main.rs +17 -5
  404. data/rust/build/src/pipeline.rs +48 -5
  405. data/rust/build/src/seams_pass.rs +123 -0
  406. data/rust/build/src/translation_pass.rs +167 -0
  407. data/rust/codegen/src/asks.rs +103 -0
  408. data/rust/codegen/src/bridging.rs +1 -1
  409. data/rust/codegen/src/commands.rs +13 -8
  410. data/rust/codegen/src/constraints.rs +25 -0
  411. data/rust/codegen/src/domain_generator.rs +11 -9
  412. data/rust/codegen/src/expr_emitter.rs +13 -0
  413. data/rust/codegen/src/json_codec.rs +54 -15
  414. data/rust/codegen/src/main.rs +2 -1
  415. data/rust/codegen/src/mutations.rs +174 -4
  416. data/rust/codegen/src/naming.rs +30 -0
  417. data/rust/codegen/src/queries.rs +11 -2
  418. data/rust/codegen/src/reactions.rs +1 -1
  419. data/rust/codegen/src/types.rs +166 -8
  420. data/rust/host/HECKS_RELEASE +1 -1
  421. data/rust/host/src/api.rs +4 -4
  422. data/rust/host/src/bin/validate_harness.rs +43 -0
  423. data/rust/host/src/boundary_fuzz/expr_json.rs +2 -2
  424. data/rust/host/src/boundary_fuzz/ir.rs +1 -0
  425. data/rust/host/src/boundary_fuzz/server.rs +1 -1
  426. data/rust/host/src/checkout.rs +35 -19
  427. data/rust/host/src/commerce_ir.rs +408 -2
  428. data/rust/host/src/expr_json.rs +118 -0
  429. data/rust/host/src/mint.rs +1 -1
  430. data/rust/host/src/payments/tests.rs +27 -1
  431. data/rust/host/src/payments.rs +12 -4
  432. data/rust/host/src/query_step.rs +22 -4
  433. data/rust/host/src/reference_validate.rs +653 -108
  434. data/rust/host/src/resend.rs +2 -0
  435. data/rust/host/src/server.rs +19 -6
  436. data/rust/host/src/ui_schema.rs +132 -5
  437. data/rust/host/src/web/newsletter.rs +7 -8
  438. data/rust/host/src/web/newsletter_send.rs +38 -13
  439. data/rust/host/src/web/registrations.rs +229 -26
  440. data/rust/host/src/web.rs +3 -3
  441. data/rust/parser/src/emit.rs +38 -6
  442. data/rust/parser/src/expr/ast_json.rs +7 -1
  443. data/rust/parser/src/expr/resolver.rs +171 -6
  444. data/rust/parser/src/ir.rs +7 -0
  445. data/rust/parser/src/keywords.rs +24 -0
  446. data/rust/parser/src/main.rs +3 -0
  447. data/rust/parser/src/parse/aggregate.rs +2 -1
  448. data/rust/parser/src/parse/chapter.rs +2 -2
  449. data/rust/parser/src/parse/command.rs +2 -0
  450. data/rust/parser/src/parse/hecksagon.rs +5 -2
  451. data/rust/parser/src/parse/lifecycle.rs +81 -0
  452. data/rust/parser/src/parse/mod.rs +15 -1
  453. data/rust/parser/src/parse/policy.rs +13 -2
  454. data/rust/parser/src/parse/value_object.rs +13 -8
  455. data/rust/parser/src/ruby_value.rs +1 -1
  456. data/rust/src/exemplar/commands.rs +4 -4
  457. data/rust/src/exemplar/constraints.rs +1 -1
  458. data/rust/src/exemplar/json.rs +43 -1
  459. data/rust/src/kernel/cli.rs +7 -5
  460. data/rust/src/kernel/dispatch.rs +2 -2
  461. data/rust/src/kernel/dispatch_guard_tests.rs +1 -1
  462. data/rust/src/kernel/expr.rs +73 -4
  463. data/rust/src/kernel/expression_operators/arithmetic.rs +130 -5
  464. data/rust/src/kernel/expression_operators/presence.rs +41 -3
  465. data/rust/src/kernel/expression_operators/text.rs +26 -2
  466. data/rust/src/kernel/json.rs +15 -0
  467. data/rust/src/kernel/mod.rs +1 -1
  468. data/rust/src/kernel/orchestrate.rs +54 -5
  469. data/rust/src/kernel/orchestrate_guard_tests.rs +67 -0
  470. data/rust/src/kernel/pattern.rs +60 -0
  471. data/rust/src/kernel/reference_lookup.rs +90 -7
  472. metadata +174 -36
  473. data/lib/hecks/doors/cli_door/nesting.rb +0 -34
  474. data/lib/hecks/doors/cli_door/shorthand.rb +0 -85
  475. data/lib/hecks/doors/cli_door.rb +0 -124
  476. data/lib/hecks/doors/cli_runner/answers.rb +0 -138
  477. data/lib/hecks/doors/cli_runner/resolution.rb +0 -146
  478. data/lib/hecks/doors/cli_runner/settling.rb +0 -115
  479. data/lib/hecks/doors/cli_runner/streaming.rb +0 -90
  480. data/lib/hecks/doors/cli_runner/suggestions.rb +0 -43
  481. data/lib/hecks/doors/cli_runner.rb +0 -66
  482. data/lib/hecks/doors/command_request.rb +0 -119
  483. data/lib/hecks/doors/handle.rb +0 -208
  484. data/lib/hecks/doors/json_door.rb +0 -132
  485. data/lib/hecks/doors/launcher_options.rb +0 -247
  486. data/lib/hecks/doors/mcp_door/properties.rb +0 -47
  487. data/lib/hecks/doors/mcp_door/tool_calls.rb +0 -113
  488. data/lib/hecks/doors/mcp_door/tool_definitions/auditing.rb +0 -51
  489. data/lib/hecks/doors/mcp_door/tool_definitions/calling.rb +0 -69
  490. data/lib/hecks/doors/mcp_door/tool_definitions/reading.rb +0 -97
  491. data/lib/hecks/doors/mcp_door/tool_definitions.rb +0 -15
  492. data/lib/hecks/doors/mcp_door.rb +0 -156
  493. data/lib/hecks/doors/mcp_door_scope/argument_policy.rb +0 -92
  494. data/lib/hecks/doors/mcp_door_scope/env_problems.rb +0 -55
  495. data/lib/hecks/doors/mcp_door_scope/presenter.rb +0 -78
  496. data/lib/hecks/doors/mcp_door_scope.rb +0 -201
  497. data/lib/hecks/doors/mcp_guide_cache.rb +0 -75
  498. data/lib/hecks/doors/ruby_door/aggregate_door.rb +0 -162
  499. data/lib/hecks/doors/ruby_door/chapter.rb +0 -89
  500. data/lib/hecks/doors/ruby_door.rb +0 -80
  501. data/lib/hecks/doors/usage_cache.rb +0 -139
  502. data/prebuilt/0ee7f83cba59ce7d1c45c65ba166b071779f70143449f584f33ff7989f64eccb.marshal +0 -0
  503. data/prebuilt/6e4a6bcf0aa880e2e6b056e1846b70e5a768090b9dc998bbadeaf5d6bcfd3db8.marshal +0 -0
  504. data/prebuilt/verdicts-91aaf93ce8981bd9a754948f075d95650dc2ec6435015376f5c60992b36a24b1.json +0 -1
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hecks
4
+ module Projections
5
+ module Site
6
+ module CmsEditor
7
+ # The aggregate that registers pictures, found by shape in the schema the editor reads.
8
+ #
9
+ # It is an aggregate with a creating command whose attributes are the aggregate's identity
10
+ # (the key), an alt text and a mime type, and optionally a width and a height; every other
11
+ # attribute of that command must be optional, because an upload cannot fill it. Neither the
12
+ # aggregate's name nor the command's decides it; the attribute names are the vocabulary, the
13
+ # way `blocks`, `kind` and `spans` are for a document body. An attribute is a plain value or
14
+ # a value object whose first part is a plain value; the editor sends a value the same way.
15
+ #
16
+ # The domain keeps the picture's record, never its bytes.
17
+ module Media
18
+ # The attribute names that carry each part of a picture's record besides its key.
19
+ ROLES = { "alt" => %w[alt alt_text], "mime" => %w[mime_type mime content_type],
20
+ "width" => %w[width], "height" => %w[height] }.freeze
21
+
22
+ # The parts every picture's registration must take.
23
+ REQUIRED = %w[key alt mime].freeze
24
+
25
+ module_function
26
+
27
+ # @param aggregates [Array<Hash{String => Object}>] the aggregates, as `Schema` reads them
28
+ # @return [Hash{String => Object}, nil] the aggregate and command that register a
29
+ # picture, each part's attribute and the query that lists the pictures; nil when no
30
+ # aggregate has the shape
31
+ def read(aggregates)
32
+ aggregates.each do |agg|
33
+ agg["commands"].select { |command| command["creates"] }.each do |command|
34
+ found = fields(agg, command)
35
+ return describe(agg, command, found) if found
36
+ end
37
+ end
38
+ nil
39
+ end
40
+
41
+ # @return [Hash{String => Hash}, nil] each part's attribute and how its value is sent, or
42
+ # nil when the command is not a picture's registration
43
+ def fields(agg, command)
44
+ chosen = parts(agg, command["attributes"])
45
+ return nil unless REQUIRED.all? { |role| chosen.key?(role) } && others_optional?(command, chosen)
46
+
47
+ wrapped = chosen.transform_values { |attr| wrap(agg, attr) }
48
+ return nil if wrapped.value?(false)
49
+
50
+ chosen.to_h { |role, attr| [role, { "name" => attr["name"], "wrap" => wrapped.fetch(role) }] }
51
+ end
52
+
53
+ # @return [Boolean] whether every attribute that is not a part of the record is optional
54
+ def others_optional?(command, chosen)
55
+ (command["attributes"] - chosen.values).all? { |attr| attr["optional"] }
56
+ end
57
+
58
+ # @return [Hash{String => Hash}] each part of the record to the attribute that carries it
59
+ def parts(agg, attrs)
60
+ found = { "key" => attrs.find { |attr| attr["name"] == agg["identity"] } }
61
+ ROLES.each { |role, names| found[role] = attrs.find { |attr| names.include?(attr["name"]) } }
62
+ found.compact
63
+ end
64
+
65
+ # @return [String, nil, false] the part a value object's value is sent in, nil for a
66
+ # plain value, false for an attribute an upload cannot fill
67
+ def wrap(agg, attr)
68
+ return false if attr["list"] || attr["kind"] == "reference"
69
+ return nil unless attr["kind"] == "object"
70
+
71
+ first = agg["valueObjects"].fetch(attr["type"], []).first
72
+ first && first["kind"] != "object" && !first["list"] ? first["name"] : false
73
+ end
74
+
75
+ # @return [Hash{String => Object}] the picture aggregate as the editor's schema names it
76
+ def describe(agg, command, found)
77
+ listing = agg["queries"].find { |query| query["attributes"].empty? }
78
+ { "aggregate" => agg["name"], **agg.slice("chapter"), "command" => command["name"], "fields" => found,
79
+ "listing" => listing && listing["name"] }
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hecks
4
+ module Projections
5
+ module Site
6
+ module CmsEditor
7
+ # Which integer attributes are moments in time, read from the names the bluebook gave them.
8
+ #
9
+ # An integer is a moment when one of the names it goes by (the attribute's, its value
10
+ # object's, the value object's part's) ends in `_at` or `At` (a date and a time), ends in
11
+ # `_on` or `On` (a date), or has `epoch` in it (seconds since 1970, so a date and a time).
12
+ # The value is always whole seconds since 1970 UTC; the editor shows it in the viewer's own
13
+ # time zone. A name is the only evidence the bluebook gives, so the rule is this narrow:
14
+ # an integer named `duration` or `Season` is never a date.
15
+ module Moment
16
+ # Names that end in `at` or `on` as a word, so `created_at` and `CreatedAt` match and
17
+ # `format`, `Season` and `Combat` do not.
18
+ AT = /(?:_at|[a-z0-9]At)\z/
19
+ ON = /(?:_on|[a-z0-9]On)\z/
20
+
21
+ module_function
22
+
23
+ # @param names [Array<String>] the names the integer goes by
24
+ # @return [String, nil] `"datetime"`, `"date"`, or nil when the integer is not a moment
25
+ def widget(names)
26
+ return "datetime" if names.any? { |name| AT.match?(name) || name.match?(/epoch/i) }
27
+
28
+ "date" if names.any? { |name| ON.match?(name) }
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "color"
4
+
5
+ module Hecks
6
+ module Projections
7
+ module Site
8
+ module CmsEditor
9
+ # The editor's colours, light and dark, derived from one accent colour, as the custom
10
+ # properties of a daisyUI theme (`--color-base-100`, `--color-primary`, ...) and the few
11
+ # extras the pages use (`--ed-muted`, `--ed-edge`). Every text and fill pair is moved
12
+ # until it reaches the accessibility standard's level for it (4.5:1 for text, 3:1 for the
13
+ # edge of a control), so a brand colour that would fail cannot make the editor unreadable.
14
+ #
15
+ # The neutrals (ground, page, ink, muted ink, rule) are fixed when the row names no
16
+ # accent, and take the accent's hue at low saturation when it does, so an accent retints
17
+ # the whole theme. Primary buttons are filled with ink (daisyUI's `neutral`); the accent
18
+ # (`primary`) marks selection, focus, links, the active navigation item and the saved or
19
+ # published state.
20
+ class Palette
21
+ # The accent a row that names none gets: a deep verdigris.
22
+ DEFAULT_ACCENT = "#1f7a6d"
23
+
24
+ # The neutrals a row that names no accent gets, per theme.
25
+ FIXED = {
26
+ light: { ground: "#f4f6f8", surface: "#ffffff", ink: "#1a222c", muted: "#5a6675", rule: "#dce1e7" },
27
+ dark: { ground: "#11161b", surface: "#181f26", ink: "#e7ebef", muted: "#9aa6b3", rule: "#28313b" }
28
+ }.freeze
29
+
30
+ # Hues of the status colours, in degrees.
31
+ TONES = { ok: 150.0, warn: 38.0, danger: 4.0 }.freeze
32
+
33
+ # The daisyUI variable each status tone fills.
34
+ STATUS = { ok: "success", warn: "warning", danger: "error" }.freeze
35
+
36
+ # @param accent [String, nil] a six-digit hex colour, or nil for the default
37
+ def initialize(accent = nil)
38
+ @custom = !accent.to_s.empty?
39
+ @accent = (accent.to_s.empty? ? DEFAULT_ACCENT : accent).downcase
40
+ end
41
+
42
+ # @return [Hash{Symbol => Hash{String => String}}] each theme's properties, name to value
43
+ def themes = { light: properties(:light), dark: properties(:dark) }
44
+
45
+ private
46
+
47
+ def properties(theme)
48
+ base = neutrals(theme)
49
+ found = base.merge(accents(theme, base)).merge(tones(theme, base))
50
+ { **daisy(found), **extras(found) }
51
+ end
52
+
53
+ def lighter(theme) = theme == :light ? :darker : :lighter
54
+
55
+ def neutrals(theme)
56
+ fixed = @custom ? tinted(FIXED.fetch(theme)) : FIXED.fetch(theme)
57
+ backs = [fixed[:surface], fixed[:ground]]
58
+ edge = Color.reach(Color.mix(fixed[:rule], fixed[:muted], 0.5), backs, 3.0, lighter(theme))
59
+ { **fixed, muted: Color.reach(fixed[:muted], backs, 4.5, lighter(theme)), edge: edge }
60
+ end
61
+
62
+ # The fixed neutrals with the accent's hue at the saturation each already has.
63
+ def tinted(fixed)
64
+ hue = Color.hsl(@accent).first
65
+ fixed.transform_values do |hex|
66
+ _, sat, light = Color.hsl(hex)
67
+ hex == "#ffffff" ? hex : Color.from_hsl(hue, sat, light)
68
+ end
69
+ end
70
+
71
+ def accents(theme, base)
72
+ backs = [base[:surface], base[:ground]]
73
+ fill = theme == :light ? @accent : Color.reach(@accent, backs, 4.5, :lighter)
74
+ soft = Color.mix(base[:surface], fill, theme == :light ? 0.1 : 0.18)
75
+ { primary: Color.reach(fill, [*backs, soft], 4.5, lighter(theme)), accent_soft: soft }
76
+ end
77
+
78
+ def tones(theme, base)
79
+ TONES.each_with_object({}) do |(name, hue), found|
80
+ back = theme == :light ? Color.from_hsl(hue, 0.5, 0.94) : Color.from_hsl(hue, 0.28, 0.18)
81
+ start = Color.from_hsl(hue, 0.55, theme == :light ? 0.3 : 0.72)
82
+ found[:"#{name}_bg"] = back
83
+ found[:"#{name}_fg"] = Color.reach(start, [back, base[:surface]], 4.5, lighter(theme))
84
+ end
85
+ end
86
+
87
+ # The first of white, a near-black and black that reads on `fill`.
88
+ def on(fill) = ["#ffffff", "#0f151a", "#000000"].find { |hex| Color.contrast(hex, fill) >= 4.5 }
89
+
90
+ def daisy(found)
91
+ colours = surfaces(found).merge(fills(found))
92
+ named = colours.slice("primary", "secondary", "accent", "info", *STATUS.values)
93
+ contents = named.to_h { |name, value| ["#{name}-content", on(value)] }
94
+ colours.merge(contents).transform_keys { |name| "--color-#{name}" }
95
+ end
96
+
97
+ def surfaces(found)
98
+ { "base-100" => found[:surface], "base-200" => found[:ground], "base-300" => found[:rule],
99
+ "base-content" => found[:ink], "neutral" => found[:ink], "neutral-content" => found[:surface] }
100
+ end
101
+
102
+ def fills(found)
103
+ { "primary" => found[:primary], "secondary" => found[:muted], "accent" => found[:primary], "info" => found[:primary],
104
+ **STATUS.to_h { |tone, name| [name, found[:"#{tone}_fg"]] } }
105
+ end
106
+
107
+ def extras(found)
108
+ { "--ed-muted" => found[:muted], "--ed-edge" => found[:edge], "--ed-soft" => found[:accent_soft],
109
+ "--ed-ok-bg" => found[:ok_bg], "--ed-warn-bg" => found[:warn_bg], "--ed-danger-bg" => found[:danger_bg] }
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "targets"
4
+
5
+ module Hecks
6
+ module Projections
7
+ module Site
8
+ module CmsEditor
9
+ # Which attributes name another aggregate's instance, found by the bluebook's own
10
+ # declarations, so the editor offers the instances in place of a text box. In this order:
11
+ # a `<kind>:<slug>` key by its pattern (see `KeyKinds`), each kind that is an aggregate's
12
+ # snake-case name offering that aggregate; a `reference_to` attribute whose target is an
13
+ # editor aggregate; a value object of one text part named `<Stem>Ref`, where `<Stem>` is
14
+ # the type of an editor aggregate's identity or its name; and `<Image|Picture|Photo|Media>
15
+ # Ref` with a picture aggregate, which suggests pictures and accepts any other text. A
16
+ # target is found once: the same chapter's, else the only one of its name; else none.
17
+ module Pickers
18
+ # The stems that name a picture, for the editor's picture aggregate.
19
+ PICTURE_STEMS = %w[Image Picture Photo Media].freeze
20
+
21
+ module_function
22
+
23
+ # @param aggregates [Array<Hash{String => Object}>] the aggregates as `Schema` shapes them
24
+ # @param media [Hash{String => Object}, nil] the picture aggregate, as `Media` says
25
+ # @return [Array<Hash{String => Object}>] the aggregates, each attribute that names an
26
+ # instance carrying its `picker`
27
+ def apply(aggregates, media = nil)
28
+ targets = Targets.new(aggregates, media)
29
+ aggregates.map { |agg| annotate(agg, targets) }
30
+ end
31
+
32
+ # @return [Hash{String => Object}] `agg` with a picker on each attribute that has one
33
+ def annotate(agg, targets)
34
+ mark = ->(list) { list.map { |attr| with_picker(attr, agg, targets) } }
35
+ agg.merge("attributes" => mark.call(agg["attributes"]), "valueObjects" => objects(agg, mark),
36
+ "commands" => marked(agg["commands"], mark), "queries" => marked(agg["queries"], mark))
37
+ end
38
+
39
+ # @return [Array<Hash>] the commands or queries, the attributes of each marked
40
+ def marked(items, mark) = items.map { |item| item.merge("attributes" => mark.call(item["attributes"])) }
41
+
42
+ # @return [Hash{String => Array}] the value objects, the parts of one of several parts
43
+ # marked; a value object of one part is entered as the attribute that names it
44
+ def objects(agg, mark)
45
+ agg["valueObjects"].transform_values do |parts|
46
+ parts.size == 1 ? parts.map { |part| part.except("keys") } : mark.call(parts)
47
+ end
48
+ end
49
+
50
+ # @return [Hash{String => Object}] `attr` with its `picker`, or as it was
51
+ def with_picker(attr, agg, targets)
52
+ picker = picker_for(attr, agg, targets)
53
+ picker ? attr.except("keys").merge("picker" => picker) : attr
54
+ end
55
+
56
+ def picker_for(attr, agg, targets)
57
+ return kinds(attr["keys"], agg, targets) if attr["keys"]
58
+ return targets.named(attr["target"], agg) if attr["kind"] == "reference"
59
+
60
+ stem = reference_stem(attr, agg)
61
+ stem && (targets.keyed_by(stem, agg) || (targets.pictures if PICTURE_STEMS.include?(stem)))
62
+ end
63
+
64
+ # @return [Hash{String => Array}] one entry per kind, with the aggregate it names if any
65
+ def kinds(words, agg, targets)
66
+ { "kinds" => words.map { |word| { "kind" => word, **(targets.kind(word, agg) || {}) } } }
67
+ end
68
+
69
+ # @return [String, nil] `Stem` of a `StemRef` value object that has one plain text part
70
+ def reference_stem(attr, agg)
71
+ return nil unless plain_text_object?(attr, agg["valueObjects"][attr["type"]])
72
+
73
+ stem = attr["type"].delete_suffix("Ref")
74
+ stem unless stem.empty? || stem == attr["type"]
75
+ end
76
+
77
+ # @return [Boolean] whether `attr` is a value object whose one part is a single text
78
+ def plain_text_object?(attr, parts)
79
+ attr["kind"] == "object" && parts&.size == 1 && parts.first["kind"] == "text" && !parts.first["list"]
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Hecks
6
+ module Projections
7
+ module Site
8
+ module CmsEditor
9
+ # JSON indented two spaces, with an empty list or object as `[]` or `{}`. It does not use
10
+ # `JSON.pretty_generate`, whose spelling of an empty list depends on which JSON extensions
11
+ # are loaded, so the projection's text is the same wherever it runs.
12
+ module Pretty
13
+ module_function
14
+
15
+ # @param value [Object] a hash, array, string, number, boolean or nil
16
+ # @param depth [Integer] the indentation level of the line the value starts on
17
+ # @return [String] the value as indented JSON
18
+ def generate(value, depth = 0)
19
+ case value
20
+ when Hash then block("{", "}", value.map { |key, item| member(key, item, depth) }, depth)
21
+ when Array then block("[", "]", value.map { |item| generate(item, depth + 1) }, depth)
22
+ else JSON.generate(value)
23
+ end
24
+ end
25
+
26
+ # @return [String] one `"key": value` line of an object
27
+ def member(key, item, depth) = "#{JSON.generate(key.to_s)}: #{generate(item, depth + 1)}"
28
+
29
+ # @return [String] the lines between `open` and `close`, each indented under `depth`
30
+ def block(open, close, lines, depth)
31
+ return "#{open}#{close}" if lines.empty?
32
+
33
+ pad = " " * (depth + 1)
34
+ "#{open}\n#{lines.map { |line| "#{pad}#{line}" }.join(",\n")}\n#{" " * depth}#{close}"
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Hecks
6
+ module Projections
7
+ module Site
8
+ module CmsEditor
9
+ # The `Editor` row's `preview`: where a record is shown on the site, as a URL template the
10
+ # editor frames in a side drawer.
11
+ #
12
+ # A template is an `http://` or `https://` address, or a path that starts with one slash,
13
+ # with the placeholders `{key}`, `{kind}`, `{slug}` and `{id}` filled from a record's
14
+ # identity when the page is built. Anything else is refused, so a template cannot name a
15
+ # scheme that runs script (`javascript:`), a host written with credentials, or a
16
+ # placeholder the editor would leave unfilled. A placeholder may not stand in the origin
17
+ # (scheme, host and port), because the content security policy names that origin once, at
18
+ # build time, and allows no other frame.
19
+ module Preview
20
+ # The words a template may put in braces.
21
+ PLACEHOLDERS = %w[key kind slug id].freeze
22
+
23
+ # An absolute address: the origin, then whatever follows it.
24
+ ABSOLUTE = %r{\A(?<origin>https?://[^/?#\s]+)(?<rest>.*)\z}m
25
+
26
+ module_function
27
+
28
+ # @param template [String] the row's `preview`, which may be empty
29
+ # @return [Array<String>] every reason the template is refused; empty when it is usable
30
+ def problems(template)
31
+ return [] if template.empty?
32
+
33
+ found = []
34
+ found << "must not hold whitespace" if template.match?(/\s/)
35
+ found.concat(shape_problems(template))
36
+ found.concat(placeholder_problems(template))
37
+ found.map { |problem| "preview #{template.inspect} #{problem}" }
38
+ end
39
+
40
+ # @return [String, nil] what the content security policy's `frame-src` names for the
41
+ # template: its origin, `'self'` for a path, or nil for no preview
42
+ def frame_source(template)
43
+ return nil if template.empty?
44
+
45
+ found = ABSOLUTE.match(template)
46
+ found ? found[:origin] : "'self'"
47
+ end
48
+
49
+ # @return [Array<String>] the problems with the scheme, the host and the path
50
+ def shape_problems(template)
51
+ found = ABSOLUTE.match(template)
52
+ return path_problems(template) unless found
53
+
54
+ origin_problems(found[:origin])
55
+ end
56
+
57
+ # @return [Array<String>] the problems with a path template
58
+ def path_problems(template)
59
+ return ["must be an http(s) address or a path that starts with one slash"] unless template.start_with?("/")
60
+ return ["must not start with two slashes, which names another host"] if template.start_with?("//")
61
+
62
+ []
63
+ end
64
+
65
+ # @return [Array<String>] the problems with the origin of an absolute template
66
+ def origin_problems(origin)
67
+ found = []
68
+ found << "must not name credentials in the address" if origin.include?("@")
69
+ found << "must not hold a placeholder in the scheme, host or port" if origin.include?("{")
70
+ found << "names no host" if !origin.include?("{") && URI.parse(origin).host.to_s.empty?
71
+ found
72
+ rescue URI::InvalidURIError
73
+ ["is not a valid address"]
74
+ end
75
+
76
+ # @return [Array<String>] a problem for each unknown placeholder and each stray brace
77
+ def placeholder_problems(template)
78
+ unknown = template.scan(/\{([^{}]*)\}/).flatten.uniq - PLACEHOLDERS
79
+ found = unknown.map do |name|
80
+ "names the placeholder {#{name}}; the placeholders are #{PLACEHOLDERS.map do |word|
81
+ "{#{word}}"
82
+ end.join(", ")}"
83
+ end
84
+ found << "has a brace that is not part of a placeholder" if template.gsub(/\{[^{}]*\}/, "").match?(/[{}]/)
85
+ found
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hecks
4
+ module Projections
5
+ module Site
6
+ module CmsEditor
7
+ # The records that belong to one record, found by the keys they carry. Aggregate `B` is
8
+ # related to `A` when `B`'s identity is a `<kind>:<slug>` key (see `KeyKinds`) whose
9
+ # kinds include `A`'s snake-case name, so `post:first-light` keys the `B` of the `Post`
10
+ # `first-light`.
11
+ #
12
+ # The rule reads the declared pattern and what the kind resolves to (see `Pickers`),
13
+ # never a name. A `B` is shown by its shape: a **document** (a rich-text body), a
14
+ # **gallery** (a list that names pictures) or **metadata** (anything else). Its creating
15
+ # command is the first that takes the identity; its editing command is the draft-saving
16
+ # command of a document that keeps drafts (see `Drafts`), else the first command on an
17
+ # existing record that is neither a lifecycle move nor destructive and takes the body or
18
+ # the pictures.
19
+ module Relations
20
+ module_function
21
+
22
+ # @param aggregates [Array<Hash{String => Object}>] the aggregates as `Pickers` leaves
23
+ # them
24
+ # @param media [Hash{String => Object}, nil] the picture aggregate, as `Media` says
25
+ # @return [Array<Hash{String => Object}>] each aggregate, carrying `related` when other
26
+ # aggregates are keyed by it
27
+ def apply(aggregates, media = nil)
28
+ aggregates.map do |agg|
29
+ found = aggregates.reject { |other| other.equal?(agg) }.filter_map { |other| relation(other, agg, media) }
30
+ found.empty? ? agg : agg.merge("related" => found)
31
+ end
32
+ end
33
+
34
+ # @return [Hash{String => Object}, nil] how `other` is related to `agg`, or nil when it
35
+ # is not
36
+ def relation(other, agg, media)
37
+ key = other["attributes"].find { |attr| attr["name"] == other["identity"] }
38
+ return nil unless key && keyed_by?(key, agg)
39
+
40
+ shape, field = shape_of(other, media)
41
+ { "aggregate" => other["name"], "chapter" => other["chapter"], "field" => key["name"], "shape" => shape,
42
+ "create" => creating(other, key), "edit" => editing(other, field) }.compact
43
+ end
44
+
45
+ # @return [Boolean] whether `key` is declared a key of `<agg's snake name>:<slug>`
46
+ def keyed_by?(key, agg)
47
+ (key.dig("picker", "kinds") || []).any? { |kind| kind["target"] == agg["name"] && kind["chapter"] == agg["chapter"] }
48
+ end
49
+
50
+ # @return [Array(String, String)] the shape, and the attribute that gives it
51
+ def shape_of(other, media)
52
+ body = other["attributes"].find { |attr| attr["widget"] == "body" && !attr["list"] }
53
+ return ["document", body["name"]] if body
54
+
55
+ pictures = pictures_of(other, media)
56
+ pictures ? ["gallery", pictures["name"]] : ["metadata", nil]
57
+ end
58
+
59
+ # @return [Hash{String => Object}, nil] the list attribute that names pictures
60
+ def pictures_of(other, media)
61
+ return nil unless media
62
+
63
+ other["attributes"].find { |attr| attr["list"] && attr.dig("picker", "target") == media["aggregate"] }
64
+ end
65
+
66
+ # @return [String, nil] the first creating command that takes the identity
67
+ def creating(other, key)
68
+ other["commands"].find do |command|
69
+ command["creates"] && command["attributes"].any? do |attr|
70
+ attr["name"] == key["name"]
71
+ end
72
+ end&.fetch("name")
73
+ end
74
+
75
+ # @return [String, nil] the command that changes the record's body, pictures or fields
76
+ def editing(other, field)
77
+ return other.dig("drafts", "save") if other["drafts"] && field == other.dig("drafts", "live")
78
+
79
+ moves = (other.dig("lifecycle", "transitions") || []).map { |move| move["verb"] }
80
+ other["commands"].find { |command| edits?(command, field, moves) }&.fetch("name")
81
+ end
82
+
83
+ def edits?(command, field, moves)
84
+ names = command["attributes"].map { |attr| attr["name"] }
85
+ !command["creates"] && !command["destructive"] && !moves.include?(command["name"]) &&
86
+ !names.empty? && (field.nil? || names.include?(field))
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hecks
4
+ module Projections
5
+ module Site
6
+ module CmsEditor
7
+ # What the `Editor` row's settings for many chapters and long lists must satisfy: chapters
8
+ # named once each, roles given for a chapter the editor edits, and a page size a list can
9
+ # hold. A chapter's name is part of an address, so it cannot be spelled as one of the
10
+ # editor's own paths.
11
+ module ScaleChecks
12
+ # The most instances a list may show to a page.
13
+ MAX_PAGE = 1000
14
+
15
+ # The first segments of the editor's own addresses, which no chapter may be named.
16
+ RESERVED = %w[assets media logout api scheduled].freeze
17
+
18
+ module_function
19
+
20
+ # @param row [Hash{Symbol => String}] the row, defaults filled
21
+ # @return [Array<String>] every problem with `chapter`, `chapter_roles` and `page_size`
22
+ def problems(row)
23
+ chapters = row[:chapter].split(",").map(&:strip)
24
+ [*chapter_problems(chapters), *role_problems(row[:chapter_roles], chapters), *page_size_problems(row[:page_size])]
25
+ end
26
+
27
+ # @return [Array<String>] the problems with an empty, repeated or reserved chapter name
28
+ def chapter_problems(chapters)
29
+ found = []
30
+ found << "chapter names no chapter" if chapters.reject(&:empty?).empty?
31
+ repeated = chapters.find { |name| chapters.count(name) > 1 }
32
+ found << "chapter names #{repeated.inspect} twice" if repeated
33
+ [*found, *reserved_problems(chapters)]
34
+ end
35
+
36
+ # @return [Array<String>] a problem for each chapter spelled as an address of the editor's
37
+ def reserved_problems(chapters)
38
+ return [] if chapters.size < 2
39
+
40
+ (chapters & RESERVED).map do |name|
41
+ "chapter #{name.inspect} is the first part of an address the editor serves; name it another way"
42
+ end
43
+ end
44
+
45
+ # @return [Array<String>] the problems with `Chapter=Role,Role;Chapter=Role` entries
46
+ def role_problems(text, chapters)
47
+ text.split(";").map(&:strip).reject(&:empty?).flat_map do |entry|
48
+ name, roles = entry.split("=", 2)
49
+ next ["chapter_roles #{entry.inspect} must read Chapter=Role,Role"] if roles.to_s.strip.empty?
50
+ next [] if chapters.include?(name.strip)
51
+
52
+ ["chapter_roles names #{name.strip.inspect}, which is not a chapter of this editor"]
53
+ end
54
+ end
55
+
56
+ # @return [Array<String>] the problem with a page size that is not a whole 1 to 1000
57
+ def page_size_problems(size)
58
+ return [] if size.match?(/\A[1-9]\d{0,3}\z/) && size.to_i <= MAX_PAGE
59
+
60
+ ["page_size #{size.inspect} must be a whole number from 1 to #{MAX_PAGE}"]
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end