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
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "open3"
5
+ require "hecks/vocabulary"
6
+ require_relative "../tools"
7
+
8
+ module Hecks
9
+ module Tools
10
+ # Decides whether a commit is only a release bump on a base that is fully certified, so CI may
11
+ # inherit the base's certification instead of running the suite again.
12
+ #
13
+ # The files a bump edits, and which of their lines it may change, are the `ReleaseFile` rows of
14
+ # the Vocabulary chapter. A change is a release bump when it touches at least one file, every
15
+ # file it touches is a row, and every line it adds or removes in a file matches that row's
16
+ # `lines` pattern (an empty pattern admits any line). Whether the base is certified is read
17
+ # from the base commit's own check runs: every `RequiredCheck` row that is not marked
18
+ # `runs: always` must have its latest run on the base succeed or be skipped.
19
+ #
20
+ # Every case that cannot be confirmed answers "not a release bump", so the suite runs.
21
+ module ReleaseOnly
22
+ GREEN = %w[success skipped].freeze
23
+
24
+ module_function
25
+
26
+ # @param changes [Hash{String => Array<String>}] each changed path to the lines it adds or
27
+ # removes
28
+ # @param files [Array<Hash{String => String}>] the `ReleaseFile` rows
29
+ # @return [Boolean] whether the change touches only release files, and only their release
30
+ # lines
31
+ def release_only?(changes, files: Hecks::Vocabulary.rows("ReleaseFile"))
32
+ return false if changes.empty?
33
+
34
+ by_path = files.to_h { |row| [row.fetch("path"), row.fetch("lines", "")] }
35
+ changes.all? { |path, lines| by_path.key?(path) && lines_allowed?(lines, by_path.fetch(path)) }
36
+ end
37
+
38
+ # @param lines [Array<String>] the changed lines of one file
39
+ # @param pattern [String] the row's `lines` regex, empty for any line
40
+ # @return [Boolean] whether every changed line matches
41
+ def lines_allowed?(lines, pattern)
42
+ return true if pattern.to_s.empty?
43
+
44
+ regex = Regexp.new(pattern)
45
+ lines.all? { |line| line.match?(regex) }
46
+ end
47
+
48
+ # @param runs [Array<Hash{String => String}>] the base commit's check runs: `name`,
49
+ # `conclusion` and `started_at`
50
+ # @param required [Array<Hash{String => String}>] the `RequiredCheck` rows
51
+ # @return [Boolean] whether every check that must be inherited has a green latest run
52
+ def certified?(runs, required: Hecks::Vocabulary.rows("RequiredCheck"))
53
+ latest = latest_runs(runs)
54
+ inherited = required.reject { |row| row["runs"] == "always" }.map { |row| row.fetch("name") }
55
+ inherited.all? { |name| GREEN.include?(latest[name].to_h["conclusion"]) }
56
+ end
57
+
58
+ # @return [Hash{String => Hash}] each check's most recently started run
59
+ def latest_runs(runs)
60
+ runs.group_by { |run| run["name"] }.transform_values { |group| group.max_by { |run| run["started_at"].to_s } }
61
+ end
62
+
63
+ # @param base [String] the commit the change is measured against
64
+ # @param head [String] the commit under test
65
+ # @param root [String] the checkout
66
+ # @return [Hash{String => Array<String>}, nil] each changed path to its changed lines, or nil
67
+ # when git could not say
68
+ def changes(base, head, root:)
69
+ out, _err, status = Open3.capture3("git", "-C", root, "diff", "-U0", "--no-color", base, head)
70
+ status.success? ? parse(out) : nil
71
+ end
72
+
73
+ # @param diff [String] `git diff -U0` output
74
+ # @return [Hash{String => Array<String>}] each changed path to the lines it adds or removes
75
+ def parse(diff)
76
+ found = Hash.new { |hash, key| hash[key] = [] }
77
+ path = nil
78
+ diff.each_line do |line|
79
+ path = file_of(line.chomp) || path
80
+ found[path] << line[1..].chomp if path && content_line?(line)
81
+ end
82
+ found.to_h
83
+ end
84
+
85
+ # @return [String, nil] the path a `+++` or `---` header names, nil for any other line
86
+ def file_of(line)
87
+ return line.delete_prefix("+++ b/") if line.start_with?("+++ b/")
88
+
89
+ line.delete_prefix("--- a/") if line.start_with?("--- a/")
90
+ end
91
+
92
+ # @return [Boolean] whether the line is an added or removed line, not a header
93
+ def content_line?(line)
94
+ line.match?(/\A[+-]/) && !line.start_with?("+++ ", "--- ")
95
+ end
96
+
97
+ # @param repository [String] `owner/name`
98
+ # @param sha [String] the commit whose check runs are read
99
+ # @param env [Hash{String => String}] carries `GH_TOKEN` for the API call
100
+ # @return [Array<Hash{String => String}>, nil] the check runs, or nil when the API failed
101
+ def check_runs(repository, sha, env: ENV)
102
+ path = "repos/#{repository}/commits/#{sha}/check-runs?per_page=100"
103
+ jq = ".check_runs[] | {name, conclusion, started_at}"
104
+ out, _err, status = Open3.capture3(env.to_h.slice("GH_TOKEN"), "gh", "api", path, "--paginate", "--jq", jq)
105
+ status.success? ? out.lines.map { |line| JSON.parse(line) } : nil
106
+ rescue JSON::ParserError
107
+ nil
108
+ end
109
+ end
110
+ end
111
+ end
@@ -18,18 +18,25 @@ module Hecks
18
18
  setting = Projections::Site::CmsEditor::Setting.read(site.chapter, table: site.table)
