hecks 3.10.0 → 3.12.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 (542) 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 +158 -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 +70 -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/newcomer.rb +50 -0
  40. data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +171 -0
  41. data/lib/hecks/adapters/driving/ruby/chapter.rb +91 -0
  42. data/lib/hecks/adapters/driving/ruby.rb +83 -0
  43. data/lib/hecks/adapters/driving/usage_cache.rb +144 -0
  44. data/lib/hecks/{doors.rb → adapters/driving.rb} +10 -6
  45. data/lib/hecks/behaviors/expectations/boot.rb +1 -1
  46. data/lib/hecks/bench/ruby_runner.rb +1 -1
  47. data/lib/hecks/bluebook/ask_resolution.rb +130 -0
  48. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +8 -2
  49. data/lib/hecks/bluebook/assembly/contracts/read_models.rb +1 -1
  50. data/lib/hecks/bluebook/assembly/contracts/rules.rb +4 -2
  51. data/lib/hecks/bluebook/assembly/contracts/structure.rb +3 -1
  52. data/lib/hecks/bluebook/assembly/marks.rb +4 -15
  53. data/lib/hecks/bluebook/assembly/scalars.rb +39 -0
  54. data/lib/hecks/bluebook/attribute.rb +1 -1
  55. data/lib/hecks/bluebook/behaviour/command.rb +13 -8
  56. data/lib/hecks/bluebook/behaviour/policy.rb +30 -0
  57. data/lib/hecks/bluebook/capabilities/attribute_values.rb +72 -0
  58. data/lib/hecks/bluebook/capabilities.rb +91 -9
  59. data/lib/hecks/bluebook/domain_port.rb +16 -4
  60. data/lib/hecks/bluebook/dsl/adapter_builder.rb +18 -6
  61. data/lib/hecks/bluebook/dsl/binding_proxy.rb +13 -0
  62. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/computed_sources.rb +98 -0
  63. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/fan_out_rows.rb +42 -0
  64. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/projected_fields.rb +14 -2
  65. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/provisions.rb +69 -3
  66. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/with_projections.rb +17 -10
  67. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +5 -0
  68. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
  69. data/lib/hecks/bluebook/dsl/command_builder/mutations.rb +8 -1
  70. data/lib/hecks/bluebook/dsl/command_builder/operands.rb +121 -0
  71. data/lib/hecks/bluebook/dsl/command_builder.rb +2 -0
  72. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +10 -1
  73. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +45 -2
  74. data/lib/hecks/bluebook/dsl/policy_builder.rb +47 -10
  75. data/lib/hecks/bluebook/dsl/rule_reference.rb +7 -4
  76. data/lib/hecks/bluebook/dsl/world_builder.rb +1 -1
  77. data/lib/hecks/bluebook/expression/ast_json/emitters.rb +6 -0
  78. data/lib/hecks/bluebook/expression/ast_json.rb +9 -6
  79. data/lib/hecks/bluebook/expression/ast_reader.rb +14 -9
  80. data/lib/hecks/bluebook/expression/projection.json +36 -0
  81. data/lib/hecks/bluebook/expression/resolver/operations.rb +31 -0
  82. data/lib/hecks/bluebook/expression/resolver/parsing.rb +38 -5
  83. data/lib/hecks/bluebook/expression/resolver/scanning.rb +22 -5
  84. data/lib/hecks/bluebook/expression/resolver/values.rb +10 -0
  85. data/lib/hecks/bluebook/expression/resolver.rb +20 -1
  86. data/lib/hecks/bluebook/hecksagon.rb +23 -3
  87. data/lib/hecks/bluebook/lifecycle.rb +22 -3
  88. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +13 -1
  89. data/lib/hecks/bluebook/meta_validator/modes.rb +4 -1
  90. data/lib/hecks/bluebook/meta_validator/readings.rb +24 -1
  91. data/lib/hecks/bluebook/meta_validator/reconstruction/heads.rb +2 -1
  92. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +6 -3
  93. data/lib/hecks/bluebook/meta_validator/shapes.rb +5 -0
  94. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +1 -1
  95. data/lib/hecks/bluebook/meta_validator.rb +12 -12
  96. data/lib/hecks/bluebook/model_check/ask_checks.rb +71 -0
  97. data/lib/hecks/bluebook/model_check/policy_checks.rb +2 -1
  98. data/lib/hecks/bluebook/model_check/reference_checks.rb +70 -0
  99. data/lib/hecks/bluebook/model_check.rb +8 -2
  100. data/lib/hecks/bluebook/policy.rb +4 -2
  101. data/lib/hecks/bluebook/smoke_test.rb +1 -1
  102. data/lib/hecks/bluebook/synthesizer.rb +2 -0
  103. data/lib/hecks/bluebook.rb +2 -0
  104. data/lib/hecks/cli/command_table.rb +1 -1
  105. data/lib/hecks/cli/console.rb +2 -1
  106. data/lib/hecks/cli/document.rb +1 -1
  107. data/lib/hecks/cli/launcher_words.rb +4 -4
  108. data/lib/hecks/cli/mcp.rb +4 -4
  109. data/lib/hecks/cli/project_cli/launcher_source.rb +2 -2
  110. data/lib/hecks/cli/project_cli/templates.rb +5 -5
  111. data/lib/hecks/cli/project_cli.rb +9 -6
  112. data/lib/hecks/cli/run.rb +2 -2
  113. data/lib/hecks/cli/smoke_http/checks.rb +4 -1
  114. data/lib/hecks/cli/smoke_http/transport.rb +3 -3
  115. data/lib/hecks/cli/smoke_http.rb +3 -0
  116. data/lib/hecks/codemod.rb +5 -2
  117. data/lib/hecks/corpus/routes.rb +7 -0
  118. data/lib/hecks/corpus/rust.rb +3 -2
  119. data/lib/hecks/deploy/bluebook/deploy.bluebook +34 -4
  120. data/lib/hecks/doc/reference/rendering.rb +6 -3
  121. data/lib/hecks/forms/app/commands.rb +8 -7
  122. data/lib/hecks/forms/app/queries.rb +2 -1
  123. data/lib/hecks/forms/app.rb +1 -1
  124. data/lib/hecks/forms/record_renderer.rb +1 -1
  125. data/lib/hecks/framework/bluebook/privacy.bluebook +11 -0
  126. data/lib/hecks/fuzzing/concurrent_dispatch.rb +4 -1
  127. data/lib/hecks/fuzzing/failure_triage.rb +2 -1
  128. data/lib/hecks/fuzzing/mutation/run.rb +11 -4
  129. data/lib/hecks/fuzzing/properties/catalog.rb +6 -4
  130. data/lib/hecks/fuzzing/properties/guaranteed_by_construction.rb +18 -11
  131. data/lib/hecks/fuzzing/properties/guards.rb +1 -1
  132. data/lib/hecks/fuzzing/self_consistency/saga_redelivery.rb +1 -1
  133. data/lib/hecks/fuzzing/self_consistency.rb +6 -2
  134. data/lib/hecks/fuzzing/shrinker.rb +3 -1
  135. data/lib/hecks/fuzzing/value_generator.rb +19 -5
  136. data/lib/hecks/gate/stages.yml +33 -7
  137. data/lib/hecks/grammar/evolve.rb +1 -1
  138. data/lib/hecks/grammar/expression_operators.json +372 -0
  139. data/lib/hecks/grammar/operators.rb +3 -1
  140. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +10 -10
  141. data/lib/hecks/hecks/adapters/codebase/inspection/rubycritic.rb +35 -0
  142. data/lib/hecks/hecks/adapters/codebase/inspection.rb +104 -0
  143. data/lib/hecks/hecks/adapters/codebase/publishing.rb +2 -1
  144. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +12 -2
  145. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +2 -1
  146. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +1 -1
  147. data/lib/hecks/hecks/adapters/cost_explorer.rb +8 -5
  148. data/lib/hecks/hecks/adapters/deploy_toolchain/statuses.rb +2 -1
  149. data/lib/hecks/hecks/adapters/finding_github.rb +13 -6
  150. data/lib/hecks/hecks/adapters/first_admin.rb +2 -2
  151. data/lib/hecks/hecks/adapters/github_rulesets.rb +6 -3
  152. data/lib/hecks/hecks/adapters/in_process_boot.rb +1 -1
  153. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +1 -1
  154. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +10 -20
  155. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +28 -5
  156. data/lib/hecks/hecks/adapters/local_files.rb +2 -2
  157. data/lib/hecks/hecks/adapters/rehearsal/audit_report.rb +107 -0
  158. data/lib/hecks/hecks/adapters/rehearsal/census.rb +71 -0
  159. data/lib/hecks/hecks/adapters/rehearsal/declaration.rb +133 -0
  160. data/lib/hecks/hecks/adapters/rehearsal/environment.rb +83 -0
  161. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +142 -0
  162. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +96 -0
  163. data/lib/hecks/hecks/adapters/rehearsal/postgres_census.rb +100 -0
  164. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +69 -0
  165. data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +133 -0
  166. data/lib/hecks/hecks/adapters/rehearsal/rows.rb +41 -0
  167. data/lib/hecks/hecks/adapters/rehearsal/rust_audit.rb +83 -0
  168. data/lib/hecks/hecks/adapters/rehearsal/subject.rb +104 -0
  169. data/lib/hecks/hecks/adapters/rehearsal.rb +54 -0
  170. data/lib/hecks/hecks/adapters/rehearsal_store.adapter +7 -0
  171. data/lib/hecks/hecks/adapters/rehearsal_store.rb +81 -0
  172. data/lib/hecks/hecks/adapters/terminal.rb +13 -11
  173. data/lib/hecks/hecks/codebase.behaviors +13 -1
  174. data/lib/hecks/hecks/codebase.bluebook +239 -5
  175. data/lib/hecks/hecks/custodian.behaviors +24 -6
  176. data/lib/hecks/hecks/custodian.bluebook +6 -1227
  177. data/lib/hecks/hecks/deploy.behaviors +1 -1
  178. data/lib/hecks/hecks/deploy.world +1 -3
  179. data/lib/hecks/hecks/hecks.behaviors +1 -1
  180. data/lib/hecks/hecks/hecks.hecksagon +53 -18
  181. data/lib/hecks/hecks/hecks.world +13 -10
  182. data/lib/hecks/hecks/rehearsal.bluebook +477 -0
  183. data/lib/hecks/hecks/site.behaviors +1 -1
  184. data/lib/hecks/hecks/site.world +0 -1
  185. data/lib/hecks/hecks/tooling.bluebook +1238 -0
  186. data/lib/hecks/indifferent_key.rb +20 -0
  187. data/lib/hecks/language/adapter.bluebook +13 -3
  188. data/lib/hecks/language/bluebook/aggregate.bluebook +23 -0
  189. data/lib/hecks/language/bluebook/bluebook.behaviors +23 -0
  190. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -0
  191. data/lib/hecks/language/bluebook/entity.bluebook +18 -0
  192. data/lib/hecks/language/bluebook/policy.behaviors +19 -0
  193. data/lib/hecks/language/bluebook/policy.bluebook +9 -1
  194. data/lib/hecks/language/bluebook/query.bluebook +1 -1
  195. data/lib/hecks/language/bluebook/vocabulary.bluebook +63 -4
  196. data/lib/hecks/language/hecksagon/domain_port.bluebook +1 -1
  197. data/lib/hecks/language/hecksagon/hecksagon.bluebook +22 -0
  198. data/lib/hecks/language/oidc.json +10 -0
  199. data/lib/hecks/language/port.bluebook +4 -4
  200. data/lib/hecks/literal.rb +12 -3
  201. data/lib/hecks/ports/key_vault.rb +7 -2
  202. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +21 -10
  203. data/lib/hecks/ports/persistence/plugins/era/postgres_era/boot.rb +4 -2
  204. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +2 -1
  205. data/lib/hecks/ports/persistence/repository_factory.rb +6 -3
  206. data/lib/hecks/projection_files/expression_tables.rb +7 -2
  207. data/lib/hecks/projections/deploy/box/compose.rb +6 -4
  208. data/lib/hecks/projections/deploy/box/edge.rb +62 -0
  209. data/lib/hecks/projections/deploy/box/logging.rb +99 -0
  210. data/lib/hecks/projections/deploy/box/roll_scripts.rb +16 -8
  211. data/lib/hecks/projections/deploy/box/settings/cdn.rb +40 -0
  212. data/lib/hecks/projections/deploy/box/settings/logs.rb +40 -0
  213. data/lib/hecks/projections/deploy/box/settings/patterns.rb +11 -0
  214. data/lib/hecks/projections/deploy/box/settings/types.rb +16 -1
  215. data/lib/hecks/projections/deploy/box/settings.rb +6 -2
  216. data/lib/hecks/projections/deploy/box/stack_yaml.rb +5 -2
  217. data/lib/hecks/projections/deploy/box/templates/Makefile.tmpl +16 -0
  218. data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +8 -4
  219. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +32 -0
  220. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +1 -1
  221. data/lib/hecks/projections/deploy/box/templates/edge.mk.tmpl +17 -0
  222. data/lib/hecks/projections/deploy/box/templates/edge.yaml.tmpl +65 -0
  223. data/lib/hecks/projections/deploy/box/templates/log-group.yaml.tmpl +12 -0
  224. data/lib/hecks/projections/deploy/box/templates/log-policy.yaml.tmpl +7 -0
  225. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +5 -4
  226. data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +5 -4
  227. data/lib/hecks/projections/deploy/box.rb +15 -3
  228. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  229. data/lib/hecks/projections/deploy/shared.rb +5 -2
  230. data/lib/hecks/projections/model/deviations.rb +2 -2
  231. data/lib/hecks/projections/model.rb +2 -2
  232. data/lib/hecks/projections/oidc.rb +13 -1
  233. data/lib/hecks/projections/shape.rb +5 -2
  234. data/lib/hecks/projections/site/cms_editor/attributes.rb +26 -6
  235. data/lib/hecks/projections/site/cms_editor/blocks.rb +157 -0
  236. data/lib/hecks/projections/site/cms_editor/checks.rb +14 -1
  237. data/lib/hecks/projections/site/cms_editor/clearing.rb +70 -0
  238. data/lib/hecks/projections/site/cms_editor/color.rb +124 -0
  239. data/lib/hecks/projections/site/cms_editor/composition.rb +42 -0
  240. data/lib/hecks/projections/site/cms_editor/destructive.rb +61 -0
  241. data/lib/hecks/projections/site/cms_editor/drafts.rb +102 -0
  242. data/lib/hecks/projections/site/cms_editor/hints.rb +74 -0
  243. data/lib/hecks/projections/site/cms_editor/icons.rb +86 -0
  244. data/lib/hecks/projections/site/cms_editor/key_kinds.rb +31 -0
  245. data/lib/hecks/projections/site/cms_editor/limits.rb +72 -0
  246. data/lib/hecks/projections/site/cms_editor/media.rb +1 -1
  247. data/lib/hecks/projections/site/cms_editor/moment.rb +34 -0
  248. data/lib/hecks/projections/site/cms_editor/palette.rb +115 -0
  249. data/lib/hecks/projections/site/cms_editor/pickers.rb +85 -0
  250. data/lib/hecks/projections/site/cms_editor/preview.rb +91 -0
  251. data/lib/hecks/projections/site/cms_editor/relations.rb +92 -0
  252. data/lib/hecks/projections/site/cms_editor/scale_checks.rb +66 -0
  253. data/lib/hecks/projections/site/cms_editor/scheduling.rb +150 -0
  254. data/lib/hecks/projections/site/cms_editor/schema.rb +96 -19
  255. data/lib/hecks/projections/site/cms_editor/setting.rb +52 -9
  256. data/lib/hecks/projections/site/cms_editor/targets.rb +86 -0
  257. data/lib/hecks/projections/site/cms_editor/templates/package.json.tmpl +18 -1
  258. data/lib/hecks/projections/site/cms_editor/templates/src/app.ts.tmpl +251 -57
  259. data/lib/hecks/projections/site/cms_editor/templates/src/assets.ts.tmpl +98 -0
  260. data/lib/hecks/projections/site/cms_editor/templates/src/auth/membership.ts.tmpl +32 -24
  261. data/lib/hecks/projections/site/cms_editor/templates/src/browser/app.css.tmpl +412 -0
  262. data/lib/hecks/projections/site/cms_editor/templates/src/browser/autosave.js.tmpl +144 -0
  263. data/lib/hecks/projections/site/cms_editor/templates/src/browser/blocks.js.tmpl +500 -0
  264. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_doc.js.tmpl +175 -0
  265. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_editor.js.tmpl +418 -0
  266. data/lib/hecks/projections/site/cms_editor/templates/src/browser/epoch.js.tmpl +61 -0
  267. data/lib/hecks/projections/site/cms_editor/templates/src/browser/forms.js.tmpl +210 -0
  268. data/lib/hecks/projections/site/cms_editor/templates/src/browser/lists.js.tmpl +40 -0
  269. data/lib/hecks/projections/site/cms_editor/templates/src/browser/main.js.tmpl +26 -0
  270. data/lib/hecks/projections/site/cms_editor/templates/src/browser/media_picker.js.tmpl +232 -0
  271. data/lib/hecks/projections/site/cms_editor/templates/src/browser/moments.js.tmpl +73 -0
  272. data/lib/hecks/projections/site/cms_editor/templates/src/browser/pickers.js.tmpl +152 -0
  273. data/lib/hecks/projections/site/cms_editor/templates/src/browser/preview.js.tmpl +98 -0
  274. data/lib/hecks/projections/site/cms_editor/templates/src/browser/shell.js.tmpl +140 -0
  275. data/lib/hecks/projections/site/cms_editor/templates/src/browser/toast.js.tmpl +66 -0
  276. data/lib/hecks/projections/site/cms_editor/templates/src/choices.ts.tmpl +99 -0
  277. data/lib/hecks/projections/site/cms_editor/templates/src/commands.ts.tmpl +78 -24
  278. data/lib/hecks/projections/site/cms_editor/templates/src/config.ts.tmpl +6 -0
  279. data/lib/hecks/projections/site/cms_editor/templates/src/flash.ts.tmpl +52 -0
  280. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +19 -2
  281. data/lib/hecks/projections/site/cms_editor/templates/src/media/handler.ts.tmpl +31 -8
  282. data/lib/hecks/projections/site/cms_editor/templates/src/names.ts.tmpl +36 -0
  283. data/lib/hecks/projections/site/cms_editor/templates/src/schema.ts.tmpl +131 -2
  284. data/lib/hecks/projections/site/cms_editor/templates/src/ui/availability.ts.tmpl +15 -0
  285. data/lib/hecks/projections/site/cms_editor/templates/src/ui/block_list.js.tmpl +213 -0
  286. data/lib/hecks/projections/site/cms_editor/templates/src/ui/blocks.ts.tmpl +275 -0
  287. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_model.js.tmpl +8 -5
  288. data/lib/hecks/projections/site/cms_editor/templates/src/ui/detail.ts.tmpl +88 -0
  289. data/lib/hecks/projections/site/cms_editor/templates/src/ui/dialogs.ts.tmpl +43 -0
  290. data/lib/hecks/projections/site/cms_editor/templates/src/ui/fields.ts.tmpl +229 -34
  291. data/lib/hecks/projections/site/cms_editor/templates/src/ui/html.ts.tmpl +40 -81
  292. data/lib/hecks/projections/site/cms_editor/templates/src/ui/icons.ts.tmpl +15 -0
  293. data/lib/hecks/projections/site/cms_editor/templates/src/ui/input.ts.tmpl +29 -12
  294. data/lib/hecks/projections/site/cms_editor/templates/src/ui/links.ts.tmpl +100 -0
  295. data/lib/hecks/projections/site/cms_editor/templates/src/ui/moments.ts.tmpl +44 -0
  296. data/lib/hecks/projections/site/cms_editor/templates/src/ui/outline.ts.tmpl +53 -17
  297. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pages.ts.tmpl +178 -88
  298. data/lib/hecks/projections/site/cms_editor/templates/src/ui/paging.ts.tmpl +82 -0
  299. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pickers.ts.tmpl +110 -0
  300. data/lib/hecks/projections/site/cms_editor/templates/src/ui/preview.ts.tmpl +87 -0
  301. data/lib/hecks/projections/site/cms_editor/templates/src/ui/related.ts.tmpl +97 -0
  302. data/lib/hecks/projections/site/cms_editor/templates/src/ui/schedule.ts.tmpl +160 -0
  303. data/lib/hecks/projections/site/cms_editor/templates/src/ui/shell.ts.tmpl +211 -0
  304. data/lib/hecks/projections/site/cms_editor/templates/src/ui/table.ts.tmpl +151 -0
  305. data/lib/hecks/projections/site/cms_editor/templates/src/ui/theme.js.tmpl +16 -0
  306. data/lib/hecks/projections/site/cms_editor/templates/src/ui/values.ts.tmpl +69 -0
  307. data/lib/hecks/projections/site/cms_editor/templates/src/ui/words.ts.tmpl +97 -0
  308. data/lib/hecks/projections/site/cms_editor/templates/src/ui/world.ts.tmpl +35 -0
  309. data/lib/hecks/projections/site/cms_editor/templates/tsconfig.json.tmpl +2 -1
  310. data/lib/hecks/projections/site/cms_editor/theme.rb +29 -0
  311. data/lib/hecks/projections/site/cms_editor/theme_checks.rb +57 -0
  312. data/lib/hecks/projections/site/cms_editor.rb +49 -23
  313. data/lib/hecks/projections/site/role_probe.rb +2 -0
  314. data/lib/hecks/projections/site/site_root/workflow.rb +5 -0
  315. data/lib/hecks/projector/cli_audience.rb +22 -1
  316. data/lib/hecks/projector/cli_projector/listing.rb +1 -1
  317. data/lib/hecks/projector/cli_projector/option_specs.rb +1 -1
  318. data/lib/hecks/projector/cli_projector/usage.rb +15 -4
  319. data/lib/hecks/projector/docs_projector/closing.rb +1 -1
  320. data/lib/hecks/projector/docs_projector.rb +1 -1
  321. data/lib/hecks/projector/exporter/bindings.rb +40 -10
  322. data/lib/hecks/projector/exporter/capability_values.rb +55 -0
  323. data/lib/hecks/projector/narrate_projector/reactions.rb +1 -1
  324. data/lib/hecks/projector/target.rb +11 -0
  325. data/lib/hecks/projector.rb +32 -5
  326. data/lib/hecks/quality_control/adapters/github_checks.rb +12 -8
  327. data/lib/hecks/quality_control/adapters/github_issues.rb +8 -4
  328. data/lib/hecks/quality_control/cli/qa_open_pr.rb +2 -2
  329. data/lib/hecks/quality_control/cli/qa_sweep/checks/shrinking.rb +1 -1
  330. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +1 -1
  331. data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
  332. data/lib/hecks/quality_control/quality_control.bluebook +6 -2
  333. data/lib/hecks/release/approval.rb +64 -0
  334. data/lib/hecks/router/{door.rb → aggregate_reader.rb} +5 -4
  335. data/lib/hecks/router/namespace_installer.rb +16 -16
  336. data/lib/hecks/router.rb +1 -1
  337. data/lib/hecks/runtime/best_effort.rb +22 -0
  338. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -2
  339. data/lib/hecks/runtime/command_interpreter.rb +3 -2
  340. data/lib/hecks/runtime/command_rules/emission.rb +1 -1
  341. data/lib/hecks/runtime/dispatcher.rb +4 -4
  342. data/lib/hecks/runtime/entity_element/location.rb +1 -1
  343. data/lib/hecks/runtime/event.rb +5 -0
  344. data/lib/hecks/runtime/identity.rb +7 -6
  345. data/lib/hecks/runtime/interpreting.rb +2 -1
  346. data/lib/hecks/runtime/invocation_builder.rb +1 -1
  347. data/lib/hecks/runtime/loader/boot_steps.rb +8 -5
  348. data/lib/hecks/runtime/loader.rb +10 -10
  349. data/lib/hecks/runtime/policy_interpreter/arguments.rb +7 -4
  350. data/lib/hecks/runtime/policy_interpreter/fan_out.rb +3 -3
  351. data/lib/hecks/runtime/policy_interpreter.rb +6 -6
  352. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -1
  353. data/lib/hecks/runtime/reaction_invocation.rb +1 -1
  354. data/lib/hecks/runtime/rebuild_sweep.rb +11 -1
  355. data/lib/hecks/runtime/registry/ask_verification.rb +35 -0
  356. data/lib/hecks/runtime/registry/hecksagon_rules.rb +10 -0
  357. data/lib/hecks/runtime/registry/merging.rb +1 -1
  358. data/lib/hecks/runtime/registry/providers.rb +16 -0
  359. data/lib/hecks/runtime/registry/read_repository.rb +6 -4
  360. data/lib/hecks/runtime/registry/verification.rb +4 -1
  361. data/lib/hecks/runtime/registry/world_defaults.rb +29 -0
  362. data/lib/hecks/runtime/registry.rb +1 -1
  363. data/lib/hecks/runtime/saga_interpreter/compensation.rb +1 -1
  364. data/lib/hecks/runtime/saga_interpreter/delivery.rb +4 -4
  365. data/lib/hecks/runtime/saga_interpreter/transitions.rb +1 -1
  366. data/lib/hecks/runtime/saga_interpreter.rb +3 -3
  367. data/lib/hecks/runtime/tenant_check.rb +5 -4
  368. data/lib/hecks/runtime/value/coercion.rb +2 -2
  369. data/lib/hecks/runtime/value/validation.rb +16 -4
  370. data/lib/hecks/runtime.rb +9 -9
  371. data/lib/hecks/rust_build/host.rb +11 -4
  372. data/lib/hecks/rust_build/prebuilt_binary.rb +101 -0
  373. data/lib/hecks/rust_build/project_rust/target_ir.rb +1 -1
  374. data/lib/hecks/rust_build/wasm.rb +1 -1
  375. data/lib/hecks/rust_build/wasm_browser.rb +1 -1
  376. data/lib/hecks/rust_build.rb +11 -0
  377. data/lib/hecks/served_options.rb +53 -0
  378. data/lib/hecks/site/bluebook/site.bluebook +1 -0
  379. data/lib/hecks/storehouse/audit.rb +1 -1
  380. data/lib/hecks/storehouse/dispatching.rb +8 -6
  381. data/lib/hecks/storehouse/introspection.rb +2 -2
  382. data/lib/hecks/storehouse/querying.rb +6 -4
  383. data/lib/hecks/storehouse.rb +4 -4
  384. data/lib/hecks/tools/ci_gate_decision.rb +35 -0
  385. data/lib/hecks/tools/ci_gates.rb +20 -1
  386. data/lib/hecks/tools/comment_style/source_file.rb +4 -1
  387. data/lib/hecks/tools/deploy_recipe_lint/fixtures.rb +4 -1
  388. data/lib/hecks/tools/lanes/files.rb +8 -0
  389. data/lib/hecks/tools/lanes/prebuilt_workflow.rb +81 -0
  390. data/lib/hecks/tools/lanes/rows.rb +6 -0
  391. data/lib/hecks/tools/lanes.rb +1 -0
  392. data/lib/hecks/tools/oidc_manifests.rb +4 -5
  393. data/lib/hecks/tools/regeneration_run.rb +11 -4
  394. data/lib/hecks/tools/release_only.rb +111 -0
  395. data/lib/hecks/tools/site_routes/editor_files.rb +12 -5
  396. data/lib/hecks/tools/tenant_provisioning.rb +2 -2
  397. data/lib/hecks/tools/tools_doc.rb +4 -4
  398. data/lib/hecks/version.rb +1 -1
  399. data/lib/hecks/vocabulary.rb +19 -5
  400. data/lib/hecks.rb +15 -15
  401. data/lib/rubocop/cop/hecks/env_dump.rb +69 -0
  402. data/lib/rubocop/cop/hecks/monotonic_duration.rb +53 -0
  403. data/lib/rubocop/cop/hecks/no_wall_clock_in_runtime.rb +34 -0
  404. data/lib/rubocop/cop/hecks/raw_shell_interpolation.rb +79 -0
  405. data/lib/rubocop/cop/hecks/sleep_in_spec.rb +34 -0
  406. data/lib/rubocop/cop/hecks/swallowed_rescue.rb +37 -0
  407. data/lib/rubocop/cop/hecks/symbol_string_key_mix.rb +50 -0
  408. data/lib/rubocop/cop/hecks/untyped_raise.rb +37 -0
  409. data/prebuilt/9ee54e2f0d4d6d3fe046827779b221e02de665fba911a048ae5dd8b8fbfb7d90.marshal +0 -0
  410. data/prebuilt/d74a00e996222c1d8b676d5959092effc46b4dc59e0555f3bf9554ac58e3094a.marshal +0 -0
  411. data/prebuilt/verdicts-df19f35868d3284a70f55e42dcef87ef4e65e6a678e5135f72e1f6cff4c1357b.json +1 -0
  412. data/rust/.gitignore +6 -0
  413. data/rust/Cargo.toml +6 -0
  414. data/rust/build/src/binding_pass.rs +103 -0
  415. data/rust/build/src/cargo_sync.rs +6 -0
  416. data/rust/build/src/lineage_pass.rs +3 -3
  417. data/rust/build/src/main.rs +17 -5
  418. data/rust/build/src/pipeline.rs +48 -5
  419. data/rust/build/src/seams_pass.rs +123 -0
  420. data/rust/build/src/translation_pass.rs +167 -0
  421. data/rust/codegen/src/asks.rs +103 -0
  422. data/rust/codegen/src/commands.rs +13 -8
  423. data/rust/codegen/src/constraints.rs +25 -0
  424. data/rust/codegen/src/domain_generator.rs +2 -0
  425. data/rust/codegen/src/expr_emitter.rs +13 -0
  426. data/rust/codegen/src/json_codec.rs +54 -15
  427. data/rust/codegen/src/main.rs +1 -0
  428. data/rust/codegen/src/mutations.rs +128 -3
  429. data/rust/codegen/src/queries.rs +11 -2
  430. data/rust/codegen/src/reactions.rs +1 -1
  431. data/rust/codegen/src/types.rs +166 -8
  432. data/rust/host/HECKS_RELEASE +1 -1
  433. data/rust/host/src/api.rs +4 -4
  434. data/rust/host/src/bin/validate_harness.rs +43 -0
  435. data/rust/host/src/boundary_fuzz/expr_json.rs +2 -2
  436. data/rust/host/src/boundary_fuzz/ir.rs +1 -0
  437. data/rust/host/src/boundary_fuzz/server.rs +1 -1
  438. data/rust/host/src/checkout.rs +35 -19
  439. data/rust/host/src/commerce_ir.rs +408 -2
  440. data/rust/host/src/expr_json.rs +118 -0
  441. data/rust/host/src/extension.rs +13 -1
  442. data/rust/host/src/lib.rs +1 -0
  443. data/rust/host/src/mint.rs +1 -1
  444. data/rust/host/src/payments/tests.rs +27 -1
  445. data/rust/host/src/payments.rs +12 -4
  446. data/rust/host/src/platform_signin/attack_tests.rs +1100 -0
  447. data/rust/host/src/platform_signin/config.rs +512 -0
  448. data/rust/host/src/platform_signin/cookie.rs +237 -0
  449. data/rust/host/src/platform_signin/extension.rs +190 -0
  450. data/rust/host/src/platform_signin/fetcher.rs +322 -0
  451. data/rust/host/src/platform_signin/handler.rs +251 -0
  452. data/rust/host/src/platform_signin/handler_tests.rs +554 -0
  453. data/rust/host/src/platform_signin/jti.rs +196 -0
  454. data/rust/host/src/platform_signin/jwks.rs +542 -0
  455. data/rust/host/src/platform_signin/jws.rs +249 -0
  456. data/rust/host/src/platform_signin/known_answer_tests.rs +237 -0
  457. data/rust/host/src/platform_signin/mapping_tests.rs +254 -0
  458. data/rust/host/src/platform_signin/mod.rs +75 -0
  459. data/rust/host/src/platform_signin/mode_tests.rs +382 -0
  460. data/rust/host/src/platform_signin/pages.rs +204 -0
  461. data/rust/host/src/platform_signin/query.rs +233 -0
  462. data/rust/host/src/platform_signin/session.rs +151 -0
  463. data/rust/host/src/platform_signin/settings.rs +385 -0
  464. data/rust/host/src/platform_signin/strict_json.rs +351 -0
  465. data/rust/host/src/platform_signin/test_support.rs +235 -0
  466. data/rust/host/src/platform_signin/ttl_tests.rs +94 -0
  467. data/rust/host/src/platform_signin/verify.rs +604 -0
  468. data/rust/host/src/platform_signin/wiring_support.rs +319 -0
  469. data/rust/host/src/query_step.rs +22 -4
  470. data/rust/host/src/reference_validate.rs +653 -108
  471. data/rust/host/src/resend.rs +2 -0
  472. data/rust/host/src/server.rs +19 -6
  473. data/rust/host/src/ui_schema.rs +132 -5
  474. data/rust/host/src/web/newsletter.rs +7 -8
  475. data/rust/host/src/web/newsletter_send.rs +38 -13
  476. data/rust/host/src/web/registrations.rs +229 -26
  477. data/rust/host/src/web.rs +53 -9
  478. data/rust/parser/src/emit.rs +38 -6
  479. data/rust/parser/src/expr/ast_json.rs +7 -1
  480. data/rust/parser/src/expr/resolver.rs +171 -6
  481. data/rust/parser/src/ir.rs +7 -0
  482. data/rust/parser/src/keywords.rs +24 -0
  483. data/rust/parser/src/main.rs +3 -0
  484. data/rust/parser/src/parse/aggregate.rs +2 -1
  485. data/rust/parser/src/parse/chapter.rs +2 -2
  486. data/rust/parser/src/parse/command.rs +2 -0
  487. data/rust/parser/src/parse/hecksagon.rs +5 -2
  488. data/rust/parser/src/parse/lifecycle.rs +81 -0
  489. data/rust/parser/src/parse/mod.rs +15 -1
  490. data/rust/parser/src/parse/policy.rs +13 -2
  491. data/rust/parser/src/parse/value_object.rs +34 -17
  492. data/rust/parser/src/ruby_value.rs +1 -1
  493. data/rust/src/exemplar/commands.rs +4 -4
  494. data/rust/src/exemplar/constraints.rs +1 -1
  495. data/rust/src/exemplar/json.rs +43 -1
  496. data/rust/src/kernel/cli.rs +7 -5
  497. data/rust/src/kernel/dispatch.rs +2 -2
  498. data/rust/src/kernel/dispatch_guard_tests.rs +1 -1
  499. data/rust/src/kernel/expr.rs +66 -3
  500. data/rust/src/kernel/expression_operators/arithmetic.rs +130 -5
  501. data/rust/src/kernel/expression_operators/text.rs +26 -2
  502. data/rust/src/kernel/json.rs +15 -0
  503. data/rust/src/kernel/mod.rs +1 -1
  504. data/rust/src/kernel/orchestrate.rs +54 -5
  505. data/rust/src/kernel/orchestrate_guard_tests.rs +67 -0
  506. data/rust/src/kernel/pattern.rs +60 -0
  507. data/rust/src/kernel/reference_lookup.rs +90 -7
  508. metadata +178 -38
  509. data/lib/hecks/doors/cli_door/nesting.rb +0 -34
  510. data/lib/hecks/doors/cli_door/shorthand.rb +0 -85
  511. data/lib/hecks/doors/cli_door.rb +0 -124
  512. data/lib/hecks/doors/cli_runner/answers.rb +0 -138
  513. data/lib/hecks/doors/cli_runner/resolution.rb +0 -146
  514. data/lib/hecks/doors/cli_runner/settling.rb +0 -115
  515. data/lib/hecks/doors/cli_runner/streaming.rb +0 -90
  516. data/lib/hecks/doors/cli_runner/suggestions.rb +0 -43
  517. data/lib/hecks/doors/cli_runner.rb +0 -66
  518. data/lib/hecks/doors/command_request.rb +0 -119
  519. data/lib/hecks/doors/handle.rb +0 -208
  520. data/lib/hecks/doors/json_door.rb +0 -132
  521. data/lib/hecks/doors/launcher_options.rb +0 -247
  522. data/lib/hecks/doors/mcp_door/properties.rb +0 -47
  523. data/lib/hecks/doors/mcp_door/tool_calls.rb +0 -113
  524. data/lib/hecks/doors/mcp_door/tool_definitions/auditing.rb +0 -51
  525. data/lib/hecks/doors/mcp_door/tool_definitions/calling.rb +0 -69
  526. data/lib/hecks/doors/mcp_door/tool_definitions/reading.rb +0 -97
  527. data/lib/hecks/doors/mcp_door/tool_definitions.rb +0 -15
  528. data/lib/hecks/doors/mcp_door.rb +0 -156
  529. data/lib/hecks/doors/mcp_door_scope/argument_policy.rb +0 -92
  530. data/lib/hecks/doors/mcp_door_scope/env_problems.rb +0 -55
  531. data/lib/hecks/doors/mcp_door_scope/presenter.rb +0 -78
  532. data/lib/hecks/doors/mcp_door_scope.rb +0 -201
  533. data/lib/hecks/doors/mcp_guide_cache.rb +0 -75
  534. data/lib/hecks/doors/ruby_door/aggregate_door.rb +0 -162
  535. data/lib/hecks/doors/ruby_door/chapter.rb +0 -89
  536. data/lib/hecks/doors/ruby_door.rb +0 -80
  537. data/lib/hecks/doors/usage_cache.rb +0 -139
  538. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_widget.js.tmpl +0 -282
  539. data/lib/hecks/projections/site/cms_editor/templates/src/ui/media_picker.js.tmpl +0 -109
  540. data/prebuilt/c28d6d9db14eea3e17161e9d9c262efee30b256cf97693a7cfd552bce24b2f7f.marshal +0 -0
  541. data/prebuilt/cb375cba61ca0dbe8ddb9739c306510fe05e5f4b7a9b6955caff8224c63ea0e0.marshal +0 -0
  542. data/prebuilt/verdicts-d6d584ad43b5f3cebf88aafbbdbf024c38574acf18e06adffbc555f7786b1c2f.json +0 -1
