hecks 3.10.0 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/bluebook/pizzas.bluebook +29 -0
  3. data/examples/pizzas/bluebook/pizzas.hecksagon +7 -0
  4. data/exe/hecks +4 -4
  5. data/lib/hecks/adapters/driven/d1.rb +2 -10
  6. data/lib/hecks/adapters/driven/heki.adapter +1 -1
  7. data/lib/hecks/adapters/driven/heki.rb +2 -12
  8. data/lib/hecks/adapters/driven/lambda.adapter +1 -1
  9. data/lib/hecks/adapters/driven/lambda.rb +3 -18
  10. data/lib/hecks/adapters/driven/postgres.rb +4 -11
  11. data/lib/hecks/adapters/driven/sqlite.adapter +1 -1
  12. data/lib/hecks/adapters/driven/sqlite.rb +2 -10
  13. data/lib/hecks/adapters/driving/admission.rb +46 -0
  14. data/lib/hecks/adapters/driving/cli/nesting.rb +36 -0
  15. data/lib/hecks/adapters/driving/cli/shorthand.rb +88 -0
  16. data/lib/hecks/adapters/driving/cli.rb +127 -0
  17. data/lib/hecks/adapters/driving/cli_runner/answers.rb +160 -0
  18. data/lib/hecks/adapters/driving/cli_runner/resolution.rb +155 -0
  19. data/lib/hecks/adapters/driving/cli_runner/settling.rb +120 -0
  20. data/lib/hecks/adapters/driving/cli_runner/streaming.rb +93 -0
  21. data/lib/hecks/adapters/driving/cli_runner/suggestions.rb +47 -0
  22. data/lib/hecks/adapters/driving/cli_runner.rb +69 -0
  23. data/lib/hecks/adapters/driving/command_request.rb +121 -0
  24. data/lib/hecks/adapters/driving/handle.rb +214 -0
  25. data/lib/hecks/adapters/driving/json.rb +136 -0
  26. data/lib/hecks/adapters/driving/launcher_options.rb +267 -0
  27. data/lib/hecks/adapters/driving/mcp/properties.rb +49 -0
  28. data/lib/hecks/adapters/driving/mcp/tool_calls.rb +116 -0
  29. data/lib/hecks/adapters/driving/mcp/tool_definitions/auditing.rb +53 -0
  30. data/lib/hecks/adapters/driving/mcp/tool_definitions/calling.rb +71 -0
  31. data/lib/hecks/adapters/driving/mcp/tool_definitions/reading.rb +99 -0
  32. data/lib/hecks/adapters/driving/mcp/tool_definitions.rb +18 -0
  33. data/lib/hecks/adapters/driving/mcp.rb +159 -0
  34. data/lib/hecks/adapters/driving/mcp_guide_cache.rb +78 -0
  35. data/lib/hecks/adapters/driving/mcp_scope/argument_policy.rb +97 -0
  36. data/lib/hecks/adapters/driving/mcp_scope/env_problems.rb +69 -0
  37. data/lib/hecks/adapters/driving/mcp_scope/presenter.rb +83 -0
  38. data/lib/hecks/adapters/driving/mcp_scope.rb +213 -0
  39. data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +171 -0
  40. data/lib/hecks/adapters/driving/ruby/chapter.rb +91 -0
  41. data/lib/hecks/adapters/driving/ruby.rb +83 -0
  42. data/lib/hecks/adapters/driving/usage_cache.rb +144 -0
  43. data/lib/hecks/{doors.rb → adapters/driving.rb} +10 -6
  44. data/lib/hecks/behaviors/expectations/boot.rb +1 -1
  45. data/lib/hecks/bench/ruby_runner.rb +1 -1
  46. data/lib/hecks/bluebook/ask_resolution.rb +130 -0
  47. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +8 -2
  48. data/lib/hecks/bluebook/assembly/contracts/rules.rb +4 -2
  49. data/lib/hecks/bluebook/assembly/contracts/structure.rb +3 -1
  50. data/lib/hecks/bluebook/assembly/marks.rb +1 -0
  51. data/lib/hecks/bluebook/attribute.rb +1 -1
  52. data/lib/hecks/bluebook/behaviour/command.rb +13 -8
  53. data/lib/hecks/bluebook/behaviour/policy.rb +30 -0
  54. data/lib/hecks/bluebook/capabilities/attribute_values.rb +72 -0
  55. data/lib/hecks/bluebook/capabilities.rb +91 -9
  56. data/lib/hecks/bluebook/domain_port.rb +16 -4
  57. data/lib/hecks/bluebook/dsl/adapter_builder.rb +18 -6
  58. data/lib/hecks/bluebook/dsl/binding_proxy.rb +13 -0
  59. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/computed_sources.rb +98 -0
  60. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/fan_out_rows.rb +42 -0
  61. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/projected_fields.rb +14 -2
  62. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/provisions.rb +69 -3
  63. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/with_projections.rb +17 -10
  64. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +5 -0
  65. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
  66. data/lib/hecks/bluebook/dsl/command_builder/mutations.rb +8 -1
  67. data/lib/hecks/bluebook/dsl/command_builder/operands.rb +121 -0
  68. data/lib/hecks/bluebook/dsl/command_builder.rb +2 -0
  69. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +10 -1
  70. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +45 -2
  71. data/lib/hecks/bluebook/dsl/policy_builder.rb +47 -10
  72. data/lib/hecks/bluebook/dsl/rule_reference.rb +7 -4
  73. data/lib/hecks/bluebook/dsl/world_builder.rb +1 -1
  74. data/lib/hecks/bluebook/expression/ast_json/emitters.rb +6 -0
  75. data/lib/hecks/bluebook/expression/ast_json.rb +9 -6
  76. data/lib/hecks/bluebook/expression/ast_reader.rb +14 -9
  77. data/lib/hecks/bluebook/expression/projection.json +36 -0
  78. data/lib/hecks/bluebook/expression/resolver/operations.rb +31 -0
  79. data/lib/hecks/bluebook/expression/resolver/parsing.rb +38 -5
  80. data/lib/hecks/bluebook/expression/resolver/scanning.rb +22 -5
  81. data/lib/hecks/bluebook/expression/resolver/values.rb +10 -0
  82. data/lib/hecks/bluebook/expression/resolver.rb +20 -1
  83. data/lib/hecks/bluebook/hecksagon.rb +23 -3
  84. data/lib/hecks/bluebook/lifecycle.rb +22 -3
  85. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +13 -1
  86. data/lib/hecks/bluebook/meta_validator/modes.rb +4 -1
  87. data/lib/hecks/bluebook/meta_validator/readings.rb +18 -1
  88. data/lib/hecks/bluebook/meta_validator/reconstruction/heads.rb +2 -1
  89. data/lib/hecks/bluebook/meta_validator/shapes.rb +5 -0
  90. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +1 -1
  91. data/lib/hecks/bluebook/meta_validator.rb +12 -12
  92. data/lib/hecks/bluebook/model_check/ask_checks.rb +71 -0
  93. data/lib/hecks/bluebook/model_check/policy_checks.rb +2 -1
  94. data/lib/hecks/bluebook/model_check/reference_checks.rb +70 -0
  95. data/lib/hecks/bluebook/model_check.rb +8 -2
  96. data/lib/hecks/bluebook/policy.rb +4 -2
  97. data/lib/hecks/bluebook/smoke_test.rb +1 -1
  98. data/lib/hecks/bluebook/synthesizer.rb +2 -0
  99. data/lib/hecks/bluebook.rb +1 -0
  100. data/lib/hecks/cli/command_table.rb +1 -1
  101. data/lib/hecks/cli/console.rb +2 -1
  102. data/lib/hecks/cli/document.rb +1 -1
  103. data/lib/hecks/cli/launcher_words.rb +4 -4
  104. data/lib/hecks/cli/mcp.rb +4 -4
  105. data/lib/hecks/cli/project_cli/launcher_source.rb +2 -2
  106. data/lib/hecks/cli/project_cli/templates.rb +5 -5
  107. data/lib/hecks/cli/project_cli.rb +9 -6
  108. data/lib/hecks/cli/run.rb +2 -2
  109. data/lib/hecks/cli/smoke_http/checks.rb +4 -1
  110. data/lib/hecks/cli/smoke_http/transport.rb +3 -3
  111. data/lib/hecks/cli/smoke_http.rb +3 -0
  112. data/lib/hecks/codemod.rb +5 -2
  113. data/lib/hecks/corpus/routes.rb +7 -0
  114. data/lib/hecks/corpus/rust.rb +3 -2
  115. data/lib/hecks/deploy/bluebook/deploy.bluebook +18 -2
  116. data/lib/hecks/doc/reference/rendering.rb +6 -3
  117. data/lib/hecks/forms/app/commands.rb +8 -7
  118. data/lib/hecks/forms/app/queries.rb +2 -1
  119. data/lib/hecks/forms/app.rb +1 -1
  120. data/lib/hecks/forms/record_renderer.rb +1 -1
  121. data/lib/hecks/framework/bluebook/privacy.bluebook +11 -0
  122. data/lib/hecks/fuzzing/concurrent_dispatch.rb +4 -1
  123. data/lib/hecks/fuzzing/failure_triage.rb +2 -1
  124. data/lib/hecks/fuzzing/mutation/run.rb +11 -4
  125. data/lib/hecks/fuzzing/properties/catalog.rb +6 -4
  126. data/lib/hecks/fuzzing/properties/guaranteed_by_construction.rb +18 -11
  127. data/lib/hecks/fuzzing/properties/guards.rb +1 -1
  128. data/lib/hecks/fuzzing/self_consistency/saga_redelivery.rb +1 -1
  129. data/lib/hecks/fuzzing/self_consistency.rb +6 -2
  130. data/lib/hecks/fuzzing/shrinker.rb +3 -1
  131. data/lib/hecks/fuzzing/value_generator.rb +19 -5
  132. data/lib/hecks/gate/stages.yml +4 -10
  133. data/lib/hecks/grammar/evolve.rb +1 -1
  134. data/lib/hecks/grammar/expression_operators.json +372 -0
  135. data/lib/hecks/grammar/operators.rb +3 -1
  136. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +10 -10
  137. data/lib/hecks/hecks/adapters/codebase/inspection/rubycritic.rb +35 -0
  138. data/lib/hecks/hecks/adapters/codebase/inspection.rb +104 -0
  139. data/lib/hecks/hecks/adapters/codebase/publishing.rb +2 -1
  140. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +12 -2
  141. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +2 -1
  142. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +1 -1
  143. data/lib/hecks/hecks/adapters/cost_explorer.rb +8 -5
  144. data/lib/hecks/hecks/adapters/deploy_toolchain/statuses.rb +2 -1
  145. data/lib/hecks/hecks/adapters/finding_github.rb +13 -6
  146. data/lib/hecks/hecks/adapters/first_admin.rb +2 -2
  147. data/lib/hecks/hecks/adapters/github_rulesets.rb +6 -3
  148. data/lib/hecks/hecks/adapters/in_process_boot.rb +1 -1
  149. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +1 -1
  150. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +10 -20
  151. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +28 -5
  152. data/lib/hecks/hecks/adapters/local_files.rb +2 -2
  153. data/lib/hecks/hecks/adapters/rehearsal/audit_report.rb +107 -0
  154. data/lib/hecks/hecks/adapters/rehearsal/census.rb +71 -0
  155. data/lib/hecks/hecks/adapters/rehearsal/declaration.rb +133 -0
  156. data/lib/hecks/hecks/adapters/rehearsal/environment.rb +83 -0
  157. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +142 -0
  158. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +96 -0
  159. data/lib/hecks/hecks/adapters/rehearsal/postgres_census.rb +100 -0
  160. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +69 -0
  161. data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +129 -0
  162. data/lib/hecks/hecks/adapters/rehearsal/rows.rb +41 -0
  163. data/lib/hecks/hecks/adapters/rehearsal/rust_audit.rb +83 -0
  164. data/lib/hecks/hecks/adapters/rehearsal/subject.rb +104 -0
  165. data/lib/hecks/hecks/adapters/rehearsal.rb +54 -0
  166. data/lib/hecks/hecks/adapters/rehearsal_store.adapter +7 -0
  167. data/lib/hecks/hecks/adapters/rehearsal_store.rb +81 -0
  168. data/lib/hecks/hecks/adapters/terminal.rb +13 -11
  169. data/lib/hecks/hecks/codebase.behaviors +13 -1
  170. data/lib/hecks/hecks/codebase.bluebook +239 -5
  171. data/lib/hecks/hecks/custodian.behaviors +24 -6
  172. data/lib/hecks/hecks/custodian.bluebook +6 -1227
  173. data/lib/hecks/hecks/deploy.behaviors +1 -1
  174. data/lib/hecks/hecks/deploy.world +1 -3
  175. data/lib/hecks/hecks/hecks.behaviors +1 -1
  176. data/lib/hecks/hecks/hecks.hecksagon +53 -18
  177. data/lib/hecks/hecks/hecks.world +10 -10
  178. data/lib/hecks/hecks/rehearsal.bluebook +477 -0
  179. data/lib/hecks/hecks/site.behaviors +1 -1
  180. data/lib/hecks/hecks/site.world +0 -1
  181. data/lib/hecks/hecks/tooling.bluebook +1238 -0
  182. data/lib/hecks/indifferent_key.rb +20 -0
  183. data/lib/hecks/language/adapter.bluebook +13 -3
  184. data/lib/hecks/language/bluebook/aggregate.bluebook +23 -0
  185. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -0
  186. data/lib/hecks/language/bluebook/entity.bluebook +18 -0
  187. data/lib/hecks/language/bluebook/policy.bluebook +9 -1
  188. data/lib/hecks/language/bluebook/query.bluebook +1 -1
  189. data/lib/hecks/language/bluebook/vocabulary.bluebook +11 -4
  190. data/lib/hecks/language/hecksagon/domain_port.bluebook +1 -1
  191. data/lib/hecks/language/hecksagon/hecksagon.bluebook +22 -0
  192. data/lib/hecks/language/oidc.json +10 -0
  193. data/lib/hecks/language/port.bluebook +4 -4
  194. data/lib/hecks/literal.rb +12 -3
  195. data/lib/hecks/ports/key_vault.rb +7 -2
  196. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +21 -10
  197. data/lib/hecks/ports/persistence/plugins/era/postgres_era/boot.rb +4 -2
  198. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +2 -1
  199. data/lib/hecks/ports/persistence/repository_factory.rb +6 -3
  200. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +29 -0
  201. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +1 -1
  202. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  203. data/lib/hecks/projections/deploy/shared.rb +5 -2
  204. data/lib/hecks/projections/model/deviations.rb +2 -2
  205. data/lib/hecks/projections/model.rb +2 -2
  206. data/lib/hecks/projections/oidc.rb +13 -1
  207. data/lib/hecks/projections/shape.rb +5 -2
  208. data/lib/hecks/projections/site/cms_editor/attributes.rb +26 -6
  209. data/lib/hecks/projections/site/cms_editor/blocks.rb +157 -0
  210. data/lib/hecks/projections/site/cms_editor/checks.rb +14 -1
  211. data/lib/hecks/projections/site/cms_editor/clearing.rb +70 -0
  212. data/lib/hecks/projections/site/cms_editor/color.rb +124 -0
  213. data/lib/hecks/projections/site/cms_editor/composition.rb +42 -0
  214. data/lib/hecks/projections/site/cms_editor/destructive.rb +61 -0
  215. data/lib/hecks/projections/site/cms_editor/drafts.rb +102 -0
  216. data/lib/hecks/projections/site/cms_editor/hints.rb +74 -0
  217. data/lib/hecks/projections/site/cms_editor/icons.rb +86 -0
  218. data/lib/hecks/projections/site/cms_editor/key_kinds.rb +31 -0
  219. data/lib/hecks/projections/site/cms_editor/limits.rb +72 -0
  220. data/lib/hecks/projections/site/cms_editor/media.rb +1 -1
  221. data/lib/hecks/projections/site/cms_editor/moment.rb +34 -0
  222. data/lib/hecks/projections/site/cms_editor/palette.rb +115 -0
  223. data/lib/hecks/projections/site/cms_editor/pickers.rb +85 -0
  224. data/lib/hecks/projections/site/cms_editor/preview.rb +91 -0
  225. data/lib/hecks/projections/site/cms_editor/relations.rb +92 -0
  226. data/lib/hecks/projections/site/cms_editor/scale_checks.rb +66 -0
  227. data/lib/hecks/projections/site/cms_editor/scheduling.rb +150 -0
  228. data/lib/hecks/projections/site/cms_editor/schema.rb +96 -19
  229. data/lib/hecks/projections/site/cms_editor/setting.rb +52 -9
  230. data/lib/hecks/projections/site/cms_editor/targets.rb +86 -0
  231. data/lib/hecks/projections/site/cms_editor/templates/package.json.tmpl +18 -1
  232. data/lib/hecks/projections/site/cms_editor/templates/src/app.ts.tmpl +251 -57
  233. data/lib/hecks/projections/site/cms_editor/templates/src/assets.ts.tmpl +98 -0
  234. data/lib/hecks/projections/site/cms_editor/templates/src/auth/membership.ts.tmpl +32 -24
  235. data/lib/hecks/projections/site/cms_editor/templates/src/browser/app.css.tmpl +412 -0
  236. data/lib/hecks/projections/site/cms_editor/templates/src/browser/autosave.js.tmpl +144 -0
  237. data/lib/hecks/projections/site/cms_editor/templates/src/browser/blocks.js.tmpl +500 -0
  238. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_doc.js.tmpl +175 -0
  239. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_editor.js.tmpl +418 -0
  240. data/lib/hecks/projections/site/cms_editor/templates/src/browser/epoch.js.tmpl +61 -0
  241. data/lib/hecks/projections/site/cms_editor/templates/src/browser/forms.js.tmpl +210 -0
  242. data/lib/hecks/projections/site/cms_editor/templates/src/browser/lists.js.tmpl +40 -0
  243. data/lib/hecks/projections/site/cms_editor/templates/src/browser/main.js.tmpl +26 -0
  244. data/lib/hecks/projections/site/cms_editor/templates/src/browser/media_picker.js.tmpl +232 -0
  245. data/lib/hecks/projections/site/cms_editor/templates/src/browser/moments.js.tmpl +73 -0
  246. data/lib/hecks/projections/site/cms_editor/templates/src/browser/pickers.js.tmpl +152 -0
  247. data/lib/hecks/projections/site/cms_editor/templates/src/browser/preview.js.tmpl +98 -0
  248. data/lib/hecks/projections/site/cms_editor/templates/src/browser/shell.js.tmpl +140 -0
  249. data/lib/hecks/projections/site/cms_editor/templates/src/browser/toast.js.tmpl +66 -0
  250. data/lib/hecks/projections/site/cms_editor/templates/src/choices.ts.tmpl +99 -0
  251. data/lib/hecks/projections/site/cms_editor/templates/src/commands.ts.tmpl +78 -24
  252. data/lib/hecks/projections/site/cms_editor/templates/src/config.ts.tmpl +6 -0
  253. data/lib/hecks/projections/site/cms_editor/templates/src/flash.ts.tmpl +52 -0
  254. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +19 -2
  255. data/lib/hecks/projections/site/cms_editor/templates/src/media/handler.ts.tmpl +31 -8
  256. data/lib/hecks/projections/site/cms_editor/templates/src/names.ts.tmpl +36 -0
  257. data/lib/hecks/projections/site/cms_editor/templates/src/schema.ts.tmpl +131 -2
  258. data/lib/hecks/projections/site/cms_editor/templates/src/ui/availability.ts.tmpl +15 -0
  259. data/lib/hecks/projections/site/cms_editor/templates/src/ui/block_list.js.tmpl +213 -0
  260. data/lib/hecks/projections/site/cms_editor/templates/src/ui/blocks.ts.tmpl +275 -0
  261. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_model.js.tmpl +8 -5
  262. data/lib/hecks/projections/site/cms_editor/templates/src/ui/detail.ts.tmpl +88 -0
  263. data/lib/hecks/projections/site/cms_editor/templates/src/ui/dialogs.ts.tmpl +43 -0
  264. data/lib/hecks/projections/site/cms_editor/templates/src/ui/fields.ts.tmpl +229 -34
  265. data/lib/hecks/projections/site/cms_editor/templates/src/ui/html.ts.tmpl +40 -81
  266. data/lib/hecks/projections/site/cms_editor/templates/src/ui/icons.ts.tmpl +15 -0
  267. data/lib/hecks/projections/site/cms_editor/templates/src/ui/input.ts.tmpl +29 -12
  268. data/lib/hecks/projections/site/cms_editor/templates/src/ui/links.ts.tmpl +100 -0
  269. data/lib/hecks/projections/site/cms_editor/templates/src/ui/moments.ts.tmpl +44 -0
  270. data/lib/hecks/projections/site/cms_editor/templates/src/ui/outline.ts.tmpl +53 -17
  271. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pages.ts.tmpl +178 -88
  272. data/lib/hecks/projections/site/cms_editor/templates/src/ui/paging.ts.tmpl +82 -0
  273. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pickers.ts.tmpl +110 -0
  274. data/lib/hecks/projections/site/cms_editor/templates/src/ui/preview.ts.tmpl +87 -0
  275. data/lib/hecks/projections/site/cms_editor/templates/src/ui/related.ts.tmpl +97 -0
  276. data/lib/hecks/projections/site/cms_editor/templates/src/ui/schedule.ts.tmpl +160 -0
  277. data/lib/hecks/projections/site/cms_editor/templates/src/ui/shell.ts.tmpl +211 -0
  278. data/lib/hecks/projections/site/cms_editor/templates/src/ui/table.ts.tmpl +151 -0
  279. data/lib/hecks/projections/site/cms_editor/templates/src/ui/theme.js.tmpl +16 -0
  280. data/lib/hecks/projections/site/cms_editor/templates/src/ui/values.ts.tmpl +69 -0
  281. data/lib/hecks/projections/site/cms_editor/templates/src/ui/words.ts.tmpl +97 -0
  282. data/lib/hecks/projections/site/cms_editor/templates/src/ui/world.ts.tmpl +35 -0
  283. data/lib/hecks/projections/site/cms_editor/templates/tsconfig.json.tmpl +2 -1
  284. data/lib/hecks/projections/site/cms_editor/theme.rb +29 -0
  285. data/lib/hecks/projections/site/cms_editor/theme_checks.rb +57 -0
  286. data/lib/hecks/projections/site/cms_editor.rb +49 -23
  287. data/lib/hecks/projections/site/role_probe.rb +2 -0
  288. data/lib/hecks/projector/cli_audience.rb +2 -1
  289. data/lib/hecks/projector/cli_projector/listing.rb +1 -1
  290. data/lib/hecks/projector/cli_projector/option_specs.rb +1 -1
  291. data/lib/hecks/projector/docs_projector/closing.rb +1 -1
  292. data/lib/hecks/projector/docs_projector.rb +1 -1
  293. data/lib/hecks/projector/exporter/bindings.rb +40 -10
  294. data/lib/hecks/projector/exporter/capability_values.rb +55 -0
  295. data/lib/hecks/projector/narrate_projector/reactions.rb +1 -1
  296. data/lib/hecks/projector/target.rb +11 -0
  297. data/lib/hecks/projector.rb +32 -5
  298. data/lib/hecks/quality_control/adapters/github_checks.rb +12 -8
  299. data/lib/hecks/quality_control/adapters/github_issues.rb +8 -4
  300. data/lib/hecks/quality_control/cli/qa_open_pr.rb +2 -2
  301. data/lib/hecks/quality_control/cli/qa_sweep/checks/shrinking.rb +1 -1
  302. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +1 -1
  303. data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
  304. data/lib/hecks/quality_control/quality_control.bluebook +6 -2
  305. data/lib/hecks/release/approval.rb +64 -0
  306. data/lib/hecks/router/{door.rb → aggregate_reader.rb} +5 -4
  307. data/lib/hecks/router/namespace_installer.rb +16 -16
  308. data/lib/hecks/router.rb +1 -1
  309. data/lib/hecks/runtime/best_effort.rb +22 -0
  310. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -2
  311. data/lib/hecks/runtime/command_interpreter.rb +3 -2
  312. data/lib/hecks/runtime/command_rules/emission.rb +1 -1
  313. data/lib/hecks/runtime/dispatcher.rb +4 -4
  314. data/lib/hecks/runtime/entity_element/location.rb +1 -1
  315. data/lib/hecks/runtime/event.rb +5 -0
  316. data/lib/hecks/runtime/identity.rb +7 -6
  317. data/lib/hecks/runtime/interpreting.rb +2 -1
  318. data/lib/hecks/runtime/invocation_builder.rb +1 -1
  319. data/lib/hecks/runtime/loader/boot_steps.rb +8 -5
  320. data/lib/hecks/runtime/loader.rb +10 -10
  321. data/lib/hecks/runtime/policy_interpreter/arguments.rb +7 -4
  322. data/lib/hecks/runtime/policy_interpreter/fan_out.rb +3 -3
  323. data/lib/hecks/runtime/policy_interpreter.rb +6 -6
  324. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -1
  325. data/lib/hecks/runtime/reaction_invocation.rb +1 -1
  326. data/lib/hecks/runtime/rebuild_sweep.rb +11 -1
  327. data/lib/hecks/runtime/registry/ask_verification.rb +35 -0
  328. data/lib/hecks/runtime/registry/hecksagon_rules.rb +10 -0
  329. data/lib/hecks/runtime/registry/merging.rb +1 -1
  330. data/lib/hecks/runtime/registry/providers.rb +16 -0
  331. data/lib/hecks/runtime/registry/read_repository.rb +6 -4
  332. data/lib/hecks/runtime/registry/verification.rb +4 -1
  333. data/lib/hecks/runtime/registry/world_defaults.rb +29 -0
  334. data/lib/hecks/runtime/registry.rb +1 -1
  335. data/lib/hecks/runtime/saga_interpreter/compensation.rb +1 -1
  336. data/lib/hecks/runtime/saga_interpreter/delivery.rb +4 -4
  337. data/lib/hecks/runtime/saga_interpreter/transitions.rb +1 -1
  338. data/lib/hecks/runtime/saga_interpreter.rb +3 -3
  339. data/lib/hecks/runtime/tenant_check.rb +5 -4
  340. data/lib/hecks/runtime/value/coercion.rb +2 -2
  341. data/lib/hecks/runtime/value/validation.rb +16 -4
  342. data/lib/hecks/runtime.rb +9 -9
  343. data/lib/hecks/rust_build/project_rust/target_ir.rb +1 -1
  344. data/lib/hecks/rust_build/wasm.rb +1 -1
  345. data/lib/hecks/rust_build/wasm_browser.rb +1 -1
  346. data/lib/hecks/rust_build.rb +11 -0
  347. data/lib/hecks/served_options.rb +53 -0
  348. data/lib/hecks/site/bluebook/site.bluebook +1 -0
  349. data/lib/hecks/storehouse/audit.rb +1 -1
  350. data/lib/hecks/storehouse/dispatching.rb +8 -6
  351. data/lib/hecks/storehouse/introspection.rb +2 -2
  352. data/lib/hecks/storehouse/querying.rb +6 -4
  353. data/lib/hecks/storehouse.rb +4 -4
  354. data/lib/hecks/tools/comment_style/source_file.rb +4 -1
  355. data/lib/hecks/tools/deploy_recipe_lint/fixtures.rb +4 -1
  356. data/lib/hecks/tools/lanes/rows.rb +6 -0
  357. data/lib/hecks/tools/oidc_manifests.rb +4 -5
  358. data/lib/hecks/tools/regeneration_run.rb +11 -4
  359. data/lib/hecks/tools/site_routes/editor_files.rb +12 -5
  360. data/lib/hecks/tools/tenant_provisioning.rb +2 -2
  361. data/lib/hecks/tools/tools_doc.rb +4 -4
  362. data/lib/hecks/version.rb +1 -1
  363. data/lib/hecks/vocabulary.rb +4 -4
  364. data/lib/hecks.rb +15 -15
  365. data/lib/rubocop/cop/hecks/env_dump.rb +69 -0
  366. data/lib/rubocop/cop/hecks/monotonic_duration.rb +53 -0
  367. data/lib/rubocop/cop/hecks/no_wall_clock_in_runtime.rb +34 -0
  368. data/lib/rubocop/cop/hecks/raw_shell_interpolation.rb +79 -0
  369. data/lib/rubocop/cop/hecks/sleep_in_spec.rb +34 -0
  370. data/lib/rubocop/cop/hecks/swallowed_rescue.rb +37 -0
  371. data/lib/rubocop/cop/hecks/symbol_string_key_mix.rb +50 -0
  372. data/lib/rubocop/cop/hecks/untyped_raise.rb +37 -0
  373. data/prebuilt/05c358d38d661a3774f813d49c66f5b68939af94432d66257a56f49ee2598994.marshal +0 -0
  374. data/prebuilt/26c40c61f09b09b11651c64668eda3ceb180f83cf2a3802d3033d2fd99ac841b.marshal +0 -0
  375. data/prebuilt/verdicts-230ac18c25abfd3d92ede032bee2661a4c2cd2773ab0cf0e2ae52546ba2f7af9.json +1 -0
  376. data/rust/.gitignore +6 -0
  377. data/rust/Cargo.toml +6 -0
  378. data/rust/build/src/binding_pass.rs +103 -0
  379. data/rust/build/src/cargo_sync.rs +6 -0
  380. data/rust/build/src/lineage_pass.rs +3 -3
  381. data/rust/build/src/main.rs +17 -5
  382. data/rust/build/src/pipeline.rs +48 -5
  383. data/rust/build/src/seams_pass.rs +123 -0
  384. data/rust/build/src/translation_pass.rs +167 -0
  385. data/rust/codegen/src/asks.rs +103 -0
  386. data/rust/codegen/src/commands.rs +13 -8
  387. data/rust/codegen/src/constraints.rs +25 -0
  388. data/rust/codegen/src/domain_generator.rs +2 -0
  389. data/rust/codegen/src/expr_emitter.rs +13 -0
  390. data/rust/codegen/src/json_codec.rs +54 -15
  391. data/rust/codegen/src/main.rs +1 -0
  392. data/rust/codegen/src/mutations.rs +128 -3
  393. data/rust/codegen/src/queries.rs +11 -2
  394. data/rust/codegen/src/reactions.rs +1 -1
  395. data/rust/codegen/src/types.rs +166 -8
  396. data/rust/host/HECKS_RELEASE +1 -1
  397. data/rust/host/src/api.rs +4 -4
  398. data/rust/host/src/bin/validate_harness.rs +43 -0
  399. data/rust/host/src/boundary_fuzz/expr_json.rs +2 -2
  400. data/rust/host/src/boundary_fuzz/ir.rs +1 -0
  401. data/rust/host/src/boundary_fuzz/server.rs +1 -1
  402. data/rust/host/src/checkout.rs +35 -19
  403. data/rust/host/src/commerce_ir.rs +408 -2
  404. data/rust/host/src/expr_json.rs +118 -0
  405. data/rust/host/src/mint.rs +1 -1
  406. data/rust/host/src/payments/tests.rs +27 -1
  407. data/rust/host/src/payments.rs +12 -4
  408. data/rust/host/src/query_step.rs +22 -4
  409. data/rust/host/src/reference_validate.rs +653 -108
  410. data/rust/host/src/resend.rs +2 -0
  411. data/rust/host/src/server.rs +19 -6
  412. data/rust/host/src/ui_schema.rs +132 -5
  413. data/rust/host/src/web/newsletter.rs +7 -8
  414. data/rust/host/src/web/newsletter_send.rs +38 -13
  415. data/rust/host/src/web/registrations.rs +229 -26
  416. data/rust/host/src/web.rs +3 -3
  417. data/rust/parser/src/emit.rs +38 -6
  418. data/rust/parser/src/expr/ast_json.rs +7 -1
  419. data/rust/parser/src/expr/resolver.rs +171 -6
  420. data/rust/parser/src/ir.rs +7 -0
  421. data/rust/parser/src/keywords.rs +24 -0
  422. data/rust/parser/src/main.rs +3 -0
  423. data/rust/parser/src/parse/aggregate.rs +2 -1
  424. data/rust/parser/src/parse/chapter.rs +2 -2
  425. data/rust/parser/src/parse/command.rs +2 -0
  426. data/rust/parser/src/parse/hecksagon.rs +5 -2
  427. data/rust/parser/src/parse/lifecycle.rs +81 -0
  428. data/rust/parser/src/parse/mod.rs +15 -1
  429. data/rust/parser/src/parse/policy.rs +13 -2
  430. data/rust/parser/src/parse/value_object.rs +13 -8
  431. data/rust/parser/src/ruby_value.rs +1 -1
  432. data/rust/src/exemplar/commands.rs +4 -4
  433. data/rust/src/exemplar/constraints.rs +1 -1
  434. data/rust/src/exemplar/json.rs +43 -1
  435. data/rust/src/kernel/cli.rs +7 -5
  436. data/rust/src/kernel/dispatch.rs +2 -2
  437. data/rust/src/kernel/dispatch_guard_tests.rs +1 -1
  438. data/rust/src/kernel/expr.rs +66 -3
  439. data/rust/src/kernel/expression_operators/arithmetic.rs +130 -5
  440. data/rust/src/kernel/expression_operators/text.rs +26 -2
  441. data/rust/src/kernel/json.rs +15 -0
  442. data/rust/src/kernel/mod.rs +1 -1
  443. data/rust/src/kernel/orchestrate.rs +54 -5
  444. data/rust/src/kernel/orchestrate_guard_tests.rs +67 -0
  445. data/rust/src/kernel/pattern.rs +60 -0
  446. data/rust/src/kernel/reference_lookup.rs +90 -7
  447. metadata +140 -38
  448. data/lib/hecks/doors/cli_door/nesting.rb +0 -34
  449. data/lib/hecks/doors/cli_door/shorthand.rb +0 -85
  450. data/lib/hecks/doors/cli_door.rb +0 -124
  451. data/lib/hecks/doors/cli_runner/answers.rb +0 -138
  452. data/lib/hecks/doors/cli_runner/resolution.rb +0 -146
  453. data/lib/hecks/doors/cli_runner/settling.rb +0 -115
  454. data/lib/hecks/doors/cli_runner/streaming.rb +0 -90
  455. data/lib/hecks/doors/cli_runner/suggestions.rb +0 -43
  456. data/lib/hecks/doors/cli_runner.rb +0 -66
  457. data/lib/hecks/doors/command_request.rb +0 -119
  458. data/lib/hecks/doors/handle.rb +0 -208
  459. data/lib/hecks/doors/json_door.rb +0 -132
  460. data/lib/hecks/doors/launcher_options.rb +0 -247
  461. data/lib/hecks/doors/mcp_door/properties.rb +0 -47
  462. data/lib/hecks/doors/mcp_door/tool_calls.rb +0 -113
  463. data/lib/hecks/doors/mcp_door/tool_definitions/auditing.rb +0 -51
  464. data/lib/hecks/doors/mcp_door/tool_definitions/calling.rb +0 -69
  465. data/lib/hecks/doors/mcp_door/tool_definitions/reading.rb +0 -97
  466. data/lib/hecks/doors/mcp_door/tool_definitions.rb +0 -15
  467. data/lib/hecks/doors/mcp_door.rb +0 -156
  468. data/lib/hecks/doors/mcp_door_scope/argument_policy.rb +0 -92
  469. data/lib/hecks/doors/mcp_door_scope/env_problems.rb +0 -55
  470. data/lib/hecks/doors/mcp_door_scope/presenter.rb +0 -78
  471. data/lib/hecks/doors/mcp_door_scope.rb +0 -201
  472. data/lib/hecks/doors/mcp_guide_cache.rb +0 -75
  473. data/lib/hecks/doors/ruby_door/aggregate_door.rb +0 -162
  474. data/lib/hecks/doors/ruby_door/chapter.rb +0 -89
  475. data/lib/hecks/doors/ruby_door.rb +0 -80
  476. data/lib/hecks/doors/usage_cache.rb +0 -139
  477. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_widget.js.tmpl +0 -282
  478. data/lib/hecks/projections/site/cms_editor/templates/src/ui/media_picker.js.tmpl +0 -109
  479. data/prebuilt/c28d6d9db14eea3e17161e9d9c262efee30b256cf97693a7cfd552bce24b2f7f.marshal +0 -0
  480. data/prebuilt/cb375cba61ca0dbe8ddb9739c306510fe05e5f4b7a9b6955caff8224c63ea0e0.marshal +0 -0
  481. data/prebuilt/verdicts-d6d584ad43b5f3cebf88aafbbdbf024c38574acf18e06adffbc555f7786b1c2f.json +0 -1