19
19
  abort "project_site: --editor names #{editor}, but the project declares no Editor row" if setting.nil?
20
20
 
21
- options = { domain_chapter: domain_chapter(project, setting), table: site.table }
22
- files = Projector.call(:site_cms_editor, bluebook: site.chapter, options: options)
21
+ files = Projector.call(:site_cms_editor, bluebook: site.chapter, options: options(project, setting, site))
23
22
  files.to_h { |name, text| [File.join(File.expand_path(editor), name), text] }
24
23
  rescue ArgumentError, Projections::Site::Table::Invalid => e
25
24
  abort "project_site: #{e.message}"
26
25
  end
27
26
 
27
+ # @return [Hash{Symbol => Object}] the chapters the editor reads, and the route table
28
+ def options(project, setting, site)
29
+ chosen = { domain_chapters: setting.chapters.map { |name| domain_chapter(project, setting, name) }, table: site.table }
30
+ chosen[:media_chapter] = domain_chapter(project, setting, setting.media_chapter) if setting.media_chapter
31
+ chosen
32
+ end
33
+
34
+ # @param name [String] the chapter to find; the row's first `chapter` unless given
28
35
  # @return [Bluebook::Chapter] the chapter the `Editor` row names, from its domain directory
29
- def domain_chapter(project, setting)
36
+ def domain_chapter(project, setting, name = setting.chapter)
30
37
  domain_root = File.join(project, setting.domain)
31
- chapter = registry_for(domain_root).bluebooks.values.find { |candidate| candidate.name == setting.chapter }
32
- abort "project_site: #{domain_root} declares no chapter #{setting.chapter}" unless chapter
38
+ chapter = registry_for(domain_root).bluebooks.values.find { |candidate| candidate.name == name }
39
+ abort "project_site: #{domain_root} declares no chapter #{name}" unless chapter
33
40
  chapter
34
41
  end
35
42
  end
@@ -80,7 +80,7 @@ module Hecks
80
80
  # @raise [SystemExit] when the declared shape is refused, or provisioning is refused
81
81
  def provision(slug, options, domain_directory)
82
82
  lib_hecks = File.expand_path("..", __dir__)
83
- dispatcher = Hecks.boot_files(CHAPTER_FILES.map { |file| File.join(lib_hecks, file) }, install_doors: false)
83
+ dispatcher = Hecks.boot_files(CHAPTER_FILES.map { |file| File.join(lib_hecks, file) }, install_driving: false)
84
84
  provision_tenant(dispatcher, slug, tenant_facts(slug, options, domain_directory))
85
85
  refuse_if_refused(dispatcher, slug)
86
86
 
@@ -126,7 +126,7 @@ module Hecks
126
126
  # @return [void]
127
127
  # @raise [SystemExit] when the tenant was never registered in Tenancy
128
128
  def confirm(dispatcher, slug, options, domain_directory)
129
- target = Hecks.boot(domain_directory, environment: slug, install_doors: false)
129
+ target = Hecks.boot(domain_directory, environment: slug, install_driving: false)
130
130
  puts "booted #{options[:domain]} for tenant #{slug.inspect} under realm #{options[:realm].inspect}"
131
131
 
132
132
  refuse_unless_tenant_capable(target.registry)
@@ -177,7 +177,7 @@ module Hecks
177
177
  class Context
178
178
  # @param root [String] the checkout
179
179
  def initialize(root)
180
- @runtime = Hecks.boot(File.join(root, "lib/hecks/hecks"), install_doors: false)
180
+ @runtime = Hecks.boot(File.join(root, "lib/hecks/hecks"), install_driving: false)
181
181
  @projections = {}