@@ -8,6 +8,15 @@
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
+ @@LOG_EXIT_CODE@@
14
+ #
15
+ # Before anything changes, the roll names the point the database can be restored to: the instance, the
16
+ # moment this roll starts, and how long its continuous backups are kept. A roll that mints a new era is
17
+ # a one-way door, so this is the rollback anchor, and it costs nothing (a point-in-time restore uses the
18
+ # backups the instance already keeps, where a manual snapshot would be stored until someone deleted it).
19
+ # SKIP_RESTORE_ANCHOR=1 skips it.
11
20
  #
12
21
  # Before the roll replaces a container, the box saves that container's log to
13
22
  # /var/log/hecks-captures/<container>-<UTC timestamp>.log and keeps the newest 14 per container
@@ -15,6 +24,7 @@
15
24
  # cannot run, the roll goes on. SKIP_LOG_CAPTURE=1 skips it; LOG_CAPTURE_SERVICES="a b" limits it to
16
25
  # those services (deploy-service.sh sets the one it rolls).
17
26
  #
27
+ @@LOG_NOTE@@
18
28
  @@USAGE@@
19
29
  set -euo pipefail
20
30
  HERE=$(cd "$(dirname "$0")" && pwd)
@@ -29,6 +39,28 @@ DB_HOST=$(out "$RDS_STACK" DbEndpoint)
29
39
  @@DB_SECRET_LINE@@