@@ -0,0 +1,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,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
@@ -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
@@ -1,7 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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"
4
10
  require_relative "media"
11
+ require_relative "pickers"
12
+ require_relative "relations"
13
+ require_relative "scheduling"
5
14
 
6
15
  module Hecks
7
16
  module Projections
@@ -18,17 +27,55 @@ module Hecks
18
27
  # A query that `returns` a value object is answered outside the domain and is left out: the
19
28
  # host refuses it, so a list cannot be built from it.
20
29
  class Schema
21
- # @param chapter [Bluebook::Chapter] the domain's chapter
22
- # @param skip [Array<String>] aggregates to leave out
23
- # @return [Hash{String => Object}] the domain's name and its aggregates
24
- # @raise [ArgumentError] when no aggregate is left
25
- def self.read(chapter, skip: [])
26
- aggregates = chapter.aggregates.reject { |agg| skip.include?(agg.hecks_name) }
27
- raise ArgumentError, "the #{chapter.name} chapter has no aggregate to edit" if aggregates.empty?
28
-
29
- shaped = aggregates.map { |agg| new(agg).to_h }
30
- media = Media.read(shaped)
31
- { "domain" => chapter.name, "aggregates" => shaped, **(media ? { "media" => media } : {}) }
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"] })
32
79
  end