182
182
  end
183
183
 
@@ -201,8 +201,8 @@ module Hecks
201
201
 
202
202
  def projection(chapter)
203
203
  @projections[chapter] ||= begin
204
- launcher = Doors::LauncherOptions.settings(@runtime, chapter)
205
- options = Doors::LauncherOptions.projection(launcher, "hecks")
204
+ launcher = Adapters::Driving::LauncherOptions.settings(@runtime, chapter)
205
+ options = Adapters::Driving::LauncherOptions.projection(launcher, "hecks")
206
206
  Projector.call(:cli, bluebook: @runtime.registry.bluebook(chapter), options: options)
207
207
  end
208
208
  end
@@ -210,7 +210,7 @@ module Hecks
210
210
  # `hecks ir`, `hecks console`, `hecks quality_control sweep.run`, `hecks host.check_era`.
211
211
  # The launcher takes a name that is only a query as a query, so no `query` word is needed.
212
212
  def words(chapter, name, row)
213
- route = route_for(Doors::LauncherOptions.settings(@runtime, chapter) || {}, chapter, name, row)
213
+ route = route_for(Adapters::Driving::LauncherOptions.settings(@runtime, chapter) || {}, chapter, name, row)
214
214
  return [route] if route
215
215
 
216
216
  [*(Naming.snake(chapter) unless chapter == "Hecks"), name]
data/lib/hecks/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module Hecks
2
2
  # Read directly by the gemspec, so this file must never require the
3
3
  # rest of the gem (its dependencies aren't resolved yet at that point).
4
- VERSION = "3.10.0".freeze
4
+ VERSION = "3.12.0".freeze
5
5
  end
@@ -51,6 +51,7 @@ module Hecks
51
51
  ].freeze,
52
52
  "CiGate" => [
53
53
  {"name"=>"runtime_changed", "workflow"=>"ci.yml", "mode"=>"touches", "pattern"=>"^lib/hecks/runtime/", "push"=>"before_sha", "label"=>"does this change touch lib/hecks/runtime/**?"}.freeze,
54
+ {"name"=>"release_only", "workflow"=>"ci.yml", "mode"=>"release_only", "pattern"=>"", "push"=>"before_sha", "label"=>"is this change only a release bump on a certified base?"}.freeze,
54
55
  {"name"=>"postgres_io_relevant_changed", "workflow"=>"ci-postgres-io-parallel.yml", "mode"=>"skips_unless", "pattern"=>"^(docs/|editors/|release/|deploy/|\\.claude/|\\.githooks/|rust/(parser|codegen|host|build|lsp|web|tests)/|\\.rubocop\\.yml$|\\.rubocop_todo\\.yml$|\\.mcp\\.json$|\\.rspec-local\\.example$|README\\.md$|CHANGELOG\\.md$|CONTRIBUTING\\.md$|SECURITY\\.md$|LICENSE$|\\.gitignore$)", "push"=>"skip", "label"=>"does this change touch anything rspec_postgres_io_parallel covers?"}.freeze
55
56
  ].freeze,
56
57
  "Comparison" => [
@@ -107,8 +108,8 @@ module Hecks
107
108
  {"type"=>"String", "strategy"=>"substring"}.freeze
108
109
  ].freeze,
109
110
  "Lane" => [
110
- {"name"=>"main", "guarded"=>"no", "pushers"=>"anyone", "feeds"=>"", "follows"=>"", "alert_after"=>""}.freeze,
111
- {"name"=>"stable", "guarded"=>"yes", "pushers"=>"green", "feeds"=>"edge", "follows"=>"main", "alert_after"=>"4"}.freeze
111
+ {"name"=>"main", "guarded"=>"no", "pushers"=>"anyone", "feeds"=>"", "follows"=>"", "alert_after"=>"", "release_trailer"=>""}.freeze,
112
+ {"name"=>"stable", "guarded"=>"yes", "pushers"=>"green", "feeds"=>"edge", "follows"=>"main", "alert_after"=>"4", "release_trailer"=>"Release-Approved-By"}.freeze
112
113
  ].freeze,
113
114
  "LoadOrder" => [
114
115
  {"glob"=>"*.port"}.freeze,
@@ -134,6 +135,9 @@ module Hecks
134
135
  {"name"=>"replace"}.freeze,
135
136
  {"name"=>"scale_call"}.freeze
136
137
  ].freeze,