30
40
  [ -n "$BOX" ] && [ "$BOX" != None ] || { echo "no instance in stack $BOX_STACK" >&2; exit 40; }
31
41
  ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
42
+ @@LOG_GROUP_CHECK@@
43
+
44
+ if [ "${SKIP_RESTORE_ANCHOR:-}" = 1 ]; then
45
+ echo "==> skipping the restore anchor (SKIP_RESTORE_ANCHOR=1)"
46
+ else
47
+ DB_ID=${DB_HOST%%.*}
48
+ BACKUPS=$(aws rds describe-db-instances --db-instance-identifier "$DB_ID" \
49
+ --query 'DBInstances[0].[BackupRetentionPeriod,LatestRestorableTime]' --output text) || BACKUPS=
50
+ read -r KEPT_DAYS RESTORABLE <<< "$BACKUPS"
51
+ case "${KEPT_DAYS:-}" in ''|*[!0-9]*)
52
+ echo "==> no restore anchor: could not read the backups of $DB_ID (SKIP_RESTORE_ANCHOR=1 rolls without one)" >&2
53
+ exit 43 ;;
54
+ esac
55
+ if [ "$KEPT_DAYS" -lt "${MIN_BACKUP_RETENTION_DAYS:-3}" ]; then
56
+ echo "==> no restore anchor: $DB_ID keeps backups $KEPT_DAYS day(s), fewer than ${MIN_BACKUP_RETENTION_DAYS:-3}" >&2
57
+ exit 43
58
+ fi
59
+ ANCHOR_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
60
+ echo "==> restore anchor: $DB_ID restorable to $ANCHOR_AT (latest $RESTORABLE, backups kept $KEPT_DAYS days)"
61
+ echo " to return: aws rds restore-db-instance-to-point-in-time --source-db-instance-identifier $DB_ID \\"
62
+ echo " --target-db-instance-identifier $DB_ID-restore --restore-time $ANCHOR_AT"
63
+ fi
32
64
 