33
80
 
34
81
  # @param aggregate [Bluebook::Aggregate] one aggregate of the chapter
@@ -39,28 +86,51 @@ module Hecks
39
86
 
40
87
  # @return [Hash{String => Object}] the aggregate as the editor reads it
41
88
  def to_h
89
+ held = aggregate_attributes
90
+ drafts = Drafts.of(@agg, held)
42
91
  { "name" => @agg.hecks_name, "description" => @agg.description, "identity" => @agg.identified_by.to_s,
43
- "lifecycle" => lifecycle, "attributes" => attributes(@agg.attributes.reject { |a| a.name == lifecycle_field }),
44
- "valueObjects" => value_objects, "commands" => @agg.commands.map { |command| command(command) },
45
- "queries" => queries }
92
+ "lifecycle" => lifecycle, "attributes" => held, "valueObjects" => value_objects,
93
+ "commands" => @agg.commands.map { |command| command(command) }, "queries" => queries,
94
+ **(drafts ? { "drafts" => drafts } : {}) }
46
95
  end
47
96
 
48
97
  private
49
98
 
50
99
  def lifecycle_field = @agg.lifecycle&.field
51
100
 
52
- def attributes(list) = list.map { |attribute| @attributes.of(attribute) }
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
53
116
 
54
117
  def value_objects