138
+ "PrebuiltBinary" => [
139
+ {"name"=>"host", "crate"=>"rust/host", "executable"=>"bootstrap", "target"=>"x86_64-unknown-linux-gnu", "runner"=>"ubuntu-24.04"}.freeze
140
+ ].freeze,
137
141
  "Primitive" => [
138
142
  {"name"=>"String"}.freeze,
139
143
  {"name"=>"Integer"}.freeze,
@@ -363,6 +367,15 @@ module Hecks
363
367
  {"refusal"=>"AttributeAbsent", "site"=>"absent_read", "template"=>"{aggregate} {field} is absent on this record — declared, not optional, and added since it was written. Backfill it in a translation (backfill :{field}, default: ...), or declare it optional: true"}.freeze,
364
368
  {"refusal"=>"ProjectionAbsent", "site"=>"absent_read", "template"=>"{aggregate} {field} is not yet projected on this record — declared via projects :{field}, but no rebuild sweep has populated it. Run the sweep, or read {reference}.{remote_field} directly if this rule cannot wait"}.freeze
365
369
  ].freeze,
370
+ "ReleaseFile" => [
371
+ {"path"=>"lib/hecks/version.rb", "lines"=>"^\\s*VERSION = \"[0-9]+\\.[0-9]+\\.[0-9]+\"\\.freeze$"}.freeze,
372
+ {"path"=>"CHANGELOG.md", "lines"=>""}.freeze,
373
+ {"path"=>"README.md", "lines"=>"^(\\*\\*Status:\\*\\* )?Current release: `[0-9]+\\.[0-9]+\\.[0-9]+`"}.freeze,
374
+ {"path"=>"Gemfile.lock", "lines"=>"^ hecks \\([0-9]+\\.[0-9]+\\.[0-9]+\\)$"}.freeze,
375
+ {"path"=>"rust/host/HECKS_RELEASE", "lines"=>"^[0-9]+\\.[0-9]+\\.[0-9]+$"}.freeze,
376
+ {"path"=>"packages/hecks-client/package.json", "lines"=>"^\\s*\"version\": \"[0-9]+\\.[0-9]+\\.[0-9]+\",?$"}.freeze,
377
+ {"path"=>"packages/hecks-client/package-lock.json", "lines"=>"^\\s*\"version\": \"[0-9]+\\.[0-9]+\\.[0-9]+\",?$"}.freeze
378
+ ].freeze,
366
379
  "RequiredCheck" => [
367
380
  {"name"=>"rspec"}.freeze,
368
381
  {"name"=>"checks"}.freeze,
@@ -372,7 +385,8 @@ module Hecks
372
385
  {"name"=>"rspec_rust_parser"}.freeze,
373
386
  {"name"=>"rspec_rust_codegen"}.freeze,
374
387
  {"name"=>"rspec_rust_host"}.freeze,
375
- {"name"=>"rspec_fuzzing"}.freeze
388
+ {"name"=>"rspec_fuzzing"}.freeze,
389
+ {"name"=>"release_consistency", "runs"=>"always"}.freeze
376
390
  ].freeze,