33
65
  WORK=$(mktemp -d); trap 'rm -rf "$WORK"' EXIT
34
66
 
@@ -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
@@ -0,0 +1,17 @@
1
+ # GENERATED by hecks deploy project -- re-run it to refresh this file rather than hand-editing.
2
+ # Included by the Makefile beside it; the aliases come from the domain's deployed_to("AwsBox")
3
+ # block (cdn).
4
+
5
+ EDGE_STACK = @@EDGE_STACK@@
6
+
7
+ .PHONY: edge
8
+
9
+ # Creates or updates the CloudFront edge stack in front of the box (run `make stacks` first: the
10
+ # box's AppOriginDomain output is the distribution's origin). The certificate is never created
11
+ # here: issue it in ACM in us-east-1, validate its DNS records, then pass its ARN.
12
+ # make edge CERTIFICATE_ARN=arn:aws:acm:us-east-1:<account>:certificate/<id>
13
+ edge:
14
+ @test -n "$(CERTIFICATE_ARN)" || { echo "usage: make edge CERTIFICATE_ARN=arn:aws:acm:us-east-1:..." >&2; exit 1; }
15
+ aws cloudformation deploy --region @@REGION@@ --template-file edge.yaml --stack-name $(EDGE_STACK) \
16
+ --parameter-overrides CertificateArn=$(CERTIFICATE_ARN) \
17
+ AppOriginDomain=$$(aws cloudformation describe-stacks --region @@REGION@@ --stack-name $(BOX_STACK) --query "Stacks[0].Outputs[?OutputKey=='AppOriginDomain'].OutputValue" --output text)
@@ -0,0 +1,65 @@
1
+ AWSTemplateFormatVersion: "2010-09-09"
2
+ Description: >
3
+ The CloudFront edge of @@STACK@@: one distribution in front of the app box. Its only origin is the
4
+ box's public DNS name over plain http on port 80, and every request to it carries the origin secret
5
+ header the box's proxy checks. Nothing is cached: the origin decides what a response means.
6
+
7
+ Parameters:
8
+ AppOriginDomain:
9
+ Type: String
10
+ AllowedPattern: ".+"
11
+ ConstraintDescription: the box stack's AppOriginDomain output
12
+ Description: DNS name of the app box, the distribution's only origin.
13
+ CertificateArn:
14
+ Type: String
15
+ AllowedPattern: "arn:aws:acm:us-east-1:.+"
16
+ ConstraintDescription: an ACM certificate in us-east-1 that covers every alias
17
+ Description: >
18
+ An issued ACM certificate in us-east-1 that covers every name in DomainNames. It is never created
19
+ here: DNS validation is a step an operator performs.
20
+ DomainNames:
21
+ Type: CommaDelimitedList
22
+ Default: "@@ALIASES@@"
23
+ Description: The host names the distribution answers on.
24
+
25
+ Resources:
26
+ Distribution:
27
+ Type: AWS::CloudFront::Distribution
28
+ DeletionPolicy: Retain
29
+ UpdateReplacePolicy: Retain
30
+ Properties:
31
+ DistributionConfig:
32
+ Enabled: true
33
+ HttpVersion: http2
34
+ IPV6Enabled: true
35
+ Aliases: !Ref DomainNames
36
+ ViewerCertificate:
37
+ AcmCertificateArn: !Ref CertificateArn
38
+ SslSupportMethod: sni-only
39
+ MinimumProtocolVersion: TLSv1.2_2021
40
+ Origins:
41
+ - Id: BoxOrigin
42
+ DomainName: !Ref AppOriginDomain
43
+ OriginCustomHeaders:
44
+ # Resolved by CloudFormation from Secrets Manager when the stack deploys, so the value
45
+ # never appears on a command line or in this file.
46
+ - HeaderName: @@ORIGIN_HEADER@@
47
+ HeaderValue: "{{resolve:secretsmanager:@@ORIGIN_SECRET@@:SecretString}}"
48
+ CustomOriginConfig:
49
+ OriginProtocolPolicy: http-only
50
+ OriginSSLProtocols: [TLSv1.2]
51
+ DefaultCacheBehavior:
52
+ TargetOriginId: BoxOrigin
53
+ ViewerProtocolPolicy: redirect-to-https
54
+ AllowedMethods: [GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE]
55
+ CachedMethods: [GET, HEAD]
56
+ Compress: true
57
+ CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # Managed-CachingDisabled
58
+ OriginRequestPolicyId: 216adef6-5c7f-47e4-b989-5492eafa07d3 # Managed-AllViewer
59
+
60
+ Outputs:
61
+ DistributionId:
62
+ Value: !Ref Distribution
63
+ DistributionDomainName:
64
+ Description: Point each alias's DNS record (CNAME or alias) at this name.
65
+ Value: !GetAtt Distribution.DomainName
@@ -0,0 +1,12 @@
1
+ # Every container's log, one stream per service. The group is made here and not by Docker
2
+ # (awslogs-create-group is "false"), so the role below needs no right to create groups, and the
3
+ # retention is a declaration. A production group outlives the stack: the evidence a container
4
+ # wrote must not go with a deleted stack.
5
+ BoxLogGroup:
6
+ Type: AWS::Logs::LogGroup
7
+ DeletionPolicy: !If [IsProduction, Retain, Delete]
8
+ UpdateReplacePolicy: !If [IsProduction, Retain, Delete]
9
+ Properties:
10
+ LogGroupName: @@LOG_GROUP@@
11
+ RetentionInDays: @@RETENTION_DAYS@@
12
+
@@ -0,0 +1,7 @@
1
+ - PolicyName: write-logs
2
+ PolicyDocument:
3
+ Version: "2012-10-17"
4
+ Statement:
5
+ - Effect: Allow
6
+ Action: [logs:CreateLogStream, logs:PutLogEvents, logs:DescribeLogStreams]
7
+ Resource: !GetAtt BoxLogGroup.Arn
@@ -13,6 +13,7 @@
13
13
  # When the world names origin_env, the named origin secret is compared with those variables in the
