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,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hecks
4
+ module Projections
5
+ module Site
6
+ module CmsEditor
7
+ # An aggregate whose records are actions to run later, found by shape. The editor makes
8
+ # and changes the records and never runs an action: a runner elsewhere does.
9
+ #
10
+ # It has a **subject** (a plain `<kind>:<slug>` key, see `KeyKinds`), an **action** from
11
+ # a closed set (`one_of`), a **due time** that is a moment (see `Moment`), a
12
+ # **lifecycle** whose first state moves to at least two states that move nowhere, and a
13
+ # **creating command** that takes the three. The command that reschedules takes the due
14
+ # time; the one that cancels is a destructive move from the first state to a final one;
15
+ # the reason is the first optional plain text left over; the query takes the subject
16
+ # alone. The first such aggregate is the one.
17
+ module Scheduling
18
+ # What a scheduling aggregate's attributes are called in its record: `subject`,
19
+ # `action`, `due`.
20
+ ROLES = %w[subject action due].freeze
21
+
22
+ # The widgets of an integer that is a moment.
23
+ MOMENTS = %w[date datetime].freeze
24
+
25
+ module_function
26
+
27
+ # @param aggregates [Array<Hash{String => Object}>] the aggregates as `Pickers` leaves
28
+ # them
29
+ # @return [Hash{String => Object}, nil] the scheduling aggregate and its parts; nil when
30
+ # none
31
+ def read(aggregates)
32
+ aggregates.lazy.filter_map { |agg| shape(agg) }.first
33
+ end
34
+
35
+ # @return [Hash{String => Object}, nil] `agg` as the scheduling aggregate, or nil
36
+ def shape(agg)
37
+ parts = parts_of(agg)
38
+ schedule = parts && endings(agg["lifecycle"]).size > 1 && creating(agg, parts)
39
+ return nil unless schedule
40
+
41
+ { "aggregate" => agg["name"], "chapter" => agg["chapter"], **parts, "pending" => agg.dig("lifecycle", "default"),
42
+ "schedule" => schedule["name"], **optional(agg, parts, schedule) }.compact
43
+ end
44
+
45
+ # @return [Hash{String => Object}, nil] the subject, action, actions and due time, or nil
46
+ def parts_of(agg)
47
+ return nil unless agg["lifecycle"]
48
+
49
+ plain = agg["attributes"].reject { |attr| attr["list"] }
50
+ found = [subject_of(agg, plain), plain.find { |attr| attr["options"] }, plain.find { |attr| moment?(attr) }]
51
+ describe(*found) if found.all?
52
+ end
53
+
54
+ # @return [Boolean] whether the attribute is an integer that is a moment
55
+ def moment?(attr) = MOMENTS.include?(attr["widget"])
56
+
57
+ # @return [Hash{String => Object}] the three attributes' names, and the action's members
58
+ def describe(subject, action, due)
59
+ { "subject" => subject["name"], "action" => action["name"], "actions" => action["options"], "due" => due["name"] }
60
+ end
61
+
62
+ # @return [Hash{String => Object}, nil] the attribute that is a key of a kind, the
63
+ # identity last
64
+ def subject_of(agg, plain)
65
+ plain.select { |attr| attr.dig("picker", "kinds") }.min_by { |attr| attr["name"] == agg["identity"] ? 1 : 0 }
66
+ end
67
+
68
+ # @return [Array<String>] the final states the first state moves to: states nothing
69
+ # moves out of
70
+ def endings(lifecycle)
71
+ moves = lifecycle["transitions"]
72
+ first = moves.select { |move| move["from"].include?(lifecycle["default"]) }.map { |move| move["to"] }.uniq
73
+ first - moves.flat_map { |move| move["from"] }
74
+ end
75
+
76
+ # @return [Hash{String => Object}, nil] the creating command that takes the subject,
77
+ # action and due time
78
+ def creating(agg, parts)
79
+ agg["commands"].find { |cmd| cmd["creates"] && takes?(cmd, parts.values_at(*ROLES)) }
80
+ end
81
+
82
+ # @return [Boolean] whether the command takes every one of the named attributes
83
+ def takes?(cmd, names) = (names - cmd["attributes"].map { |attr| attr["name"] }).empty?
84
+
85
+ # @return [Hash{String => Object}] the commands, query and reason that may be missing
86
+ def optional(agg, parts, schedule)
87
+ { "reschedule" => rescheduling(agg, parts), "cancel" => cancelling(agg), "query" => querying(agg, parts),
88
+ "reason" => reason(agg, parts), "generated" => generated?(agg, parts, schedule) }
89
+ end
90
+
91
+ # @return [String, nil] the command on a record, not a lifecycle move, that takes the
92
+ # due time
93
+ def rescheduling(agg, parts)
94
+ agg["commands"].find { |cmd| !cmd["creates"] && !move?(agg, cmd) && takes?(cmd, [parts["due"]]) }&.fetch("name")
95
+ end
96
+
97
+ # @return [String, nil] the destructive lifecycle move from the first state to a final
98
+ # one
99
+ def cancelling(agg)
100
+ agg["commands"].find { |cmd| cmd["destructive"] && ends?(agg, cmd) }&.fetch("name")
101
+ end
102
+
103
+ # @return [String, nil] the query whose one argument is the subject
104
+ def querying(agg, parts)
105
+ agg["queries"].find { |query| query["attributes"].map { |attr| attr["name"] } == [parts["subject"]] }&.fetch("name")
106
+ end
107
+
108
+ # @return [Boolean] whether the command is a lifecycle move
109
+ def move?(agg, cmd) = agg["lifecycle"]["transitions"].any? { |move| move["verb"] == cmd["name"] }
110
+
111
+ # @return [Boolean] whether the command moves a first-state record to a final state
112
+ def ends?(agg, cmd)
113
+ lifecycle = agg["lifecycle"]
114
+ final = endings(lifecycle)
115
+ lifecycle["transitions"].any? do |move|
116
+ move["verb"] == cmd["name"] && move["from"].include?(lifecycle["default"]) && final.include?(move["to"])
117
+ end
118
+ end
119
+
120
+ # @return [String, nil] the first optional plain text attribute no other part uses
121
+ def reason(agg, parts)
122
+ used = [agg["identity"], *parts.values_at(*ROLES)]
123
+ spare = agg["attributes"].find { |attr| attr["optional"] && !used.include?(attr["name"]) && plain_text?(agg, attr) }
124
+ spare&.fetch("name")
125
+ end
126
+
127
+ # @return [Boolean] whether the attribute is one plain text, alone or as a value
128
+ # object's only part
129
+ def plain_text?(agg, attr)
130
+ return false if %w[list picker options widget].any? { |key| attr[key] }
131
+
132
+ parts = agg["valueObjects"][attr["type"]]
133
+ parts ? lone_text?(parts) : attr["kind"] == "text"
134
+ end
135
+
136
+ # @return [Boolean] whether the parts are one plain text
137
+ def lone_text?(parts) = parts.size == 1 && parts.first["kind"] == "text" && !parts.first["list"]
138
+
139
+ # @return [true, nil] whether the creating command takes an identity that is not the
140
+ # subject,
141
+ # which the editor then makes itself
142
+ def generated?(agg, parts, schedule)
143
+ taken = schedule["attributes"].any? { |attr| attr["name"] == agg["identity"] }
144
+ true if taken && agg["identity"] != parts["subject"]
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "attributes"
4
+ require_relative "blocks"
5
+ require_relative "clearing"
6
+ require_relative "composition"
7
+ require_relative "destructive"
8
+ require_relative "drafts"
9
+ require_relative "limits"
10
+ require_relative "media"
11
+ require_relative "pickers"
12
+ require_relative "relations"
13
+ require_relative "scheduling"
14
+
15
+ module Hecks
16
+ module Projections
17
+ module Site
18
+ module CmsEditor
19
+ # What the editor reads of a domain chapter, as plain data: each aggregate with its
20
+ # identity, attributes, value objects, lifecycle, commands and queries. The editor's
21
+ # `schema.ts` is this hash as a typed constant, and nothing else about the domain reaches
22
+ # the generated files, so the output is a pure function of the chapter.
23
+ #
24
+ # An aggregate that registers pictures (see `Media`) is also named under `media`, so the
25
+ # editor can offer an upload; a chapter with none has no such key.
26
+ #
27
+ # A query that `returns` a value object is answered outside the domain and is left out: the
28
+ # host refuses it, so a list cannot be built from it.
29
+ class Schema
30
+ # @param chapters [Bluebook::Chapter, Array<Bluebook::Chapter>] the chapter, or the
31
+ # chapters in the order the row names them; the first names the domain addressed
32
+ # @param skip [Array<String>] aggregates to leave out: `Name`, or `Chapter::Name`
33
+ # @param pictures [Bluebook::Chapter, nil] another chapter whose picture aggregate the
34
+ # editor's pictures use, in place of any in `chapters`
35
+ # @param roles [Hash{String => Array<String>}] the roles each chapter is limited to
36
+ # @return [Hash{String => Object}] the domain's name and its aggregates; `chapters` too
37
+ # when there are several
38
+ # @raise [ArgumentError] when no aggregate is left, or `pictures` has no picture aggregate
39
+ def self.read(chapters, skip: [], pictures: nil, roles: {})
40
+ list = chapters.is_a?(Array) ? chapters : [chapters]
41
+ shaped = Composition.aggregates(list, skip: skip, roles: roles) { |agg| new(agg).to_h }
42
+ raise ArgumentError, "the #{list.map(&:name).join(", ")} chapter has no aggregate to edit" if shaped.empty?
43
+
44
+ media = pictures ? elsewhere(pictures) : within(shaped, list.first.name)
45
+ assemble(list, Relations.apply(Pickers.apply(shaped, media), media), media)
46
+ end
47
+
48
+ # @return [Hash{String => Object}] the schema: the domain, the chapters when several, the
49
+ # aggregates, and the picture and scheduling aggregates when there are such
50
+ def self.assemble(list, aggregates, media)
51
+ chapters = list.size > 1 ? { "chapters" => list.map(&:name) } : {}
52
+ scheduling = Scheduling.read(aggregates)
53
+ { "domain" => list.first.name, **chapters, "aggregates" => aggregates, **(media ? { "media" => media } : {}),
54
+ **(scheduling ? { "scheduling" => scheduling } : {}) }
55
+ end
56
+
57
+ # The picture aggregate among the editor's own, with the chapter that holds it when that
58
+ # is not the first, since the host is addressed by chapter.
59
+ #
60
+ # @return [Hash{String => Object}, nil] the picture aggregate, as `Media` describes it
61
+ def self.within(shaped, first)
62
+ found = Media.read(shaped)
63
+ chapter = found&.delete("chapter")
64
+ chapter && chapter != first ? found.merge("domain" => chapter) : found
65
+ end
66
+
67
+ # The picture aggregate of another chapter: the same record as one in the editor's own
68
+ # chapter, with the chapter's name and the aggregate itself, since the editor's own
69
+ # aggregates do not hold it.
70
+ #
71
+ # @return [Hash{String => Object}] the picture aggregate, as `Media` describes it
72
+ # @raise [ArgumentError] when the chapter has no aggregate that registers pictures
73
+ def self.elsewhere(chapter)
74
+ shaped = chapter.aggregates.map { |agg| new(agg).to_h }
75
+ found = Media.read(shaped)
76
+ raise ArgumentError, "the #{chapter.name} chapter has no aggregate that registers pictures" unless found
77
+
78
+ found.merge("domain" => chapter.name, "definition" => shaped.find { |agg| agg["name"] == found["aggregate"] })
79
+ end
80
+
81
+ # @param aggregate [Bluebook::Aggregate] one aggregate of the chapter
82
+ def initialize(aggregate)
83
+ @agg = aggregate
84
+ @attributes = Attributes.new(aggregate.value_objects)
85
+ end
86
+
87
+ # @return [Hash{String => Object}] the aggregate as the editor reads it
88
+ def to_h
89
+ held = aggregate_attributes
90
+ drafts = Drafts.of(@agg, held)
91
+ { "name" => @agg.hecks_name, "description" => @agg.description, "identity" => @agg.identified_by.to_s,
92
+ "lifecycle" => lifecycle, "attributes" => held, "valueObjects" => value_objects,
93
+ "commands" => @agg.commands.map { |command| command(command) }, "queries" => queries,
94
+ **(drafts ? { "drafts" => drafts } : {}) }
95
+ end
96
+
97
+ private
98
+
99
+ def lifecycle_field = @agg.lifecycle&.field
100
+
101
+ # The aggregate's own attributes, bounded by the aggregate's invariants (see `Limits`).
102
+ def aggregate_attributes
103
+ bounded(attributes(@agg.attributes.reject { |a| a.name == lifecycle_field }), @agg.invariants)
104
+ end
105
+
106
+ def attributes(list) = list.map { |attribute| limited(@attributes.of(attribute)) }
107
+
108
+ # `list` with what the holder's rules say about the most each may hold (see `Limits`).
109
+ def bounded(list, rules) = Limits.apply(list, rules)
110
+
111
+ # `attr` with the most its block list may hold, when a command's `given` says.
112
+ def limited(attr)
113
+ most = attr.key?("blockList") ? Blocks.maximum(@agg.commands, attr["name"]) : nil
114
+ most ? attr.merge("max" => most) : attr
115
+ end
116
+
117
+ def value_objects
118
+ @agg.value_objects.to_h do |object|
119
+ [object.hecks_name, bounded(attributes(object.attributes), object.invariants)]
120
+ end
121
+ end
122
+
123
+ def lifecycle
124
+ @lifecycle ||= shaped_lifecycle
125
+ end
126
+
127
+ def shaped_lifecycle
128
+ lifecycle = @agg.lifecycle
129
+ return nil unless lifecycle
130
+
131
+ shaped = { "field" => lifecycle.field.to_s, "default" => lifecycle.default,
132
+ "transitions" => lifecycle.transitions.map { |verb, move| transition(verb, move) } }
133
+ shaped.merge("tones" => Destructive.tones(shaped))
134
+ end
135
+
136
+ def transition(verb, move)
137
+ { "verb" => verb, "from" => Array(move.from), "to" => move.target }
138
+ end
139
+
140
+ def command(command)
141
+ { "name" => command.hecks_name, "goal" => command.goal, "role" => command.role, "creates" => command.creates?,
142
+ "on" => command.creates? ? nil : command.references.to_s,
143
+ "destructive" => Destructive.command?(command.hecks_name, lifecycle),
144
+ "attributes" => bounded(attributes(command.attributes), command.givens) }
145
+ .then { |shaped| clearing(shaped, command) }
146
+ end
147
+
148
+ # Leaves the arguments that only clear (see `Clearing`) out of the command's form, and
149
+ # names those that are lists as `empty`, for the editor to send as empty lists.
150
+ def clearing(shaped, command) = Clearing.apply(shaped, Clearing.of(command, @agg))
151
+
152
+ def queries
153
+ @agg.queries.reject(&:returns).map do |query|
154
+ { "name" => query.name, "description" => query.description, "attributes" => attributes(query.attributes) }
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../root_rows"
5
+ require_relative "../routes_ts"
6
+ require_relative "checks"
7
+ require_relative "preview"
8
+ require_relative "../../../version"
9
+
10
+ module Hecks
11
+ module Projections
12
+ module Site
13
+ module CmsEditor
14
+ # The project's `Editor` row, checked: which domain chapter the editor edits, where the
15
+ # editor is served, the session cookie it keeps, and the host and sign-in it reads.
16
+ #
17
+ # `docs/site-routes.md` lists the fields. A project that declares no row has no editor to
18
+ # project.
19
+ class Setting
20
+ # The `Editor` row: its fields, which are required and what the others default to.
21
+ ROW = RootRows.new(
22
+ "Editor",
23
+ fields: { domain: String, chapter: String, base_path: String, sso_path: String, session_cookie: String,
24
+ host_cookie: String, host_env: String, host_default: String, roles: String, login: String,
25
+ title: String, brand: String, accent: String, logo: String, skip: String, media: String,
26
+ media_dir: String, media_max_bytes: String, chapter_roles: String, page_size: [String, Integer],
27
+ preview: String },
28
+ required: %i[domain chapter host_env login],
29
+ defaults: { base_path: "/editor", session_cookie: "hecks_editor", host_cookie: "hecks_session",
30
+ host_default: "http://127.0.0.1:4322", roles: "Admin,Owner", title: "Editor", brand: "",
31
+ accent: "", logo: "", skip: "", media: "", media_dir: "media", media_max_bytes: "5242880",
32
+ chapter_roles: "", page_size: "25", preview: "" }
33
+ )
34
+
35
+ # @return [Hash{Symbol => String}] the checked row, defaults filled
36
+ attr_reader :row
37
+
38
+ # Reads and checks the row a chapter declares.
39
+ #
40
+ # @param chapter [Bluebook::Chapter] the chapter that declares the route table
41
+ # @param table [Table, nil] the checked route table, to check the login page against
42
+ # @return [Setting, nil] the checked setting, or nil when the chapter declares no row
43
+ # @raise [Table::Invalid] when the row is refused
44
+ def self.read(chapter, table: nil)
45
+ row = ROW.read(chapter).first
46
+ row && new(row, table: table)
47
+ end
48
+
49
+ # @param row [Hash{Symbol => String}] the row, defaults filled
50
+ # @param table [Table, nil] the checked route table
51
+ # @raise [Table::Invalid] naming every problem when there is one
52
+ def initialize(row, table: nil)
53
+ @row = { sso_path: "#{row[:base_path]}/api/sso" }.merge(row, page_size: row[:page_size].to_s)
54
+ problems = Checks.problems(@row, roles, table)
55
+ return if problems.empty?
56
+
57
+ raise Table::Invalid, "the Editor row is refused:\n#{problems.map { |line| " - #{line}" }.join("\n")}"
58
+ end
59
+
60
+ # @return [String] the domain's directory, relative to the project
61
+ def domain = row.fetch(:domain)
62
+
63
+ # @return [String] the first chapter the editor edits, which names the generated package
64
+ def chapter = chapters.first.to_s
65
+
66
+ # @return [Array<String>] every chapter the editor edits, in the order the row names them:
67
+ # `chapter` is one name or several, comma separated
68
+ def chapters = row.fetch(:chapter).split(",").map(&:strip).reject(&:empty?)
69
+
70
+ # @return [Hash{String => Array<String>}] the roles a chapter's aggregates are limited to,
71
+ # from `Chapter=Role,Role;Chapter=Role`; a chapter not named is open to every role the
72
+ # editor admits
73
+ def chapter_roles
74
+ row.fetch(:chapter_roles).split(";").map(&:strip).reject(&:empty?).to_h do |entry|
75
+ name, roles = entry.split("=", 2)
76
+ [name.to_s.strip, roles.to_s.split(",").map(&:strip).reject(&:empty?)]
77
+ end
78
+ end
79
+
80
+ # @return [String, nil] the URL template a record is previewed at, or nil for no preview
81
+ def preview = row.fetch(:preview).strip.then { |template| template.empty? ? nil : template }
82
+
83
+ # @return [Integer] how many instances a list shows to a page
84
+ def page_size = row.fetch(:page_size).to_i
85
+
86
+ # @return [String, nil] the other chapter whose picture aggregate the editor's pictures
87
+ # use, or nil when the pictures (if any) are in the editor's own chapter
88
+ def media_chapter = row.fetch(:media).strip.then { |name| name.empty? ? nil : name }
89
+
90
+ # @return [String, nil] the product name the header shows, or nil for the domain's name
91
+ def brand = row.fetch(:brand).strip.then { |name| name.empty? ? nil : name }
92
+
93
+ # @return [String, nil] the accent colour as hex, or nil for the default theme
94
+ def accent = row.fetch(:accent).strip.then { |hex| hex.empty? ? nil : hex }
95
+
96
+ # @return [Array<String>] the aggregates left out: `Name`, or `Chapter::Name`
97
+ def skip = row.fetch(:skip).split(",").map(&:strip).reject(&:empty?)
98
+
99
+ # @return [Array<String>] the roles that count as an editor
100
+ def roles = row.fetch(:roles).split(",").map(&:strip).reject(&:empty?)
101
+
102
+ # @return [String] the generated package's name
103
+ def package = "#{chapter.gsub(/([a-z])([A-Z])/, '\1-\2').tr("_", "-").downcase}-editor"
104
+
105
+ # @return [Hash{String => String}] each placeholder to the text that replaces it
106
+ def tokens
107
+ { "__BANNER__" => RoutesTs::BANNER, "__PACKAGE__" => package, "__ROLES__" => RoutesTs.literal(roles),
108
+ "__MEDIA_MAX_BYTES__" => row.fetch(:media_max_bytes), "__PAGE_SIZE__" => page_size.to_s,
109
+ "__CLIENT_VERSION__" => Hecks::VERSION, **preview_tokens, **quoted }
110
+ end
111
+
112
+ # @return [Hash{String => String}] the preview template as a literal, and the policy line
113
+ # that lets the editor frame its origin, which is empty when there is no preview
114
+ def preview_tokens
115
+ source = Preview.frame_source(row.fetch(:preview).strip)
116
+ { "__PREVIEW__" => JSON.generate(preview), "__FRAME_POLICY__" => source ? " \"frame-src #{source}\"," : "" }
117
+ end
118
+
119
+ # @return [Hash{String => String}] each text field's placeholder to the field as JSON
120
+ def quoted
121
+ { "TITLE" => :title, "BASE_PATH" => :base_path, "SSO_PATH" => :sso_path, "LOGIN" => :login,
122
+ "SESSION_COOKIE" => :session_cookie, "HOST_COOKIE" => :host_cookie, "HOST_ENV" => :host_env,
123
+ "HOST_DEFAULT" => :host_default, "MEDIA_DIR" => :media_dir, "LOGO" => :logo }
124
+ .to_h { |token, field| ["__#{token}__", JSON.generate(row.fetch(field))] }
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hecks
4
+ module Projections
5
+ module Site
6
+ module CmsEditor
7
+ # The aggregates an attribute can name, found among the aggregates of one editor, and what
8
+ # a picker needs to know about each: its key, the attribute that labels it and the query
9
+ # that lists the ones to offer.
10
+ class Targets
11
+ # The attribute names that label an instance, in the order they are tried; an aggregate
12
+ # with none is labelled by its identity.
13
+ LABELS = %w[title name label].freeze
14
+
15
+ # The zero-argument queries that list what is on offer, tried in this order: ones that
16
+ # list everything, then the one that lists what is in use.
17
+ LISTINGS = %w[all list listing active].freeze
18
+
19
+ # @param aggregates [Array<Hash{String => Object}>] the aggregates as `Schema` shapes them
20
+ # @param media [Hash{String => Object}, nil] the picture aggregate, as `Media` says
21
+ def initialize(aggregates, media = nil)
22
+ @aggregates = aggregates
23
+ @media = media
24
+ end
25
+
26
+ # @return [Hash{String => Object}, nil] the aggregate named `name`, as a picker names it
27
+ def named(name, from)
28
+ pick(@aggregates.select { |agg| agg["name"] == name }, from)
29
+ end
30
+
31
+ # @return [Hash{String => Object}, nil] the aggregate whose identity is of type `stem`, or
32
+ # that is named `stem`, as a picker names it
33
+ def keyed_by(stem, from)
34
+ pick(@aggregates.select { |agg| identity_type(agg) == stem }, from) || named(stem, from)
35
+ end
36
+
37
+ # @return [Hash{String => Object}, nil] the aggregate that is `word` in snake case
38
+ def kind(word, from)
39
+ pick(@aggregates.select { |agg| snake(agg["name"]) == word }, from)
40
+ end
41
+
42
+ # @return [Hash{String => Object}, nil] the picture aggregate, which offers keys and
43
+ # accepts any other text as well, since a picture may be named by an address
44
+ def pictures
45
+ return nil unless @media
46
+
47
+ agg = @media["definition"] || @aggregates.find { |candidate| candidate["name"] == @media["aggregate"] }
48
+ return nil unless agg
49
+
50
+ described = describe(agg, agg["chapter"] || @media["domain"], @media["listing"])
51
+ described.merge("label" => @media.dig("fields", "alt", "name"), "strict" => false)
52
+ end
53
+
54
+ private
55
+
56
+ def pick(found, from)
57
+ own = found.select { |agg| agg["chapter"] == from["chapter"] }
58
+ chosen = own.first || (found.first if found.size == 1)
59
+ chosen && describe(chosen, chosen["chapter"], listing(chosen)).merge("strict" => true)
60
+ end
61
+
62
+ def describe(agg, chapter, listing)
63
+ { "target" => agg["name"], "chapter" => chapter, "key" => agg["identity"], "label" => label(agg),
64
+ "listing" => listing }.compact
65
+ end
66
+
67
+ def identity_type(agg)
68
+ agg["attributes"].find { |attr| attr["name"] == agg["identity"] }&.fetch("type", nil)
69
+ end
70
+
71
+ def label(agg)
72
+ plain = agg["attributes"].reject { |attr| attr["list"] || attr["widget"] == "body" }.map { |attr| attr["name"] }
73
+ LABELS.find { |name| plain.include?(name) } || agg["identity"]
74
+ end
75
+
76
+ def listing(agg)
77
+ named = agg["queries"].select { |query| query["attributes"].empty? }.map { |query| query["name"] }
78
+ LISTINGS.filter_map { |word| named.find { |name| name.downcase == word } }.first
79
+ end
80
+
81
+ def snake(name) = name.gsub(/([a-z0-9])([A-Z])/, "\\1_\\2").downcase
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "__PACKAGE__",
3
+ "private": true,
4
+ "type": "module",
5
+ "engines": {
6
+ "node": ">=22.18"
7
+ },
8
+ "scripts": {
9
+ "clean": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"",
10
+ "build": "npm run clean && npm run build:css && npm run build:js",
11
+ "build:css": "tailwindcss --input src/browser/app.css --output dist/editor.css --minify",
12
+ "build:js": "esbuild src/browser/main.js --bundle --splitting --format=esm --target=es2022 --minify --outdir=dist --entry-names=editor --chunk-names=[name]-[hash]",
13
+ "prestart": "npm run build",
14
+ "start": "node src/server.ts",
15
+ "check": "tsc -p tsconfig.json"
16
+ },
17
+ "dependencies": {
18
+ "@hecks/client": "^__CLIENT_VERSION__",
19
+ "@tiptap/core": "3.31.4",
20
+ "@tiptap/extension-blockquote": "3.31.4",
21
+ "@tiptap/extension-image": "3.31.4",
22
+ "@tiptap/extensions": "3.31.4",
23
+ "@tiptap/pm": "3.31.4",
24
+ "@tiptap/starter-kit": "3.31.4",
25
+ "tiptap-extension-flat-list": "0.4.0"
26
+ },
27
+ "devDependencies": {
28
+ "@tailwindcss/cli": "4.3.3",
29
+ "@tailwindcss/typography": "0.5.20",
30
+ "@types/node": "^22.10.0",
31
+ "daisyui": "5.7.47",
32
+ "esbuild": "0.28.2",
33
+ "tailwindcss": "4.3.3",
34
+ "typescript": "^5.8.0"
35
+ }
36
+ }