55
- @agg.value_objects.to_h { |object| [object.hecks_name, attributes(object.attributes)] }
118
+ @agg.value_objects.to_h do |object|
119
+ [object.hecks_name, bounded(attributes(object.attributes), object.invariants)]
120
+ end
56
121
  end
57
122
 
58
123
  def lifecycle
124
+ @lifecycle ||= shaped_lifecycle
125
+ end
126
+
127
+ def shaped_lifecycle
59
128
  lifecycle = @agg.lifecycle
60
129
  return nil unless lifecycle
61
130
 
62
- { "field" => lifecycle.field.to_s, "default" => lifecycle.default,
63
- "transitions" => lifecycle.transitions.map { |verb, move| transition(verb, move) } }
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))
64
134
  end
65
135
 
66
136
  def transition(verb, move)
@@ -69,9 +139,16 @@ module Hecks
69
139
 
70
140
  def command(command)
71
141
  { "name" => command.hecks_name, "goal" => command.goal, "role" => command.role, "creates" => command.creates?,
72
- "on" => command.creates? ? nil : command.references.to_s, "attributes" => attributes(command.attributes) }
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) }
73
146
  end
74
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
+
75
152
  def queries
76
153
  @agg.queries.reject(&:returns).map do |query|
77
154
  { "name" => query.name, "description" => query.description, "attributes" => attributes(query.attributes) }