14
14
  # task definition and the render refuses on any difference (Caddy checks the CDN's header against
15
15
  # the secret; the containers check it against their own copy). Values are never printed.
16
+ @@COMPOSE_LOG_NOTE@@
16
17
  set -euo pipefail
17
18
  HERE=$(cd "$(dirname "$0")" && pwd)
18
19
  DB_HOST=${1:?rds endpoint}
@@ -36,7 +37,7 @@ if [ -n "$ORIGIN_ENV" ]; then
36
37
  fi
37
38
 
38
39
  jq --argjson def "$DEF" --arg host "$DB_HOST" --arg secret "$DB_SECRET" '
39
- def log: {driver: "json-file", options: {"max-size": "10m", "max-file": "3"}};
40
+ @@LOG_DEF@@
40
41
  def env(c): ((c.environment // []) | map({(.name): .value}) | add // {})
41
42
  | (if has("DB_HOST") then .DB_HOST = $host | .DB_SECRET_ARN = $secret else . end);
42
43
  . as $in
@@ -44,16 +45,16 @@ jq --argjson def "$DEF" --arg host "$DB_HOST" --arg secret "$DB_SECRET" '
44
45
  ($in.services | with_entries(.value |= (. as $s
45
46
  | ($def | map(select(.name == $s.name)) | first) as $c
46
47
  | if $c == null then error("task definition has no container named " + $s.name) else
47
- {image: $c.image, network_mode: "host", restart: "unless-stopped", logging: log,
48
+ {image: $c.image, network_mode: "host", restart: "unless-stopped", logging: @@LOG_SERVICE@@,
48
49
  environment: ({PORT: ($s.port | tostring)} + env($c))}
49
50
  + (if (($c.secrets // []) | length) > 0 then {env_file: [($s.name + ".secrets.env")]} else {} end)
50
51
  end)))
51
52
  + {caddy: ({image: "@@PROXY_IMAGE@@", network_mode: "host", restart: "unless-stopped",
52
- volumes: ["./Caddyfile:/etc/caddy/Caddyfile:ro", "./caddy-extra:/etc/caddy/extra:ro"], logging: log}
53
+ volumes: ["./Caddyfile:/etc/caddy/Caddyfile:ro", "./caddy-extra:/etc/caddy/extra:ro"], logging: @@LOG_CADDY@@}
53
54
  + (if $in.origin then {env_file: ["caddy.secrets.env"]} else {} end))}
54
55
  + (if $in.tunnel then {cloudflared: {image: $in.tunnel.image, network_mode: "host", restart: "unless-stopped",
55
56
  command: ["tunnel", "--no-autoupdate", "--url", $in.tunnel.url, "run"],
56
- env_file: ["cloudflared.secrets.env"], logging: log}} else {} end))}' \
57
+ env_file: ["cloudflared.secrets.env"], logging: @@LOG_TUNNEL@@}} else {} end))}' \
57
58
  "$HERE/services.json" > compose.json