377
391
  "RetiredScript" => [
378
392
  {"script"=>"ir", "section"=>"Custodian", "aggregate"=>"Introspection", "verb"=>"ir"}.freeze,
@@ -402,8 +416,8 @@ module Hecks
402
416
  {"script"=>"compact", "section"=>"Custodian", "aggregate"=>"Era", "verb"=>"compact"}.freeze,
403
417
  {"script"=>"heki_compact", "section"=>"Custodian", "aggregate"=>"Era", "verb"=>"compact_heki"}.freeze,
404
418
  {"script"=>"vendor_bluebook", "section"=>"Custodian", "aggregate"=>"Package", "verb"=>"vendor", "note"=>"waits: a refusal exits 1 with its reason on stderr"}.freeze,
405
- {"script"=>"project_cli", "section"=>"Custodian", "aggregate"=>"Door", "verb"=>"project_cli"}.freeze,
406
- {"script"=>"hecks_mcp_door", "section"=>"Custodian", "aggregate"=>"Door", "verb"=>"serve_mcp"}.freeze,
419
+ {"script"=>"project_cli", "section"=>"Custodian", "aggregate"=>"Launch", "verb"=>"project_cli"}.freeze,
420
+ {"script"=>"hecks_mcp_door", "section"=>"Custodian", "aggregate"=>"Launch", "verb"=>"serve_mcp"}.freeze,
407
421
  {"script"=>"project_rust", "section"=>"Custodian", "aggregate"=>"Build", "verb"=>"project_rust"}.freeze,
408
422
  {"script"=>"project_wasm", "section"=>"Custodian", "aggregate"=>"Build", "verb"=>"build_wasm"}.freeze,
409
423
  {"script"=>"project_host", "section"=>"Custodian", "aggregate"=>"Build", "verb"=>"build_host"}.freeze,
data/lib/hecks.rb CHANGED
@@ -12,7 +12,7 @@ require_relative "hecks/construct"
12
12
  # extends this to declare what it emits.
13
13
  require_relative "hecks/ir"
14
14
  require_relative "hecks/literal"
15
- require_relative "hecks/doors"
15
+ require_relative "hecks/adapters/driving"
16
16
  require_relative "hecks/query_specification"
17
17
 
18
18
  require_relative "hecks/ports"
@@ -27,13 +27,13 @@ require_relative "hecks/projector"
27
27
  # installation of them.
28
28
  require_relative "hecks/projections"
29
29
  # After `Projector`, which dispatches against the `:cli` projection.
30
- require_relative "hecks/doors/cli_door"
31
- require_relative "hecks/doors/usage_cache"
32
- require_relative "hecks/doors/cli_runner"
30
+ require_relative "hecks/adapters/driving/cli"
31
+ require_relative "hecks/adapters/driving/usage_cache"
32
+ require_relative "hecks/adapters/driving/cli_runner"
33
33
  require_relative "hecks/storehouse"
34
34
  require_relative "hecks/mcp_stdio_guard"
35
- require_relative "hecks/doors/mcp_door_scope"
36
- require_relative "hecks/doors/mcp_guide_cache"
35
+ require_relative "hecks/adapters/driving/mcp_scope"
36
+ require_relative "hecks/adapters/driving/mcp_guide_cache"
37
37
  require_relative "hecks/framework"
38
38
  require_relative "hecks/vendoring"
39
39
  require_relative "hecks/embryonaut_bluebook"
@@ -56,12 +56,12 @@ module Hecks
56
56
  # the `HECKS_ENVIRONMENT` variable; pass a name to choose one, or `nil` for none.
57
57
  # @param path [String] path to a domain directory, or a file inside one
58
58
  # @param shared [String, nil] a shared-root override; see `Runtime::Loader.boot`
59
- # @param install_doors [Boolean] install the `Widget::Item.Add`-style facade
59
+ # @param install_driving [Boolean] install the `Widget::Item.Add`-style facade
60
60
  # @param environment [String, nil] env name; its `.hecksagon`/`.world` overlay,
61
61
  # if present, loads after the domain's own. Defaults to `HECKS_ENVIRONMENT`; nil loads none
62
62
  # @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] dispatcher bound to the domain
63
- def boot(path, shared: nil, install_doors: true, environment: Runtime::Loader::FROM_ENV)
64
- Runtime.boot(path, shared: shared, install_doors: install_doors, environment: environment)
63
+ def boot(path, shared: nil, install_driving: true, environment: Runtime::Loader::FROM_ENV)
64
+ Runtime.boot(path, shared: shared, install_driving: install_driving, environment: environment)
65
65
  end
66
66
 
67
67
  # Loads a domain's declarations without binding any adapter or opening any store.
@@ -78,10 +78,10 @@ module Hecks
78
78
  # once. See `Runtime::Loader.boot_described`.
79
79
  #
80
80
  # @param described [Runtime::Loader::Described] what `describe` answered
81
- # @param install_doors [Boolean] install the `Widget::Item.Add`-style facade
81
+ # @param install_driving [Boolean] install the `Widget::Item.Add`-style facade
82
82
  # @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] dispatcher bound to the domain
83
- def boot_described(described, install_doors: true)
84
- Runtime.boot_described(described, install_doors: install_doors)
83
+ def boot_described(described, install_driving: true)
84
+ Runtime.boot_described(described, install_driving: install_driving)
85
85
  end
86
86
 
87
87
  # Boots a domain from an explicit list of files (a `.bluebook`, its
@@ -89,11 +89,11 @@ module Hecks
89
89
  # see `Runtime::Loader.boot_files` for why this exists beside `boot`.
90
90
  # @param paths [String, Array<String>] one or more file paths within the domain
91
91
  # @param shared [String, nil] a shared-root override; see `Runtime::Loader.boot_files`
92
- # @param install_doors [Boolean] install the `Widget::Item.Add`-style facade
92
+ # @param install_driving [Boolean] install the `Widget::Item.Add`-style facade
93
93
  # @param environment [String, nil] environment name passed to the selected-file loader
94
94
  # @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] dispatcher bound to the domain
95
- def boot_files(paths, shared: nil, install_doors: true, environment: Runtime::Loader::FROM_ENV)
96
- Runtime.boot_files(paths, shared: shared, install_doors: install_doors, environment: environment)
95
+ def boot_files(paths, shared: nil, install_driving: true, environment: Runtime::Loader::FROM_ENV)
96
+ Runtime.boot_files(paths, shared: shared, install_driving: install_driving, environment: environment)
97
97
  end
98
98
 
99
99
  # Binds the ambient registry for the duration of the block.
@@ -0,0 +1,69 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Hecks
4
+ # Flags a secret leaving the process in output: the whole environment printed, or a
5
+ # `*_KEY` / `*_SECRET` / `*_TOKEN` / `*_PASSWORD` variable interpolated into a string.
6
+ #
7
+ # An interpolated secret ends up in a log line, a failure message or a command line. Reading
8
+ # one into a local (or an HTTP header) is fine; building a string around it is not. Restoring
9
+ # the environment in a spec (`saved = ENV.to_h`) is not output, so it is left alone.
10
+ #
11
+ # @example
12
+ # puts ENV.to_h # bad
13
+ # warn "using #{ENV.fetch("API_TOKEN")}" # bad
14
+ # headers["Authorization"] = ENV.fetch("API_TOKEN") # good
15
+ class EnvDump < Base
16
+ MSG_DUMP = "This prints the whole environment, which carries every secret the process holds. " \
17
+ "Print only the variable names you need.".freeze
18
+ MSG_SECRET = "`%<read>s` is a secret read into a string, so it will land in a log line or a message. " \
19
+ "Pass it straight to the call that needs it instead of interpolating it.".freeze
20
+
21
+ OUTPUT_CALLS = %i[puts p pp print warn].freeze
22
+ SECRET_NAME = /(KEY|SECRET|TOKEN|PASSWORD|PASSWD|CREDENTIAL)/i
23
+
24
+ # @!method whole_env?(node)
25
+ def_node_matcher :whole_env?, <<~PATTERN
26
+ {(const {nil? cbase} :ENV) (send (const {nil? cbase} :ENV) {:to_h :to_a :to_s :inspect})}
27
+ PATTERN
28
+
29
+ # @!method env_read(node)
30
+ def_node_matcher :env_read, <<~PATTERN
31
+ (send (const {nil? cbase} :ENV) {:[] :fetch} (str $_) ...)
32
+ PATTERN
33
+
34
+ # Flags the whole environment handed to an output call.
35
+ #
36
+ # @param node [RuboCop::AST::SendNode] the call being visited
37
+ # @return [void]
38
+ def on_send(node)
39
+ return unless node.receiver.nil? && OUTPUT_CALLS.include?(node.method_name)
40
+ return unless node.arguments.any? { |argument| whole_env?(argument) }
41
+
42
+ add_offense(node, message: MSG_DUMP)
43
+ end
44
+
45
+ # Flags a secret variable interpolated into a string.
46
+ #
47
+ # @param node [RuboCop::AST::BeginNode] the `#{}` part being visited
48
+ # @return [void]
49
+ def on_begin(node)
50
+ return unless interpolation?(node)
51
+
52
+ read = node.children.filter_map { |child| secret_read(child) }.first
53
+ add_offense(node, message: format(MSG_SECRET, read: read)) if read
54
+ end
55
+
56
+ private
57
+
58
+ def interpolation?(node)
59
+ %i[dstr dsym xstr].include?(node.parent&.type)
60
+ end
61
+
62
+ def secret_read(node)
63
+ name = env_read(node)
64
+ node.source if name&.match?(SECRET_NAME)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,53 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Hecks
4
+ # Flags a duration measured as `Time.now - start`, where `start` was read from the clock.
5
+ #
6
+ # The wall clock steps when it is corrected, so a subtraction can go negative or jump.
7
+ # Elapsed time comes from the monotonic clock. `Time.now - seconds` (a past timestamp)
8
+ # and `Time.now - File.mtime(path)` (an age against a stored time) are left alone.
9
+ #
10
+ # @example
11
+ # started = Time.now
12
+ # elapsed = Time.now - started # bad
13
+ #
14
+ # started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
15
+ # elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started # good
16
+ class MonotonicDuration < Base
17
+ MSG = "`Time.now - %<start>s` measures elapsed time on the wall clock, which can step. " \
18
+ "Take both ends from `Process.clock_gettime(Process::CLOCK_MONOTONIC)`.".freeze
19
+
20
+ # @!method wall_clock_minus_variable(node)
21
+ def_node_matcher :wall_clock_minus_variable, <<~PATTERN
22
+ (send #clock_read? :- ${lvar ivar})
23
+ PATTERN
24
+
25
+ # @!method clock_read?(node)
26
+ def_node_matcher :clock_read?, <<~PATTERN
27
+ {(send (const {nil? cbase} :Time) :now) (send (send (const {nil? cbase} :Time) :now) :to_f)}
28
+ PATTERN
29
+
30
+ # Flags `Time.now - <variable>` when the variable holds an earlier clock read.
31
+ #
32
+ # @param node [RuboCop::AST::SendNode] the subtraction being visited
33
+ # @return [void]
34
+ def on_send(node)
35
+ start = wall_clock_minus_variable(node)
36
+ return unless start && assigned_from_clock?(node, start)
37
+
38
+ add_offense(node, message: format(MSG, start: start.source))
39
+ end
40
+
41
+ private
42
+
43
+ # Whether the variable is assigned a clock read anywhere in the enclosing method or class.
44
+ def assigned_from_clock?(node, variable)
45
+ scope = node.each_ancestor(:def, :defs, :class, :module).first || processed_source.ast
46
+ scope.each_descendant(:lvasgn, :ivasgn).any? do |assignment|
47
+ assignment.name == variable.children.first && clock_read?(assignment.expression)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,34 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Hecks
4
+ # Flags a direct wall-clock read (`Time.now`, `Date.today`, `Time.current`) in the runtime.
5
+ #
6
+ # The runtime must stay replayable (ADR 0081), so time reaches a command through `needs :now`
7
+ # or through the one stamping seam, `Hecks::Runtime::Event.stamp`; a stray read makes a
8
+ # replay differ from the original run and cannot be pinned in a test.
9
+ #
10
+ # @example
11
+ # Event.new(name: n, occurred_at: Time.now.utc.iso8601) # bad
12
+ # Event.new(name: n, occurred_at: Event.stamp) # good
13
+ class NoWallClockInRuntime < Base
14
+ MSG = "`%<call>s` reads the wall clock inside the runtime, so a replay cannot reproduce it. " \
15
+ "Declare `needs :now` on the command, or stamp an event with `Hecks::Runtime::Event.stamp`.".freeze
16
+
17
+ # @!method wall_clock_read?(node)
18
+ def_node_matcher :wall_clock_read?, <<~PATTERN
19
+ (send (const {nil? cbase} {:Time :Date :DateTime}) {:now :today :current})
20
+ PATTERN
21
+
22
+ # Flags a `Time.now`-style call.
23
+ #
24
+ # @param node [RuboCop::AST::SendNode] the call being visited
25
+ # @return [void]
26
+ def on_send(node)
27
+ return unless wall_clock_read?(node)
28
+
29
+ add_offense(node, message: format(MSG, call: node.source))
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,79 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Hecks
4
+ # Flags a shell command built by interpolating a value into one string.
5
+ #
6
+ # `system("rm -rf #{dir}")` hands the shell a string it re-parses, so a value with a space
7
+ # or a metacharacter changes the command. The array form (`system("rm", "-rf", dir)`)
8
+ # passes each argument as-is. An interpolation wrapped in `Shellwords.escape` or
9
+ # `shellescape` is left alone.
10
+ #
11
+ # @example
12
+ # system("git checkout #{branch}") # bad
13
+ # system("git", "checkout", branch) # good
14
+ class RawShellInterpolation < Base
15
+ MSG = "`%<call>s` runs a string with `\#{}` interpolated into it, so the shell re-parses the value. " \
16
+ "Pass the command as separate arguments (`%<call>s(\"cmd\", arg)`), or wrap the value in " \
17
+ "`Shellwords.escape`.".freeze
18
+
19
+ XSTR_MSG = "A backtick or `%x` command with `\#{}` interpolated into it is re-parsed by the shell. " \
20
+ "Use `IO.popen([\"cmd\", arg], &:read)` or `Open3.capture2(\"cmd\", arg)`, or wrap the " \
21
+ "value in `Shellwords.escape`.".freeze
22
+
23
+ SHELL_CALLS = %i[system exec spawn].freeze
24
+ OPEN3_CALLS = %i[capture2 capture2e capture3 popen2 popen2e popen3 pipeline pipeline_r pipeline_w
25
+ pipeline_rw pipeline_start].freeze
26
+
27
+ # @!method open3_receiver?(node)
28
+ def_node_matcher :open3_receiver?, "(const {nil? cbase} :Open3)"
29
+
30
+ # @!method escaped?(node)
31
+ def_node_matcher :escaped?, <<~PATTERN
32
+ {(send (const {nil? cbase} :Shellwords) {:escape :shellescape :join :shelljoin} ...)
33
+ (send _ {:shellescape :shelljoin})}
34
+ PATTERN
35
+
36
+ # Flags a single-string command with a raw interpolation.
37
+ #
38
+ # @param node [RuboCop::AST::SendNode] the call being visited
39
+ # @return [void]
40
+ def on_send(node)
41
+ return unless shell_call?(node)
42
+
43
+ command = command_argument(node)
44
+ return unless command&.dstr_type? && raw_interpolation?(command)
45
+
46
+ add_offense(node, message: format(MSG, call: node.method_name))
47
+ end
48
+
49
+ # Flags a backtick or `%x` literal with a raw interpolation.
50
+ #
51
+ # @param node [RuboCop::AST::StrNode] the command literal being visited
52
+ # @return [void]
53
+ def on_xstr(node)
54
+ return unless raw_interpolation?(node)
55
+
56
+ add_offense(node, message: XSTR_MSG)
57
+ end
58
+
59
+ private
60
+
61
+ def shell_call?(node)
62
+ return SHELL_CALLS.include?(node.method_name) if node.receiver.nil? || node.receiver.cbase_type?
63
+
64
+ open3_receiver?(node.receiver) && OPEN3_CALLS.include?(node.method_name)
65
+ end
66
+
67
+ # The lone command string, after an optional leading env hash; nil when it passes argv.
68
+ def command_argument(node)
69
+ args = node.arguments.drop_while(&:hash_type?)
70
+ args.first if args.size == 1
71
+ end
72
+
73
+ def raw_interpolation?(node)
74
+ node.each_child_node(:begin).any? { |part| !escaped?(part.children.first) }
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,34 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Hecks
4
+ # Flags a fixed `sleep` in a spec.
5
+ #
6
+ # A fixed sleep is a guess about how long another thread needs: too short and the example is
7
+ # flaky, too long and the suite is slow. Wait on the thing itself instead, with a Queue, a
8
+ # latch, or `ThreadParking.wait_until_parked(thread)` when the assertion is "still blocked".
9
+ #
10
+ # @example
11
+ # sleep 0.2 # bad
12
+ # ThreadParking.wait_until_parked(waiter) # good
13
+ class SleepInSpec < Base
14
+ MSG = "A fixed `sleep` guesses how long another thread needs, so the example is either flaky or " \
15
+ "slow. Wait on the thread itself: a Queue, or `ThreadParking.wait_until_parked(thread)`.".freeze
16
+
17
+ RESTRICT_ON_SEND = %i[sleep].freeze
18
+
19
+ # @!method fixed_sleep?(node)
20
+ def_node_matcher :fixed_sleep?, <<~PATTERN
21
+ (send {nil? (const {nil? cbase} :Kernel)} :sleep ...)
22
+ PATTERN
23
+
24
+ # Flags a `sleep` call.
25
+ #
26
+ # @param node [RuboCop::AST::SendNode] the call being visited
27
+ # @return [void]
28
+ def on_send(node)
29
+ add_offense(node, message: MSG) if fixed_sleep?(node)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,37 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Hecks
4
+ # Flags a broad `rescue` whose whole body is `nil`, `false` or `next`.
5
+ #
6
+ # A bare `rescue` or `rescue StandardError` that answers a literal turns any crash into an
7
+ # ordinary "no result". Rescuing a named error is left alone; a deliberate guard goes
8
+ # through `Hecks::Runtime::BestEffort.call(default) { ... }`.
9
+ #
10
+ # @example
11
+ # adapter.capable? rescue false # bad
12
+ # BestEffort.call(false) { adapter.capable? } # good
13
+ class SwallowedRescue < Base
14
+ MSG = "This broad `rescue` answers `%<answer>s` for any failure, so a crash reads as an ordinary " \
15
+ "\"no result\". Rescue the specific error, or use `Runtime::BestEffort.call(default) { ... }` " \
16
+ "for a deliberate guard.".freeze
17
+
18
+ # @!method swallowed_answer(node)
19
+ def_node_matcher :swallowed_answer, <<~PATTERN
20
+ (resbody {nil? (array (const {nil? cbase} {:StandardError :Exception}))} _
21
+ ${(nil) (false) (next)})
22
+ PATTERN
23
+
24
+ # Flags a broad rescue clause that answers a bare literal.
25
+ #
26
+ # @param node [RuboCop::AST::ResbodyNode] the rescue clause being visited
27
+ # @return [void]
28
+ def on_resbody(node)
29
+ answer = swallowed_answer(node)
30
+ return unless answer
31
+
32
+ add_offense(node, message: format(MSG, answer: answer.source))
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end