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
@@ -16,6 +16,9 @@ module Hecks
16
16
  # it, a span with no complete day, or a failed `aws` call is refused with a reason, which the
17
17
  # runtime records on the check.
18
18
  class CostExplorer
19
+ # Raised when the budget is exceeded or Cost Explorer cannot be read.
20
+ class Error < RuntimeError; end
21
+
19
22
  # Days in an average month, for scaling a daily mean to a monthly rate.
20
23
  DAYS_IN_MONTH = 30.4375
21
24
 
@@ -35,17 +38,17 @@ module Hecks
35
38
  # first day to count (`YYYY-MM-DD`), each a value object or a plain value
36
39
  # @return [Hash{Symbol => Hash}] `report:` the rate against the budget, the shape
37
40
  # `CostCheck.Pass` takes
38
- # @raise [RuntimeError] when no complete day has passed since `since`, `aws` fails, or the
41
+ # @raise [Error] when no complete day has passed since `since`, `aws` fails, or the
39
42
  # rate is over the budget
40
43
  def measure(**held)
41
44
  budget = Integer(plain(held[:budget]))
42
45
  first = Date.iso8601(plain(held[:since]).to_s)
43
46
  last = today
44
- raise "no complete day since #{first}; today (#{last}) is still being billed" if first >= last
47
+ raise Error, "no complete day since #{first}; today (#{last}) is still being billed" if first >= last
45
48
 
46
49
  days = daily_costs(first, last)
47
50
  line = report_line(first, last, days, budget)
48
- raise "over budget: #{line}" if monthly_rate(days) > budget
51
+ raise Error, "over budget: #{line}" if monthly_rate(days) > budget
49
52
 
50
53
  { report: { value: line } }
51
54
  end
@@ -62,10 +65,10 @@ module Hecks
62
65
  "--granularity", "DAILY", "--metrics", "UnblendedCost",
63
66
  "--group-by", "Type=DIMENSION,Key=SERVICE", "--output", "json"
64
67
  )
65
- raise "aws ce get-cost-and-usage failed: #{err.strip.empty? ? out.strip : err.strip}" unless status.success?
68
+ raise Error, "aws ce get-cost-and-usage failed: #{err.strip.empty? ? out.strip : err.strip}" unless status.success?
66
69
 
67
70
  rows = JSON.parse(out).fetch("ResultsByTime")
68
- raise "Cost Explorer returned no days for #{first}..#{last}" if rows.empty?
71
+ raise Error, "Cost Explorer returned no days for #{first}..#{last}" if rows.empty?
69
72
 
70
73
  rows.to_h { |row| [row.dig("TimePeriod", "Start"), by_service(row)] }
71
74
  end
@@ -26,7 +26,8 @@ module Hecks
26
26
 
27
27
  # What each status of `deploy-box.sh` means, for the reason a refusal gives.
28
28
  BOX_STATUS = { 40 => "the box stack has no instance", 41 => "the roll did not succeed on the box",
29
- 42 => "the box is not healthy after the roll" }.freeze
29
+ 42 => "the box is not healthy after the roll",
30
+ 43 => "the database has no usable restore anchor (backups unreadable or kept too few days)" }.freeze
30
31
 
31
32
  # What each status of `deploy-service.sh` means; it ends with the box roll's own statuses.