58
59
 
59
60
  jq --argjson def "$DEF" '. as $in
@@ -6,6 +6,7 @@
6
6
  # Tags default to "latest". Every container gets DB_HOST, DB_NAME, DB_SECRET_ARN and PORT, and runs on the
7
7
  # box's own network, so the proxy reaches each one on 127.0.0.1:<port>. Secrets named in services.json
8
8
  # are written to secrets.json for fetch-secrets.sh to resolve on the box.
9
+ @@COMPOSE_LOG_NOTE@@
9
10
  set -euo pipefail
10
11
  HERE=$(cd "$(dirname "$0")" && pwd)
11
12
  DB_HOST=${1:?rds endpoint}
@@ -18,20 +19,20 @@ ECR="$ACCOUNT.dkr.ecr.$REGION.amazonaws.com"
18
19
  TAGS=$(printf '%s\n' "$@" | jq -R 'select(length > 0) | split("=") | {(.[0]): .[1]}' | jq -s 'add // {}')
19
20
 
20
21
  jq --arg ecr "$ECR" --argjson tags "$TAGS" --arg host "$DB_HOST" --arg secret "$DB_SECRET" --arg db "@@DB_NAME@@" '
21
- def log: {driver: "json-file", options: {"max-size": "10m", "max-file": "3"}};
22
+ @@LOG_DEF@@
22
23
  . as $in
