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
@@ -8,6 +8,14 @@
8
8
  # 40 the box stack has no instance
9
9
  # 41 the roll did not succeed on the box
10
10
  # 42 the roll succeeded but the box is not healthy after it
11
+ # 43 no restore anchor: the database's backups cannot be read, or are kept fewer days than
12
+ # MIN_BACKUP_RETENTION_DAYS (default 3)
13
+ #
14
+ # Before anything changes, the roll names the point the database can be restored to: the instance, the
15
+ # moment this roll starts, and how long its continuous backups are kept. A roll that mints a new era is
16
+ # a one-way door, so this is the rollback anchor, and it costs nothing (a point-in-time restore uses the
17
+ # backups the instance already keeps, where a manual snapshot would be stored until someone deleted it).
18
+ # SKIP_RESTORE_ANCHOR=1 skips it.
11
19
  #
12
20
  # Before the roll replaces a container, the box saves that container's log to
13
21
  # /var/log/hecks-captures/<container>-<UTC timestamp>.log and keeps the newest 14 per container
@@ -30,6 +38,27 @@ DB_HOST=$(out "$RDS_STACK" DbEndpoint)
30
38
  [ -n "$BOX" ] && [ "$BOX" != None ] || { echo "no instance in stack $BOX_STACK" >&2; exit 40; }
31
39
  ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
32
40
 
41
+ if [ "${SKIP_RESTORE_ANCHOR:-}" = 1 ]; then
42
+ echo "==> skipping the restore anchor (SKIP_RESTORE_ANCHOR=1)"
43
+ else
44
+ DB_ID=${DB_HOST%%.*}
45
+ BACKUPS=$(aws rds describe-db-instances --db-instance-identifier "$DB_ID" \
46
+ --query 'DBInstances[0].[BackupRetentionPeriod,LatestRestorableTime]' --output text) || BACKUPS=
47
+ read -r KEPT_DAYS RESTORABLE <<< "$BACKUPS"
48
+ case "${KEPT_DAYS:-}" in ''|*[!0-9]*)
49
+ echo "==> no restore anchor: could not read the backups of $DB_ID (SKIP_RESTORE_ANCHOR=1 rolls without one)" >&2
50
+ exit 43 ;;
51
+ esac
52
+ if [ "$KEPT_DAYS" -lt "${MIN_BACKUP_RETENTION_DAYS:-3}" ]; then
53
+ echo "==> no restore anchor: $DB_ID keeps backups $KEPT_DAYS day(s), fewer than ${MIN_BACKUP_RETENTION_DAYS:-3}" >&2
54
+ exit 43
55
+ fi
56
+ ANCHOR_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
57
+ echo "==> restore anchor: $DB_ID restorable to $ANCHOR_AT (latest $RESTORABLE, backups kept $KEPT_DAYS days)"
58
+ echo " to return: aws rds restore-db-instance-to-point-in-time --source-db-instance-identifier $DB_ID \\"
59
+ echo " --target-db-instance-identifier $DB_ID-restore --restore-time $ANCHOR_AT"
60
+ fi
61
+
33
62
  WORK=$(mktemp -d); trap 'rm -rf "$WORK"' EXIT
34
63
 
35
64
  # SMOKE_LISTENER=1 (a rehearsal box only) mounts a loopback HTTPS site on 127.0.0.1:8443 that adds the
@@ -24,7 +24,7 @@
24
24
  # 32 the box's Compose file is unreadable 33 the box stack has no instance
25
25
  # 34 the stack has no parameter for the container 35 the stack update failed or did not settle
26
26
  # 36 a parameter other than the container's changed 37 the task definition lacks the pushed image
27
- # 38 the task definition has no such container 40-42 deploy-box.sh's own
27
+ # 38 the task definition has no such container 40-43 deploy-box.sh's own
28
28
  #
29
29
  # Environment:
30
30
  # LOCAL_IMAGE local image to push, default <ecr repository>:latest