32
33
  SERVICE_STATUS = { 2 => "unknown service", 30 => "the existing tag is not in ECR",
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "open3"
4
+ require_relative "../../indifferent_key"
4
5
 
5
6
  module Hecks
6
7
  module Adapters
@@ -12,6 +13,9 @@ module Hecks
12
13
  # and the runtime records the refusal on the finding. An ask that needs the issue before one
13
14
  # was opened is refused the same way, so a finding never blocks on GitHub.
14
15
  class FindingGithub
16
+ # Raised when `gh` cannot file or find the finding's issue.
17
+ class Error < RuntimeError; end
18
+
15
19
  # The environment variable that names the repository when no setting does.
16
20
  REPOSITORY_VARIABLE = "HECKS_FINDINGS_REPO"
17
21
 
@@ -27,12 +31,12 @@ module Hecks
27
31
  # @param held [Hash] the finding's fields; `title` and `body` are value objects or strings
28
32
  # @return [Hash{Symbol => Hash}] `issue_number:` of the new issue, the shape
29
33
  # `Finding.RecordIssue` takes
30
- # @raise [RuntimeError] when no repository is named, `gh` exits non-zero, or prints no
34
+ # @raise [Error] when no repository is named, `gh` exits non-zero, or prints no
31
35
  # issue URL
32
36
  def open_issue(**held)
33
37
  out = gh("issue", "create", "--title", plain(held[:title]).to_s, "--body", plain(held[:body]).to_s)
34
38
  number = out.lines.last.to_s.strip[%r{/issues/(\d+)\z}, 1]
35
- raise "gh issue create printed no issue URL: #{out.strip.inspect}" unless number
39
+ raise Error, "gh issue create printed no issue URL: #{out.strip.inspect}" unless number
36
40
 
37
41
  { issue_number: { value: number.to_i } }
38
42
  end
@@ -82,7 +86,10 @@ module Hecks
82
86
 
83
87
  def gh(*arguments)
84
88
  out, err, status = Open3.capture3("gh", *arguments, "--repo", repository)
85
- raise "gh #{arguments.first(2).join(" ")} failed — #{err.strip.empty? ? out.strip : err.strip}" unless status.success?
89
+ unless status.success?
90
+ raise Error,
91
+ "gh #{arguments.first(2).join(" ")} failed — #{err.strip.empty? ? out.strip : err.strip}"
92
+ end
86
93
 
87
94
  out
88
95
  end
@@ -91,12 +98,12 @@ module Hecks
91
98
  name = @repository || ENV.fetch(REPOSITORY_VARIABLE, nil)
92
99
  return name unless name.to_s.strip.empty?
93
100
 
94
- raise "no repository named: set the `repository` setting or #{REPOSITORY_VARIABLE} to owner/name"
101
+ raise Error, "no repository named: set the `repository` setting or #{REPOSITORY_VARIABLE} to owner/name"
95
102
  end
96
103
 
97
104
  def issue(held)
98
105
  number = plain(held[:issue_number])
99
- raise "the finding has no GitHub issue yet" if number.to_s.empty?
106
+ raise Error, "the finding has no GitHub issue yet" if number.to_s.empty?
100
107
 
101
108
  number.to_s
102
109
  end
@@ -107,7 +114,7 @@ module Hecks
107
114
  def plain(field)
108
115
  return field unless field.is_a?(Hash)
109
116
 
110
- field.key?(:value) ? field[:value] : field["value"]
117
+ IndifferentKey.read(field, :value)
111
118
  end
112
119
  end
113
120
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../doors/json_door"
3
+ require_relative "../../adapters/driving/json"
4
4
 
5
5
  module Hecks
6
6
  module Adapters
@@ -87,7 +87,7 @@ module Hecks
87
87
  %i[admit grant people].to_h { |key| [key, provider.provided_verb(Bluebook::Capabilities::MEMBERSHIP, key)] }
88
88
  end
89
89
 
90
- def rows(verb) = @runtime.query(verb).map { |row| Doors::JsonDoor.materialize(row) }
90
+ def rows(verb) = @runtime.query(verb).map { |row| Adapters::Driving::Json.materialize(row) }
91
91
 
92
92
  # The role a verb's command declares, which the dispatch must assert to be let through.
93
93
  def gated_role(provider, verb)
@@ -12,6 +12,9 @@ module Hecks
12
12
  # ruleset says; it only carries a ruleset to and from GitHub. `{owner}/{repo}` is a `gh api`
13
13
  # template resolved from the git remote.
14
14
  class GithubRulesets
15
+ # Raised when `gh` is missing or refuses a ruleset call.
16
+ class Error < RuntimeError; end
17
+
15
18
  # The part of a ruleset a lane projects: everything else GitHub fills in (ids, links,
16
19
  # timestamps, defaults) is left out of a comparison.
17
20
  COMPARED = %w[name target enforcement].freeze
@@ -23,7 +26,7 @@ module Hecks
23
26
  end
24
27
 
25
28
  # @return [Array<Hash>] every ruleset of the repository, in full
26
- # @raise [RuntimeError] when `gh` is missing or refuses
29
+ # @raise [Error] when `gh` is missing or refuses
27
30
  def all
28
31
  listed = json(call("api", "repos/{owner}/{repo}/rulesets?per_page=100"))
29
32
  listed.map { |ruleset| json(call("api", "repos/{owner}/{repo}/rulesets/#{ruleset.fetch("id")}")) }
@@ -37,7 +40,7 @@ module Hecks
37
40
  #
38
41
  # @param projected [Hash] the ruleset as `Tools::Lanes` projects it
39
42
  # @return [Symbol] `:created` or `:updated`
40
- # @raise [RuntimeError] when GitHub refuses it
43
+ # @raise [Error] when GitHub refuses it
41
44
  def apply(projected)
42
45
  live = named(projected.fetch("name"))
43
46
  path = "repos/{owner}/{repo}/rulesets"
@@ -110,7 +113,7 @@ module Hecks
110
113
 
111
114
  def call(*args, stdin: nil)
112
115
  out, err, ok = @runner.call(args, stdin)
113
- raise "gh #{args.first(3).join(" ")} failed: #{err.strip.empty? ? out.strip : err.strip}" unless ok
116
+ raise Error, "gh #{args.first(3).join(" ")} failed: #{err.strip.empty? ? out.strip : err.strip}" unless ok
114
117
 
115
118
  out
116
119
  end
@@ -174,7 +174,7 @@ module Hecks
174
174
  raise Runtime::NotFound, "no domain here, and none named" unless path
175
175
  raise Runtime::NotFound, "no such domain #{path.inspect}" unless Dir.exist?(path)
176
176
 
177
- Hecks.boot(File.expand_path(path), install_doors: false)
177
+ Hecks.boot(File.expand_path(path), install_driving: false)
178
178
  end
179
179
 
180
180
  def chapter_of(domain, chapter)
@@ -23,7 +23,7 @@ module Hecks
23
23
  def initialize(path, aggregates:, kind:)
24
24
  raise Runtime::NotFound, "no such domain #{path.inspect}" unless Dir.exist?(path.to_s)
25
25
 
26
- @registry = Hecks.boot(File.expand_path(path), install_doors: false).registry
26
+ @registry = Hecks.boot(File.expand_path(path), install_driving: false).registry
27
27
  @wanted = aggregates
28
28
  @kind = kind
29
29
  end
@@ -12,17 +12,10 @@ module Hecks
12
12
  # It answers what the audit's three layers found, as the report `hecks audit_translation`
13
13
  # prints, and the edge itself, so approving it audits the very edge that was read.
14
14
  class EdgeAudit
15
- # What one audit found.
16
- #
17
- # @!attribute [r] edge
18
- # @return [Bluebook::Translation] the audited edge
19
- # @!attribute [r] leaving
20
- # @return [Integer] how many edges leave the latest held era
21
- # @!attribute [r] ok
22
- # @return [Boolean] whether no layer refused
23
- # @!attribute [r] report
24
- # @return [String] the report, one block per aggregate
25
- Finding = Struct.new(:edge, :leaving, :ok, :report, keyword_init: true)
15
+ # What one audit found: the audited `edge`, how many edges are `leaving` the latest held
16
+ # era, whether it is `ok`, the `report` text, every refusal (`Aggregate#id: wording`) and
17
+ # the records as translated (`after`), by aggregate name and id.
18
+ Finding = Struct.new(:edge, :leaving, :ok, :report, :refusals, :after, keyword_init: true)
26
19
 
27
20
  # Audits the edge that leaves the latest held era.
28
21
  #
@@ -89,14 +82,11 @@ module Hecks
89
82
  end
90
83
 
91
84
  def finding(chain, era, leaving, edge = chain.last[:translation])
92
- lines = []
93
- ok = true
94
- @held.bluebook.aggregates.each do |aggregate|
95
- verdict, records = verdict_for(aggregate, edge, era, chain)
96
- lines.concat(describe(aggregate, edge, verdict, records))
97
- ok &&= verdict.ok?
98
- end
99
- Finding.new(edge: edge, leaving: leaving, ok: ok, report: lines.join("\n"))
85
+ audits = @held.bluebook.aggregates.map { |aggregate| [aggregate, *verdict_for(aggregate, edge, era, chain)] }
86
+ lines = audits.flat_map { |aggregate, verdict, records, _| describe(aggregate, edge, verdict, records) }
87
+ refusals = audits.flat_map { |_, verdict, _, _| verdict.violations }
88
+ Finding.new(edge: edge, leaving: leaving, ok: refusals.empty?, report: lines.join("\n"),
89
+ refusals: refusals, after: audits.to_h { |aggregate, _, _, after| [aggregate.name, after] })
100
90
  end
101
91
 
102
92
  def verdict_for(aggregate, edge, era, chain)
@@ -108,7 +98,7 @@ module Hecks
108
98
  end
109
99
  verdict = Translation::Audit.check(aggregate: aggregate, declared: edge.for_aggregate(aggregate.name),
110
100
  before: before, after: after)
111
- [verdict, after.size]
101
+ [verdict, after.size, after]
112
102
  end
113
103
 
114
104
  def translated(aggregate, era, chain)
@@ -22,16 +22,18 @@ module Hecks
22
22
  #
23
23
  # @param path [String] the domain directory, or the project holding its `bluebook/`
24
24
  # @param mode [Symbol] one of `MODES`
25
+ # @param environment [String, nil] the overlay under `environments/` to load last;
26
+ # `HECKS_PROJECT_ENVIRONMENT` when not given
25
27
  # @return [HeldDomain] the loaded domain
26
28
  # @raise [Runtime::NotFound] if the directory does not exist, declares no bluebook, or
27
29
  # does not load
28
- def self.open(path, mode: :translated)
30
+ def self.open(path, mode: :translated, environment: ENV.fetch("HECKS_PROJECT_ENVIRONMENT", nil))
29
31
  raise ArgumentError, "unknown mode #{mode.inspect}" unless MODES.include?(mode)
30
32
 
31
33
  loading = Ports::Loading.bootstrap
32
34
  directory = loading.bluebook_directory(path)
33
35
  registry = Runtime::Registry.new(root: File.dirname(directory))
34
- load_into(registry, loading, directory, mode)
36
+ load_into(registry, loading, directory, mode, environment)
35
37
  new(registry, directory)
36
38
  rescue Errno::ENOENT
37
39
  raise Runtime::NotFound, "no such domain #{path.inspect}"
@@ -40,8 +42,7 @@ module Hecks
40
42
  end
41
43
 
42
44
  # @api private
43
- def self.load_into(registry, loading, directory, mode)
44
- environment = ENV.fetch("HECKS_PROJECT_ENVIRONMENT", nil)
45
+ def self.load_into(registry, loading, directory, mode, environment)
45
46
  Hecks.with_registry(registry) do
46
47
  loading.load_library
47
48
  loading.load_project(loading.shared_root(nil, directory))
@@ -91,7 +92,29 @@ module Hecks
91
92
  # @return [Hash] the settings
92
93
  def settings
93
94
  declared = registry.binding_settings(bluebook.name, Ports::Persistence::VERB, adapter_name)
94
- ENV["HECKS_SCHEMA"] ? declared.merge(schema: ENV["HECKS_SCHEMA"]) : declared
95
+ declared = declared.merge(schema: ENV["HECKS_SCHEMA"]) if ENV["HECKS_SCHEMA"]
96
+ declared = declared.merge(schema: @schema) if @schema
97
+ @database ? declared.merge(database: @database) : declared
98
+ end
99
+
100
+ # Names the schema its journal store is read in, over the world's and `HECKS_SCHEMA`, so a
101
+ # rehearsal reads the schema it reports.
102
+ #
103
+ # @param schema [String] a schema name
104
+ # @return [HeldDomain] self
105
+ def read_schema(schema)
106
+ @schema = schema
107
+ self
108
+ end
109
+
110
+ # Points the journal store at another database than its world names, so a rehearsal
111
+ # reads a copy and never the world's own. Every read and write after this reaches it.
112
+ #
113
+ # @param database [String] a `postgres://` URL
114
+ # @return [HeldDomain] self
115
+ def read_from(database)
116
+ @database = database
117
+ self
95
118
  end
96
119
 
97
120
  # The text the era is frozen from: the bluebook source as it stands on disk.
@@ -23,7 +23,7 @@ module Hecks
23
23
  # named ones, through `Hecks::CLI::ProjectCli`. A launcher is a generated file that only
24
24
  # points at its domain, so an existing one is replaced and nothing else is touched.
25
25
  #
26
- # @param held [Hash] the `Door` record: `domains` (comma separated paths under the current
26
+ # @param held [Hash] the `Launch` record: `domains` (comma separated paths under the current
27
27
  # directory; every domain found when absent)
28
28
  # @return [Hash{Symbol => Hash}] `output:` one line per launcher written
29
29
  # @raise [ConsoleCapture::Failure] when no launcher was written
@@ -45,7 +45,7 @@ module Hecks
45
45
  # Nothing is replaced: every file is checked before the first is written, and a directory
46
46
  # that already holds a bluebook is refused.
47
47
  #
48
- # @param held [Hash] the `Door` record: `name`, and optionally `adapter` and `dir`
48
+ # @param held [Hash] the `Launch` record: `name`, and optionally `adapter` and `dir`
49
49
  # @return [Hash{Symbol => Hash}] `output:` the report that was printed
50
50
  # @raise [ConsoleCapture::Failure] when the name or adapter is refused, or a file exists
51
51
  def scaffold(**held)
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hecks
4
+ module Adapters
5
+ module Rehearsal
6
+ # What the mint's audit read and what it concluded, as the facts the report holds: how much
7
+ # was examined by each engine, whether anything was, why not when nothing was, and how the
8
+ # two engines stood against each other.
9
+ class AuditReport
10
+ # The limit a clean verdict carries when no stored row was read.
11
+ UNAUDITED = "the stored rows"
12
+
13
+ # The wording that explains a Rust-only refusal: the Ruby audit's first layer does not hold
14
+ # a stored value to a closed set, a required field or an undeclared value-object key.
15
+ RUST_ONLY = "The Rust audit also holds a stored value to its closed set, a required field and an " \
16
+ "undeclared value-object key, which the Ruby audit's first layer does not yet: a row " \
17
+ "only Rust refuses is that known difference between the engines, not a second finding."
18
+
19
+ # @param minting [String] none, first or next
20
+ # @param edge [Bluebook::Translation, nil] the edge the audit ran through
21
+ # @param states [Hash{String => Hash}] the translated records the audit read, by aggregate
22
+ # @param ruby [Array<String>] the Ruby audit's refusals
23
+ # @param rust [RustAudit::Result] the second engine's answer
24
+ def initialize(minting:, edge:, states:, ruby:, rust:)
25
+ @minting = minting
26
+ @edge = edge
27
+ @states = states
28
+ @ruby = ruby
29
+ @rust = rust
30
+ end
31
+
32
+ # @return [Boolean] whether any stored row was read by the audit
33
+ def audited? = !@edge.nil? && @minting == "next"
34
+
35
+ # @return [Integer] the rows the Ruby audit read
36
+ def rows = @states.values.sum(&:size)
37
+
38
+ # @return [Hash] `audited`, `rows_examined`, `aggregates_examined`, `rust_rows_examined`
39
+ # (zero when the second engine did not run, which `parity` says) and the sentence the
40
+ # verdict rests on
41
+ def facts
42
+ { audited: flag(audited?), rows_examined: count(rows), reason: { value: reason },
43
+ aggregates_examined: count(aggregates),
44
+ rust_rows_examined: count(@rust.rows) }
45
+ end
46
+
47
+ # @return [Hash] the Rust engine's standing against the Ruby one, with who refused what
48
+ def parity
49
+ return { engine: "rust", status: "not_run", note: @rust.note } unless @rust.ran
50
+ return { engine: "rust", status: "agree", note: agreed } if ruby_records == rust_records
51
+
52
+ { engine: "rust", status: "differ", note: differed }
53
+ end
54
+
55
+ # @return [Array<Hash>] the limit a rehearsal that read nothing carries
56
+ def limits = audited? ? [] : [{ what: UNAUDITED, why: reason }]
57
+
58
+ private
59
+
60
+ def flag(value) = { value: value }
61
+
62
+ def count(value) = { value: value }
63
+
64
+ def reason
65
+ case @minting
66
+ when "none" then "no mint is needed at this shape, so no stored row was examined"
67
+ when "first" then "no era is held, so the first mint holds the shape as it stands and no stored row was examined"
68
+ else next_reason
69
+ end
70
+ end
71
+
72
+ def next_reason
73
+ return "no edge leads the held era to this shape, so no stored row was examined" unless @edge
74
+
75
+ "the audit read #{rows} stored row(s) of #{aggregates} aggregate(s) through the edge " \
76
+ "#{@edge.from} -> #{@edge.to}"
77
+ end
78
+
79
+ def aggregates = @states.count { |_, held| held.any? }
80
+
81
+ def ruby_records = records(@ruby)
82
+
83
+ def rust_records = records(@rust.violations)
84
+
85
+ def records(refusals) = refusals.map { |text| text[/\A[^:]+/] }.uniq.sort
86
+
87
+ def agreed = "ruby refused #{ruby_records.size} records, rust #{rust_records.size}; rust read #{@rust.rows} rows"
88
+
89
+ def differed
90
+ rust_only = rust_records - ruby_records
91
+ ruby_only = ruby_records - rust_records
92
+ "the engines refused different records: ruby #{ruby_records.size}, rust #{rust_records.size}. " \
93
+ "#{only("rust", rust_only)}#{only("ruby", ruby_only)}#{known_gap(rust_only)}" \
94
+ "Both are held against the report: the union is refused."
95
+ end
96
+
97
+ def known_gap(rust_only) = rust_only.empty? ? "" : "#{RUST_ONLY} "
98
+
99
+ def only(engine, held)
100
+ return "" if held.empty?
101
+
102
+ "Only #{engine} refused: #{held.first(5).join(", ")}#{", ..." if held.size > 5}. "
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hecks
4
+ module Adapters
5
+ module Rehearsal
6
+ # What a data copy holds for the domain, counted before any audit runs: the eras, the rows
7
+ # each aggregate's head holds, and the entries and live records of the journal.
8
+ #
9
+ # The mint's audit reads the journal (the latest entry of each record), never the head, so
10
+ # the two counts sit side by side and a divergence between them is visible, not refused.
11
+ Census = Struct.new(:schema, :source, :present, :eras, :journal_entries, :counts, :elsewhere,
12
+ keyword_init: true) do
13
+ # One aggregate's rows: what its head holds and what the journal's latest entries hold.
14
+ const_set(:Count, Struct.new(:aggregate, :head, :journal, keyword_init: true))
15
+
16
+ # @return [Array<String>] other schemas of the copy that hold a lineage
17
+ def elsewhere = self[:elsewhere] || []
18
+
19
+ # @return [Integer] the rows the heads hold, over every aggregate
20
+ def head_rows = counts.sum(&:head)
21
+
22
+ # @return [Integer] the live records the journal's latest entries hold
23
+ def journal_rows = counts.sum(&:journal)
24
+
25
+ # @return [Boolean] whether the copy holds no row of the domain at all
26
+ def empty? = head_rows.zero? && journal_rows.zero? && journal_entries.zero?
27
+
28
+ # @param domain [String] the domain's name, for the sentence
29
+ # @return [String] what the copy holds, in words
30
+ def summary(domain)
31
+ "the copy holds #{eras} era(s), #{head_rows} head rows and #{journal_rows} journal records for " \
32
+ "#{domain} in schema #{schema}"
33
+ end
34
+
35
+ # @param domain [String] the domain's name, for the sentence
36
+ # @return [String] why a copy that holds no row cannot be rehearsed, and what to set
37
+ def refusal(domain)
38
+ held = eras.zero? ? "no era and no rows" : "#{eras} era(s) but no rows"
39
+ where = if source == "memory"
40
+ "in the `rows` document"
41
+ else
42
+ "in schema #{schema}#{" (not in the copy)" unless present}"
43
+ end
44
+ "the copy holds #{held} for domain #{domain} #{where}: #{remedy}#{hint}"
45
+ end
46
+
47
+ # @return [Hash] the report facts, as the `RehearsalRun` commands take them
48
+ def facts
49
+ { schema_used: { name: schema, source: source, present: present }, eras_held: { value: eras },
50
+ journal_entries: { value: journal_entries }, head_rows: { value: head_rows }, census: counts.map(&:to_h) }
51
+ end
52
+
53
+ private
54
+
55
+ def where
56
+ return "in the `rows` document" if source == "memory"
57
+
58
+ "in schema #{schema}#{" (not in the copy)" unless present}"
59
+ end
60
+
61
+ def remedy
62
+ return "name the document that holds the stored records in `rows`" if source == "memory"
63
+
64
+ "set the schema in the world file (`rehearsed_by` { schema \"...\" })"
65
+ end
66
+
67
+ def hint = elsewhere.empty? ? "" : "; schemas in this copy that hold a lineage: #{elsewhere.join(", ")}"
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Hecks
6
+ module Adapters
7
+ # Everything a rehearsal does on a client's domain: where its data copy comes from, how the
8
+ # mint's audit is run over it, and what is reported. The two port adapters (`RehearsalStore`
9
+ # and `RehearsalSubject`) are thin; the work lives here.
10
+ module Rehearsal
11
+ # A rehearsal that was not admitted or could not be carried on: the port answers it as a
12
+ # refusal, which the run keeps as `refusal`.
13
+ class Refused < StandardError; end
14
+
15
+ # What a subject's own `.world` says about the copy of its data, read from its
16
+ # `rehearsed_by` block and checked before anything is touched.
17
+ #
18
+ # A connection is only ever named by an environment variable (`connection_env`); a literal
19
+ # connection string, user or password in the world is refused, so the file holds no secret.
20
+ class Declaration
21
+ KINDS = %w[memory postgres].freeze
22
+ MODES = %w[empty dump readonly_copy].freeze
23
+ LITERALS = %i[database url connection password user].freeze
24
+ LOCAL_HOSTS = [nil, "", "localhost", "127.0.0.1", "::1", "[::1]"].freeze
25
+
26
+ NAME = /\A[A-Za-z0-9_-]+\z/
27
+
28
+ attr_reader :kind, :mode, :connection_env, :dump, :rows, :drop_after, :allow_remote, :schema, :environment
29
+
30
+ # @param world [#for_verb, nil] the subject's world, as the registry holds it
31
+ # @param directory [String] the domain's bluebook directory, which a relative file is under
32
+ # @return [Declaration] the checked declaration
33
+ # @raise [Refused] if the world declares no `rehearsed_by`, or declares it wrongly
34
+ def self.from_world(world, directory)
35
+ settings = world&.for_verb("rehearsed_by") || {}
36
+ raise Refused, "the domain's world declares no `rehearsed_by`: nothing says where its data copy is" if settings.empty?
37
+
38
+ new(settings, directory).tap(&:check!)
39
+ end
40
+
41
+ # @param settings [Hash] the block's settings, `adapter:` among them
42
+ # @param directory [String] where a relative `dump` or `rows` is found
43
+ def initialize(settings, directory)
44
+ @kind = settings[:adapter].to_s.downcase
45
+ @connection_env = settings[:connection_env]&.to_s
46
+ @dump = expand(settings[:dump], directory)
47
+ @rows = expand(settings[:rows], directory)
48
+ @mode = settings.fetch(:mode) { default_mode }.to_s
49
+ read_switches(settings)
50
+ end
51
+
52
+ # @return [void]
53
+ # @raise [Refused] naming what is wrong with the declaration
54
+ def check!
55
+ check_names!
56
+ raise Refused, literal_wording unless @literals.empty?
57
+
58
+ kind == "postgres" ? check_postgres! : check_memory!
59
+ end
60
+
61
+ # @return [String] the connection the environment holds
62
+ # @raise [Refused] if the variable the world names is unset or empty, or names a server
63
+ # that is not this machine and the world did not say `allow_remote true`
64
+ def connection
65
+ value = ENV.fetch(connection_env, "")
66
+ raise Refused, "the environment variable #{connection_env} is not set" if value.strip.empty?
67
+
68
+ refuse_remote!(value)
69
+ value
70
+ end
71
+
72
+ private
73
+
74
+ def refuse_remote!(value)
75
+ host = URI.parse(value).host
76
+ return if allow_remote || LOCAL_HOSTS.include?(host)
77
+
78
+ raise Refused, "#{connection_env} names the server #{host.inspect}, which is not this machine: " \
79
+ "a rehearsal reads a local copy; say `allow_remote true` in rehearsed_by to point it elsewhere"
80
+ rescue URI::InvalidURIError
81
+ nil
82
+ end
83
+
84
+ def read_switches(settings)
85
+ @drop_after = settings.fetch(:drop_after, true) != false
86
+ @allow_remote = settings.fetch(:allow_remote, false) == true
87
+ @literals = LITERALS & settings.keys.map(&:to_sym)
88
+ @schema = settings[:schema]&.to_s
89
+ @environment = settings[:environment]&.to_s
90
+ end
91
+
92
+ def default_mode = dump || rows ? "dump" : "empty"
93
+
94
+ def check_names!
95
+ raise Refused, "rehearsed_by names #{kind.inspect}; the stores are #{KINDS.join(" and ")}" unless KINDS.include?(kind)
96
+
97
+ raise Refused, "rehearsed_by mode #{mode.inspect} is not one of #{MODES.join(", ")}" unless MODES.include?(mode)
98
+
99
+ check_label!("schema", schema)
100
+ check_label!("environment", environment)
101
+ end
102
+
103
+ def check_label!(word, value)
104
+ return if value.nil? || NAME.match?(value)
105
+
106
+ raise Refused, "rehearsed_by #{word} #{value.inspect} is not a name (letters, digits, `_`, `-`)"
107
+ end
108
+
109
+ def expand(path, directory)
110
+ path && File.expand_path(path.to_s, directory)
111
+ end
112
+
113
+ def literal_wording
114
+ "rehearsed_by holds #{@literals.join(", ")}: name the environment variable in `connection_env`, " \
115
+ "never the value, so the world file holds no secret"
116
+ end
117
+
118
+ def check_postgres!
119
+ unless connection_env
120
+ raise Refused,
121
+ "a postgres rehearsal needs `connection_env`, the name of the variable holding the server"
122
+ end
123
+ raise Refused, "mode dump needs `dump`, the file to restore" if mode == "dump" && !dump
124
+ end
125
+
126
+ def check_memory!
127
+ raise Refused, "mode readonly_copy is a postgres mode: a memory store has no copy to read" if mode == "readonly_copy"
128
+ raise Refused, "mode dump needs `rows`, the JSON file the memory store holds" if mode == "dump" && !rows
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end