23
24
  | {services: (
24
25
  ($in.services | with_entries(.value |= (
25
26
  {image: ($ecr + "/" + .repository + ":" + ($tags[.name] // "latest")),
26
- network_mode: "host", restart: "unless-stopped", logging: log,
27
+ network_mode: "host", restart: "unless-stopped", logging: @@LOG_SERVICE@@,
27
28
  environment: ({PORT: (.port | tostring), DB_HOST: $host, DB_NAME: $db, DB_SECRET_ARN: $secret} + .env)}
28
29
  + (if (.secrets | length) > 0 then {env_file: [(.name + ".secrets.env")]} else {} end))))
29
30
  + {caddy: ({image: "@@PROXY_IMAGE@@", network_mode: "host", restart: "unless-stopped",
30
- volumes: ["./Caddyfile:/etc/caddy/Caddyfile:ro", "./caddy-extra:/etc/caddy/extra:ro"], logging: log}
31
+ volumes: ["./Caddyfile:/etc/caddy/Caddyfile:ro", "./caddy-extra:/etc/caddy/extra:ro"], logging: @@LOG_CADDY@@}
31
32
  + (if $in.origin then {env_file: ["caddy.secrets.env"]} else {} end))}
32
33
  + (if $in.tunnel then {cloudflared: {image: $in.tunnel.image, network_mode: "host", restart: "unless-stopped",
33
34
  command: ["tunnel", "--no-autoupdate", "--url", $in.tunnel.url, "run"],
34
- env_file: ["cloudflared.secrets.env"], logging: log}} else {} end))}' \
35
+ env_file: ["cloudflared.secrets.env"], logging: @@LOG_TUNNEL@@}} else {} end))}' \
35
36
  "$HERE/services.json" > compose.json