@@ -365,7 +365,7 @@ signup_admin() {
365
365
  # Prints what to paste into the browser console to sign in as the preview's first
366
366
  # admin: a session for that email signed with THIS preview's session secret (the
367
367
  # secret itself is never printed). Anyone with AWS access to the account could
368
- # mint the same, which is the point: it is the deployer's door, not a public one.
368
+ # mint the same, which is the point: it is the deployer's entry, not a public one.
369
369
  cmd_login() {
370
370
  [ -n "$(stack_status)" ] || die "no preview for branch '$BRANCH' - run 'preview.sh deploy'"
371
371
  admin_identity
@@ -7,6 +7,9 @@ module Hecks
7
7
  # VPC, RDS/Aurora, bastion and cross-domain invoke plumbing shared by the
8
8
  # `Lambda` and `Fargate` deploy targets. Plain module functions, not a Target.
9
9
  module Shared
10
+ # Raised when a bastion parameter names a stack output that is not declared.
11
+ class Error < RuntimeError; end
12
+
10
13
  # The names a domain's own VPC and database are rendered by.
11
14
  #
12
15
  # @!attribute [r] shared [Boolean] whether this domain borrows another domain's RDS instance
@@ -66,11 +69,11 @@ module Hecks
66
69
 
67
70
  # Generation-time check that every bastion parameter maps to a stack output.
68
71
  #
69
- # @raise [RuntimeError] if a parameter names a `from_output` absent from `stack_outputs`
72
+ # @raise [Error] if a parameter names a `from_output` absent from `stack_outputs`
70
73
  def check_bastion_parameters!(bastion_parameters, stack_outputs)
71
74
  bastion_parameters.each do |param|
72
75
  stack_outputs.any? { |o| o[:key] == param[:from_output] } or
73
- raise "bastion parameter #{param[:name]} references undeclared stack output #{param[:from_output]}"
76
+ raise Error, "bastion parameter #{param[:name]} references undeclared stack output #{param[:from_output]}"
74
77
  end
75
78
  end
76
79
 
@@ -17,8 +17,8 @@ module Hecks
17
17
 
18
18
  # One model field gathered from several declared ones.
19
19
  FOLDED = {
20
- "Aggregate" => { lifecycle: %i[state_field state_start transitions] },
21
- "Entity" => { lifecycle: %i[state_field state_start transitions] },
20
+ "Aggregate" => { lifecycle: %i[state_field state_start transitions marks] },
21
+ "Entity" => { lifecycle: %i[state_field state_start transitions marks] },
22
22
  "Query" => { order_by: %i[order_field order_way] }
23
23
  }.freeze
24
24
 
@@ -16,10 +16,10 @@ module Hecks
16
16
  construct: "Policy",
17
17
  file: "policy.rb",
18
18
  behaviour: "Behaviour::Policy",
19
- readers: %i[name on_event trigger_command target_domain expect_undelivered where for_each with_spec],
19
+ readers: %i[name on_event trigger_command ask target_domain expect_undelivered where for_each with_spec],
20
20
  accessors: %i[aggregate],
21
21
  defaults: { name: nil, on_event: "nil", trigger_command: "nil",
22
- target_domain: "nil", expect_undelivered: "false", where: "nil", for_each: "nil",
22
+ ask: "nil", target_domain: "nil", expect_undelivered: "false", where: "nil", for_each: "nil",
23
23
  with_spec: "[]", aggregate: "nil" },
24
24
  # The language holds a flag as "true" and the builder as `true`; both become a boolean.
25
25
  coerce: { name: ".to_s", aggregate: "&.to_s", expect_undelivered: ".to_s == \"true\"" },
@@ -1,3 +1,4 @@
1
+ require "json"
1
2
  require_relative "../naming"
2
3
  require_relative "../projector"
3
4
 
@@ -35,6 +36,17 @@ module Hecks
35
36
  }
36
37
  end
37
38
 
39
+ # The manifest as the text `oidc.json` holds: indented JSON ending in a newline, with an empty
40
+ # list as `[]`. `JSON.pretty_generate` spells an empty list `[\n\n]` in some json releases
41
+ # (2.7.2 among them) and `[]` in others, so the file is normalised here and a committed
42
+ # manifest compares equal whichever json a machine has.
43
+ #
44
+ # @param manifest [Hash{String => Object}] what `call` returns
45
+ # @return [String] the text of the manifest file
46
+ def render(manifest)
47
+ "#{JSON.pretty_generate(manifest).gsub(/\[\n\s*\]/, "[]")}\n"
48
+ end
49
+
38
50
  # Every command's scope entry, entity commands included (they dispatch as `Entity.Command`).
39
51
  # Sorted by scope so two manifest versions diff cleanly.
40
52
  #
@@ -79,7 +91,7 @@ module Hecks
79
91
  end
80
92
 
81
93
  # One scope entry per command, spelled like `banking:account.open`; snake-cased through
82
- # `Naming.snake`, as the facade names its door methods.
94
+ # `Naming.snake`, as the facade names its module methods.
83
95
  #
84
96
  # @param commands [Array<Bluebook::Command>] the commands to project
85
97
  # @param verb_prefix [String] the owning aggregate or entity's fully-qualified
@@ -6,6 +6,9 @@ module Hecks
6
6
  # The storage shape of one bluebook, the form `StorageShape.mint_hash` hashes to name an era.
7
7
  # Diffing two of these answers whether a change bumps the era.
8
8
  module Shape
9
+ # Raised when the shape projection is asked for without the era plugin loaded.
10
+ class Error < RuntimeError; end
11
+
9
12
  extend Projector::Target
10
13
 
11
14
  projects_as :shape
@@ -15,8 +18,8 @@ module Hecks
15
18
  # Registered even when the era plugin is unloaded; the call then refuses clearly.
16
19
  def call(bluebook:, options: {})
17
20
  unless Ports::Persistence.plugin?(:era)
18
- raise "the :shape projection needs the era persistence plugin loaded " \
19
- "(require \"hecks/ports/persistence/plugins/era\")"
21
+ raise Error, "the :shape projection needs the era persistence plugin loaded " \
22
+ "(require \"hecks/ports/persistence/plugins/era\")"
20
23
  end
21
24
 
22
25
  Runtime::StorageShape.project(bluebook)
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "blocks"
4
+ require_relative "hints"
5
+
3
6
  module Hecks
4
7
  module Projections
5
8
  module Site
@@ -9,11 +12,8 @@ module Hecks
9
12
  #
10
13
  # A type that names one of the aggregate's value objects is an `object`; a reference is a
11
14
  # `reference`; the primitives are `integer`, `number`, `boolean` and `text`, and any other
12
- # name is entered as text.
13
- #
14
- # An attribute whose value object has the shape of a structured document, a `blocks`
15
- # list of a value object that has `kind` and `spans`, carries `widget: "body"` and is
16
- # edited with the rich-text widget. The shape decides it, never a name.
15
+ # is text. The shape decides what more it is, never a name: a body (`widget: "body"`), a
16
+ # block list (see `Blocks`), a moment, a `<kind>:` key (see `Hints`) or a picture.
17
17
  class Attributes
18
18
  # The parts a value object needs for its list of blocks to be a document body's blocks.
19
19
  BLOCK_PARTS = %w[kind spans].freeze
@@ -26,6 +26,7 @@ module Hecks
26
26
  def initialize(objects)
27
27
  @by_name = objects.to_h { |object| [object.hecks_name, object] }
28
28
  @objects = @by_name.keys
29
+ @hints = Hints.new(@by_name)
29
30
  end
30
31
 
31
32
  # @param attribute [Bluebook::Attribute] a declared attribute
@@ -35,11 +36,30 @@ module Hecks
35
36
  return reference(attribute, type.target_name) if type.is_a?(Bluebook::Reference)
36
37
 
37
38
  entry = base(attribute, type.to_s, kind(type.to_s))
38
- body?(attribute) ? entry.merge("widget" => "body") : entry
39
+ return entry.merge("widget" => "body") if body?(attribute)
40
+
41
+ entry.merge(@hints.of(attribute), listing(attribute), pictured(attribute))
39
42
  end
40
43
 
41
44
  private
42
45
 
46
+ # @return [Hash{String => Object}] `blockList` when the attribute is a list of blocks
47
+ def listing(attribute)
48
+ found = Blocks.of(attribute, @by_name)
49
+ found ? { "blockList" => found } : {}
50
+ end
51
+
52
+ # @return [Hash{String => Object}] `picture` (the parts that name the file, its
53
+ # description and its caption) when the value object is a picture: it has a media key
54
+ # and an `alt` and is not a block of a body or a list of mixed blocks
55
+ def pictured(attribute)
56
+ object = @by_name[attribute.type.to_s]
57
+ return {} unless object && !Blocks.body_block?(object) && !listing(attribute).key?("blockList")
58
+
59
+ found = Blocks.picture(object)
60
+ found&.key?("key") && found.key?("alt") ? { "picture" => found } : {}
61
+ end
62
+
43
63
  # A single (not listed) attribute whose value object holds a list of document blocks.
44
64
  def body?(attribute)
45
65
  return false if attribute.list?
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hecks
4
+ module Projections
5
+ module Site
6
+ module CmsEditor
7
+ # A list of mixed blocks, found by shape: a `list_of` value object that has a discriminator,
8
+ # a part that is one of a closed set of kinds. The editor draws it as ordered cards.
9
+ #
10
+ # The discriminator is a part whose type is a closed set (`one_of`) of one plain value, or,
11
+ # failing that, a text part that an invariant restricts to a literal list
12
+ # (`["a", "b"].include?(kind)`). A value object that holds `spans` is a body's block, which
13
+ # the rich-text widget edits, and is never a block list.
14
+ #
15
+ # Which slots each kind uses is read from a declared table: a closed set of rows with the
16
+ # parts `kind`, `requires` and `uses` whose kinds are among the discriminator's. With no
17
+ # such table every slot is shown, and the invariants that name a kind only hint which slots
18
+ # it reads. A limit is read from an invariant or given of the form `<list>.size <= <n>`.
19
+ module Blocks
20
+ # The parts a table of rules must have.
21
+ RULE_PARTS = %w[kind requires uses].freeze
22
+
23
+ # The parts that make a value object's picture, in the vocabulary of a body's image block.
24
+ PICTURE = { "key" => "media_ref", "alt" => "alt", "caption" => "caption" }.freeze
25
+
26
+ # An invariant that restricts a text part to a literal list of kinds.
27
+ LITERALS = /\A\[((?:"[^"]+"(?:, )?)+)\]\.include\?\((\w+)\)\z/
28
+
29
+ # An invariant that holds unless the kind is the named one: what the rest of it reads.
30
+ GUARD = /\A(\w+)(?:\.value)? != "([^"]+)" \|\| (.*)\z/m
31
+
32
+ # A limit written as a size comparison.
33
+ LIMIT = /\A(\w+)\.size <= (\d+)\z/
34
+
35
+ module_function
36
+
37
+ # @param attribute [Bluebook::Attribute] a declared attribute
38
+ # @param objects [Hash{String => Bluebook::ValueObject}] the aggregate's value objects
39
+ # @return [Hash{String => Object}, nil] the block list the attribute is, or nil
40
+ def of(attribute, objects)
41
+ item = objects[attribute.type.to_s]
42
+ return nil unless attribute.list? && item && !body_block?(item)
43
+
44
+ found = discriminator(item, objects)
45
+ found && describe(item, objects, *found)
46
+ end
47
+
48
+ # @return [Boolean] whether the value object holds `spans`, as a body's block does
49
+ def body_block?(item) = item.attributes.any? { |part| part.name.to_s == "spans" }
50
+
51
+ # @return [Array(String, Array<String>), nil] the part and the kinds it is one of
52
+ def discriminator(item, objects)
53
+ closed(item, objects) || literal(item)
54
+ end
55
+
56
+ # @return [Array(String, Array<String>), nil] a part typed as a closed set of plain values
57
+ def closed(item, objects)
58
+ item.attributes.each do |part|
59
+ kinds = members_of(objects[part.type.to_s]) unless part.list?
60
+ return [part.name.to_s, kinds] if kinds && !kinds.empty?
61
+ end
62
+ nil
63
+ end
64
+
65
+ # @return [Array<String>, nil] the values of a closed set of one plain part
66
+ def members_of(set)
67
+ return nil unless set&.closed_set? && set.attributes.size == 1
68
+
69
+ set.members.filter_map { |member| member[set.attributes.first.name]&.to_s }
70
+ end
71
+
72
+ # @return [Array(String, Array<String>), nil] a text part restricted by a literal list
73
+ def literal(item)
74
+ found = literals(item).find { |match| item.attributes.any? { |part| part.name.to_s == match[2] && !part.list? } }
75
+ found && [found[2], found[1].scan(/"([^"]+)"/).flatten]
76
+ end
77
+
78
+ # @return [Array<MatchData>] the invariants that restrict a part to a literal list
79
+ def literals(item) = item.invariants.filter_map { |rule| LITERALS.match(rule.canonical.to_s) }
80
+
81
+ def describe(item, objects, name, kinds)
82
+ table = rules(objects, kinds)
83
+ found = { "discriminator" => name, "kinds" => kinds, "limits" => limits(item), "pictures" => pictures(item, objects) }
84
+ found["rules"] = table if table
85
+ found["mentions"] = mentions(item, name, kinds) unless table
86
+ found.reject { |_, value| value.respond_to?(:empty?) && value.empty? }
87
+ end
88
+
89
+ # @return [Hash{String => Hash}, nil] each kind's `requires` and `uses`, from the table
90
+ def rules(objects, kinds)
91
+ table = objects.values.find { |object| ruled?(object, kinds) }
92
+ return nil unless table
93
+
94
+ table.members.to_h { |row| [row[:kind].to_s, { "requires" => words(row[:requires]), "uses" => words(row[:uses]) }] }
95
+ end
96
+
97
+ def ruled?(object, kinds)
98
+ object.closed_set? && (RULE_PARTS - object.attributes.map { |part| part.name.to_s }).empty? &&
99
+ object.members.all? { |row| kinds.include?(row[:kind].to_s) }
100
+ end
101
+
102
+ def words(text) = text.to_s.split(",").map(&:strip).reject(&:empty?)
103
+
104
+ # @return [Hash{String => Array<String>}] the slots each kind's guarded invariants read
105
+ def mentions(item, name, kinds)
106
+ parts = item.attributes.map { |part| part.name.to_s } - [name]
107
+ guarded(item, name).each_with_object({}) do |(kind, rest), found|
108
+ next unless kinds.include?(kind)
109
+
110
+ found[kind] = ((found[kind] || []) | parts.select { |part| rest.match?(/\b#{Regexp.escape(part)}\b/) })
111
+ end
112
+ end
113
+
114
+ def guarded(item, name)
115
+ item.invariants.filter_map do |rule|
116
+ match = GUARD.match(rule.canonical.to_s)
117
+ [match[2], match[3]] if match && match[1] == name
118
+ end
119
+ end
120
+
121
+ # @return [Hash{String => Integer}] each list part of the item and its declared maximum
122
+ def limits(item)
123
+ item.invariants.filter_map do |rule|
124
+ match = LIMIT.match(rule.canonical.to_s)
125
+ [match[1], match[2].to_i] if match
126
+ end.to_h
127
+ end
128
+
129
+ # @param commands [Array<Bluebook::Command>] the aggregate's commands
130
+ # @param name [String] a list attribute's name
131
+ # @return [Integer, nil] the most the list may hold, from a command's `given`
132
+ def maximum(commands, name)
133
+ sizes = commands.flat_map(&:givens).filter_map do |rule|
134
+ match = LIMIT.match(rule.canonical.to_s)
135
+ match[2].to_i if match && match[1] == name
136
+ end
137
+ sizes.min
138
+ end
139
+
140
+ # @return [Hash{String => Hash}] the value object and each value object its lists hold,
141
+ # to the parts that name a picture, for those that have the parts
142
+ def pictures(item, objects)
143
+ held = item.attributes.select(&:list?).filter_map { |part| objects[part.type.to_s] }
144
+ [item, *held].filter_map { |object| (found = picture(object)) && [object.hecks_name, found] }.to_h
145
+ end
146
+
147
+ def picture(object)
148
+ names = object.attributes.reject(&:list?).map { |part| part.name.to_s }
149
+ return nil unless names.include?(PICTURE["key"]) && names.include?(PICTURE["alt"])
150
+
151
+ PICTURE.select { |_, part| names.include?(part) }
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "preview"
4
+ require_relative "scale_checks"
5
+ require_relative "theme_checks"
6
+
3
7
  module Hecks
4
8
  module Projections
5
9
  module Site
@@ -23,7 +27,8 @@ module Hecks
23
27
  def problems(row, roles, table)
24
28
  base = row[:base_path]
25
29
  [*path_problems(row, base), *("Editor roles name no role" if roles.empty?), *login_problems(row[:login], table),
26
- *media_problems(row)]
30
+ *media_problems(row), *media_chapter_problems(row), *ThemeChecks.problems(row), *ScaleChecks.problems(row),
31
+ *Preview.problems(row[:preview].strip)]
27
32
  end
28
33
 
29
34
  # @return [Array<String>] the problems with the base path and the sign-in path
@@ -45,6 +50,14 @@ module Hecks
45
50
  found
46
51
  end
47
52
 
53
+ # @return [Array<String>] the problem with a `media` chapter that is one the editor edits
54
+ def media_chapter_problems(row)
55
+ named = row[:media].strip
56
+ return [] unless row[:chapter].split(",").map(&:strip).include?(named)
57
+
58
+ ["media #{named.inspect} is this editor's own chapter; name another chapter"]
59
+ end
60
+
48
61
  # The login page is a public row: a visitor with no session must reach it.
49
62
  #
50
63
  # @return [Array<String>] the problems with the login page
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hecks
4
+ module Projections
5
+ module Site
6
+ module CmsEditor
7
+ # A command argument that only clears: the bluebook's way of setting an optional field back
8
+ # to nothing is an optional argument the command never reads, named as the source of a
9
+ # `sets <field>, to: :<argument>` mutation (`attribute :nothing, Body, optional: true`,
10
+ # `sets :draft_body, to: :nothing`). Nobody types into it, so the editor offers no field
11
+ # for it and the server sends its empty value itself: an empty list, or nothing at all.
12
+ #
13
+ # The shape decides it, never a name. The argument is optional, is no attribute of the
14
+ # aggregate, feeds only plain `sets` mutations of other fields that can be empty (optional
15
+ # or a list), and no `given` or `ensures` of the command reads it.
16
+ module Clearing
17
+ module_function
18
+
19
+ # @param command [Bluebook::Command] the command
20
+ # @param aggregate [Bluebook::Aggregate] the aggregate it belongs to
21
+ # @return [Array<Bluebook::Attribute>] the command's arguments that only clear
22
+ def of(command, aggregate)
23
+ command.attributes.select { |argument| only_clears?(argument, command, aggregate) }
24
+ end
25
+
26
+ # @param shaped [Hash{String => Object}] the command as the editor reads it
27
+ # @param only [Array<Bluebook::Attribute>] its arguments that only clear
28
+ # @return [Hash{String => Object}] `shaped` without those arguments, and naming the ones
29
+ # that are lists as `empty`, for the editor to send as empty lists
30
+ def apply(shaped, only)
31
+ return shaped if only.empty?
32
+
33
+ names = only.map { |argument| argument.name.to_s }
34
+ lists = only.select(&:list?).map { |argument| argument.name.to_s }
35
+ kept = shaped["attributes"].reject { |attr| names.include?(attr["name"]) }
36
+ shaped.merge("attributes" => kept, **(lists.empty? ? {} : { "empty" => lists }))
37
+ end
38
+
39
+ # @return [Boolean] whether `argument` is a clearing argument of `command`
40
+ def only_clears?(argument, command, aggregate)
41
+ return false if !argument.optional? || named?(aggregate.attributes, argument.name)
42
+
43
+ fed = fed_by(argument, command)
44
+ !fed.empty? && fed.all? { |mutation| clears?(mutation, aggregate) } && !read?(argument, command)
45
+ end
46
+
47
+ # @return [Array<Bluebook::Mutation>] the mutations that take their value from `argument`
48
+ def fed_by(argument, command)
49
+ command.mutations.select { |mutation| mutation.source.is_a?(Symbol) && named?([argument], mutation.source) }
50
+ end
51
+
52
+ # @return [Boolean] whether any of `attributes` is called `name`
53
+ def named?(attributes, name) = attributes.any? { |held| held.name.to_s == name.to_s }
54
+
55
+ # @return [Boolean] whether the mutation is a plain set of a field that can be empty
56
+ def clears?(mutation, aggregate)
57
+ target = aggregate.attributes.find { |held| held.name.to_s == mutation.target.to_s }
58
+ mutation.op == :set && !target.nil? && (target.optional? || target.list?)
59
+ end
60
+
61
+ # @return [Boolean] whether a `given` or `ensures` of the command names the argument
62
+ def read?(argument, command)
63
+ word = /\b#{Regexp.escape(argument.name.to_s)}\b/
64
+ [*command.givens, *command.ensures].any? { |rule| word.match?(rule.canonical.to_s) }
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hecks
4
+ module Projections
5
+ module Site
6
+ module CmsEditor
7
+ # Colour arithmetic for the editor's theme: parsing and writing six-digit hex, moving
8
+ # between red-green-blue and hue-saturation-lightness, mixing, and the contrast ratio the
9
+ # web accessibility guidelines define. Pure functions of their inputs, so the theme the
10
+ # editor is generated with is the same on every run.
11
+ module Color
12
+ # A six-digit hex colour with its leading hash.
13
+ HEX = /\A#[0-9a-fA-F]{6}\z/
14
+
15
+ # The step by which `reach` moves a colour's lightness while it searches.
16
+ STEP = 0.01
17
+
18
+ module_function
19
+
20
+ # @param value [String] a colour such as `"#1f7a6d"`
21
+ # @return [Array<Float>] its red, green and blue, each from 0.0 to 1.0
22
+ def rgb(value) = value.delete_prefix("#").scan(/../).map { |pair| pair.hex / 255.0 }
23
+
24
+ # @param channels [Array<Float>] red, green and blue, each from 0.0 to 1.0
25
+ # @return [String] the colour as lower-case hex
26
+ def hex(channels)
27
+ channels.each_with_object(+"#") { |c, text| text << format("%02x", (c.clamp(0.0, 1.0) * 255).round) }
28
+ end
29
+
30
+ # @param value [String] a colour
31
+ # @return [Array<Float>] hue in degrees, then saturation and lightness from 0.0 to 1.0
32
+ def hsl(value)
33
+ red, green, blue = rgb(value)
34
+ high = [red, green, blue].max
35
+ low = [red, green, blue].min
36
+ light = (high + low) / 2.0
37
+ return [0.0, 0.0, light] if high == low
38
+
39
+ spread = high - low
40
+ sat = light > 0.5 ? spread / (2.0 - high - low) : spread / (high + low)
41
+ [angle(red, green, blue, high, spread), sat, light]
42
+ end
43
+
44
+ # @param hue [Float] degrees
45
+ # @param sat [Float] saturation from 0.0 to 1.0
46
+ # @param light [Float] lightness from 0.0 to 1.0
47
+ # @return [String] the colour as hex
48
+ def from_hsl(hue, sat, light)
49
+ chroma = (1 - ((2 * light) - 1).abs) * sat
50
+ shift = light - (chroma / 2)
51
+ hex(sector(hue % 360, chroma, second(hue % 360, chroma)).map { |channel| channel + shift })
52
+ end
53
+
54
+ # @return [Float] the second-largest channel of a colour of the given hue and chroma
55
+ def second(hue, chroma) = chroma * (1 - (((hue / 60.0) % 2) - 1).abs)
56
+
57
+ # @param one [String] a colour
58
+ # @param other [String] another colour
59
+ # @param share [Float] how much of `other` goes in, from 0.0 to 1.0
60
+ # @return [String] the colours mixed channel by channel
61
+ def mix(one, other, share) = hex(rgb(one).zip(rgb(other)).map { |a, b| (a * (1 - share)) + (b * share) })
62
+
63
+ # @param value [String] a colour
64
+ # @return [Float] its relative luminance as the accessibility guidelines define it
65
+ def luminance(value)
66
+ red, green, blue = rgb(value).map { |c| c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055)**2.4 }
67
+ (0.2126 * red) + (0.7152 * green) + (0.0722 * blue)
68
+ end
69
+
70
+ # @param one [String] a colour
71
+ # @param other [String] another colour
72
+ # @return [Float] the contrast ratio, from 1.0 to 21.0
73
+ def contrast(one, other)
74
+ high = [luminance(one), luminance(other)].max
75
+ low = [luminance(one), luminance(other)].min
76
+ (high + 0.05) / (low + 0.05)
77
+ end
78
+
79
+ # Moves a colour's lightness, a little at a time, until it reads against every background.
80
+ #
81
+ # @param value [String] where to start
82
+ # @param against [Array<String>] backgrounds the colour must read on
83
+ # @param ratio [Float] the least contrast to reach
84
+ # @param toward [Symbol] `:darker` or `:lighter`
85
+ # @return [String] `value` itself when it already reads, else the nearest such colour
86
+ def reach(value, against, ratio, toward)
87
+ hue, sat, light = hsl(value)
88
+ sign = toward == :darker ? -STEP : STEP
89
+ light += sign until light.round(2).clamp(0.0,
90
+ 1.0) != light.round(2) || reads?(from_hsl(hue, sat, light), against, ratio)
91
+ from_hsl(hue, sat, light.clamp(0.0, 1.0))
92
+ end
93
+
94
+ # @param value [String] a colour
95
+ # @param against [Array<String>] backgrounds
96
+ # @param ratio [Float] the least contrast
97
+ # @return [Boolean] whether `value` reaches `ratio` on every background
98
+ def reads?(value, against, ratio) = against.all? { |back| contrast(value, back) >= ratio }
99
+
100
+ # @param background [String] a fill
101
+ # @param light [String] a light foreground to try
102
+ # @param dark [String] a dark foreground to try
103
+ # @return [String] whichever reads better on `background`
104
+ def foreground(background, light, dark) = contrast(light, background) >= contrast(dark, background) ? light : dark
105
+
106
+ # @return [Float] the hue, in degrees, of a colour given by its channels
107
+ def angle(red, green, blue, high, spread)
108
+ degrees = if high == red then ((green - blue) / spread) % 6
109
+ elsif high == green then ((blue - red) / spread) + 2
110
+ else ((red - green) / spread) + 4
111
+ end
112
+ degrees * 60.0
113
+ end
114
+
115
+ # @return [Array<Float>] the red, green and blue before lightness is added back
116
+ def sector(hue, chroma, mid)
117
+ [[chroma, mid, 0], [mid, chroma, 0], [0, chroma, mid], [0, mid, chroma], [mid, 0, chroma],
118
+ [chroma, 0, mid]][(hue / 60).floor % 6]
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hecks
4
+ module Projections
5
+ module Site
6
+ module CmsEditor
7
+ # Several chapters as one editor: each aggregate keeps the name its chapter gave it and is
8
+ # stamped with the chapter that declares it, so two aggregates of one name in different
9
+ # chapters are two aggregates. The stamps exist only when the editor spans more than one
10
+ # chapter (`chapter`), or when a chapter's roles are limited (`roles`), so an editor of one
11
+ # open chapter reads exactly as a chapter alone does.
12
+ module Composition
13
+ module_function
14
+
15
+ # @param chapters [Array<Bluebook::Chapter>] the chapters, in the order the row names them
16
+ # @param skip [Array<String>] `Name` (in every chapter) or `Chapter::Name` to leave out
17
+ # @param roles [Hash{String => Array<String>}] the roles each chapter is limited to
18
+ # @yieldparam aggregate [Bluebook::Aggregate] one aggregate to shape
19
+ # @yieldreturn [Hash{String => Object}] the aggregate as the editor reads it
20
+ # @return [Array<Hash{String => Object}>] the shaped aggregates, chapter by chapter
21
+ def aggregates(chapters, skip:, roles:)
22
+ many = chapters.size > 1
23
+ chapters.flat_map do |chapter|
24
+ kept(chapter, skip).map { |agg| stamp(yield(agg), many ? chapter.name : nil, roles[chapter.name]) }
25
+ end
26
+ end
27
+
28
+ # @return [Array<Bluebook::Aggregate>] the chapter's aggregates that are not skipped
29
+ def kept(chapter, skip)
30
+ chapter.aggregates.reject { |agg| skip.intersect?([agg.hecks_name, "#{chapter.name}::#{agg.hecks_name}"]) }
31
+ end
32
+
33
+ # @return [Hash{String => Object}] `shaped` with its chapter and roles after its name
34
+ def stamp(shaped, named, roles)
35
+ extra = { "chapter" => named, "roles" => roles }.compact
36
+ { "name" => shaped["name"], **extra, **shaped.except("name") }
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end