36
37
 
37
38
  jq '. as $in
@@ -5,10 +5,12 @@ require_relative "box/hosting"
5
5
  require_relative "box/roll_recipe"
6
6
  require_relative "box/caddy"
7
7
  require_relative "box/access"
8
+ require_relative "box/logging"
8
9
  require_relative "box/stack_yaml"
9
10
  require_relative "box/compose"
10
11
  require_relative "box/shared_database"
11
12
  require_relative "box/roll_scripts"
13
+ require_relative "box/edge"
12
14
 
13
15
  module Hecks
14
16
  module Projections
@@ -21,10 +23,12 @@ module Hecks
21
23
  extend Projector::Target
22
24
  extend Caddy
23
25
  extend Access
26
+ extend Logging
24
27
  extend StackYaml
25
28
  extend Compose
26
29
  extend SharedDatabase
27
30
  extend RollScripts
31
+ extend Edge
28
32
 
29
33
  projects_as :aws_box, needs_world: true, emits: :files
30
34
 
@@ -123,7 +127,15 @@ module Hecks
123
127
  volume_gb: { value: settings.fetch(:volume_gb, 30) },
124
128
  database_class: { value: settings.fetch(:database_class, "db.t4g.small") },
125
129
  storage_gb: { value: settings.fetch(:storage_gb, 20) }
126
- }
130
+ }.merge(cdn_arguments(settings[:cdn]))
131
+ end
132
+
133
+ # @param cdn [Hash{Symbol => Object}, nil] the world's `cdn` setting
134
+ # @return [Hash{Symbol => Array}] the `cdn_aliases` argument, or nothing without a `cdn`
135
+ def cdn_arguments(cdn)
136
+ return {} unless cdn.is_a?(Hash) && cdn[:aliases].is_a?(Array)
137
+
138
+ { cdn_aliases: cdn[:aliases].map { |name| { value: name } } }
127
139
  end
128
140
 
129
141
  # @param plan [Settings::Plan] the resolved settings
@@ -136,8 +148,8 @@ module Hecks
136
148
  "render-compose.sh" => render_compose_sh(plan, region),
137
149
  "fetch-secrets.sh" => File.read(File.join(TEMPLATE_DIR, "fetch-secrets.sh")),
138
150
  "deploy-box.sh" => deploy_box_sh(plan, region),
139
- "Makefile" => makefile(plan)
140
- }.merge(database_files(plan)).merge(migration_files(plan))
151
+ "Makefile" => makefile(plan, region)
152
+ }.merge(database_files(plan)).merge(migration_files(plan)).merge(edge_files(plan, region))
141
153
  .then { |files| Hosting.extend_files(files, plan: plan, region: region) }
142
154
  end
143
155
 
@@ -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