hecks 2.9.0 → 3.0.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 (600) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +51 -6
  3. data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
  4. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  5. data/lib/hecks/adapters/driven/lambda.rb +1 -1
  6. data/lib/hecks/adapters/driven/memory.rb +16 -3
  7. data/lib/hecks/adapters/driven/postgres/reconnect.rb +6 -2
  8. data/lib/hecks/adapters/driven/postgres/shared_connection.rb +180 -0
  9. data/lib/hecks/adapters/driven/postgres.rb +4 -2
  10. data/lib/hecks/adapters/driven/prism.rb +41 -8
  11. data/lib/hecks/adapters/driven/tenant_provisioner.rb +73 -16
  12. data/lib/hecks/behaviors/expectations.rb +1 -1
  13. data/lib/hecks/behaviors.rb +1 -1
  14. data/lib/hecks/bench/cli.rb +62 -0
  15. data/lib/hecks/bench/environment.rb +71 -0
  16. data/lib/hecks/bench/postgres_probe.rb +30 -0
  17. data/lib/hecks/bench/report.rb +76 -0
  18. data/lib/hecks/bench/ruby_runner.rb +93 -0
  19. data/lib/hecks/bench/run.rb +55 -0
  20. data/lib/hecks/bench/rust_runner.rb +101 -0
  21. data/lib/hecks/bench/stats.rb +46 -0
  22. data/lib/hecks/bench/suite.rb +98 -0
  23. data/lib/hecks/bench/workload.rb +89 -0
  24. data/lib/hecks/bench.rb +17 -0
  25. data/lib/hecks/bluebook/assembly/contracts.rb +2 -0
  26. data/lib/hecks/bluebook/attribute.rb +1 -1
  27. data/lib/hecks/bluebook/behaviour/aggregate.rb +30 -1
  28. data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -0
  29. data/lib/hecks/bluebook/chapter.rb +6 -2
  30. data/lib/hecks/bluebook/domain_port.rb +36 -5
  31. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +10 -0
  32. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +8 -0
  33. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -1
  34. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +31 -3
  35. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +122 -3
  36. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +1 -1
  37. data/lib/hecks/bluebook/dsl/query_builder.rb +27 -1
  38. data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -1
  39. data/lib/hecks/bluebook/dsl/world_builder.rb +29 -2
  40. data/lib/hecks/bluebook/expression/canonical_form.rb +1 -1
  41. data/lib/hecks/bluebook/expression/resolver.rb +3 -2
  42. data/lib/hecks/bluebook/hexagon.rb +13 -5
  43. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -0
  44. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +4 -1
  45. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +246 -0
  46. data/lib/hecks/bluebook/meta_validator.rb +9 -4
  47. data/lib/hecks/bluebook/model_check.rb +23 -0
  48. data/lib/hecks/bluebook/pattern_subset.rb +38 -0
  49. data/lib/hecks/bluebook/query.rb +20 -5
  50. data/lib/hecks/bluebook/smoke_test.rb +1 -1
  51. data/lib/hecks/canonical_json.rb +30 -0
  52. data/lib/hecks/chapters.rb +102 -0
  53. data/lib/hecks/cli/behaviors.rb +61 -0
  54. data/lib/hecks/cli/canonicalise.rb +25 -0
  55. data/lib/hecks/cli/check_engine_agreement.rb +32 -0
  56. data/lib/hecks/cli/check_era.rb +88 -0
  57. data/lib/hecks/cli/console.rb +66 -0
  58. data/lib/hecks/cli/deploy_template_diff.rb +40 -0
  59. data/lib/hecks/cli/doc_coverage.rb +32 -0
  60. data/lib/hecks/cli/document.rb +2 -2
  61. data/lib/hecks/cli/expression_projection.rb +41 -0
  62. data/lib/hecks/cli/follow.rb +88 -0
  63. data/lib/hecks/cli/history.rb +44 -0
  64. data/lib/hecks/cli/ir.rb +1 -1
  65. data/lib/hecks/cli/mcp.rb +3 -3
  66. data/lib/hecks/cli/model_check.rb +7 -4
  67. data/lib/hecks/cli/pattern_cases.rb +47 -0
  68. data/lib/hecks/cli/present.rb +39 -0
  69. data/lib/hecks/cli/project_cli.rb +224 -32
  70. data/lib/hecks/cli/project_diagrams.rb +4 -3
  71. data/lib/hecks/cli/project_glossary.rb +44 -0
  72. data/lib/hecks/cli/reference.rb +46 -0
  73. data/lib/hecks/cli/refresh_projections.rb +44 -0
  74. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +150 -0
  75. data/lib/hecks/cli/release.rb +70 -0
  76. data/lib/hecks/cli/release_gem.rb +59 -0
  77. data/lib/hecks/cli/rspec_io_parallel_files.rb +137 -0
  78. data/lib/hecks/cli/rspec_shard_files.rb +66 -0
  79. data/lib/hecks/cli/run.rb +3 -3
  80. data/lib/hecks/cli/seed_semantics_corpus.rb +71 -0
  81. data/lib/hecks/cli/serve_query_ir_mcp.rb +30 -0
  82. data/lib/hecks/cli/shape.rb +70 -0
  83. data/lib/hecks/cli/smoke_http.rb +230 -0
  84. data/lib/hecks/cli/smoke_test.rb +5 -3
  85. data/lib/hecks/cli/spec_example.rb +27 -0
  86. data/lib/hecks/cli/statements.rb +32 -0
  87. data/lib/hecks/cli/stores.rb +4 -5
  88. data/lib/hecks/cli/stress_concurrency_specs.rb +177 -0
  89. data/lib/hecks/cli.rb +201 -52
  90. data/lib/hecks/codemod.rb +324 -0
  91. data/lib/hecks/corpus.rb +423 -0
  92. data/lib/hecks/deploy/bluebook/deploy.bluebook +591 -23
  93. data/lib/hecks/deploy/bluebook/deploy.hecksagon +41 -4
  94. data/lib/hecks/deploy/oidc.json +82 -2
  95. data/lib/hecks/doc/reference.rb +351 -0
  96. data/lib/hecks/doors/cli_door.rb +196 -0
  97. data/lib/hecks/doors/cli_runner.rb +297 -0
  98. data/lib/hecks/{facade → doors}/command_request.rb +1 -1
  99. data/lib/hecks/{facade → doors}/handle.rb +2 -2
  100. data/lib/hecks/{facade → doors}/json_door.rb +4 -4
  101. data/lib/hecks/doors/launcher_options.rb +129 -0
  102. data/lib/hecks/{cli → doors}/mcp_door.rb +7 -7
  103. data/lib/hecks/doors/mcp_door_scope.rb +97 -0
  104. data/lib/hecks/{facade/surface → doors/ruby_door}/aggregate_door.rb +8 -3
  105. data/lib/hecks/{facade/surface → doors/ruby_door}/chapter.rb +3 -3
  106. data/lib/hecks/{facade/surface.rb → doors/ruby_door.rb} +36 -5
  107. data/lib/hecks/doors.rb +61 -0
  108. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +1 -1
  109. data/lib/hecks/embryonaut_bluebook.rb +1 -1
  110. data/lib/hecks/engine_agreement.rb +156 -0
  111. data/lib/hecks/facade.rb +24 -37
  112. data/lib/hecks/forms/app.rb +6 -6
  113. data/lib/hecks/forms/banking_presentation.rb +50 -0
  114. data/lib/hecks/forms/field_shape.rb +2 -1
  115. data/lib/hecks/forms/port_argument.rb +1 -1
  116. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +393 -0
  117. data/lib/hecks/fuzzing/combination_miner.rb +86 -0
  118. data/lib/hecks/fuzzing/concurrent_dispatch.rb +214 -0
  119. data/lib/hecks/fuzzing/coverage_campaign.rb +143 -0
  120. data/lib/hecks/fuzzing/differential.rb +156 -0
  121. data/lib/hecks/fuzzing/domain_generator.rb +615 -0
  122. data/lib/hecks/fuzzing/era_boundary.rb +71 -0
  123. data/lib/hecks/fuzzing/form_census.rb +136 -0
  124. data/lib/hecks/fuzzing/generated_domain_check.rb +93 -0
  125. data/lib/hecks/fuzzing/invalid_value_generator.rb +64 -0
  126. data/lib/hecks/fuzzing/isolated_boot.rb +301 -0
  127. data/lib/hecks/fuzzing/nondeterministic.rb +51 -0
  128. data/lib/hecks/fuzzing/outside_world.rb +45 -0
  129. data/lib/hecks/fuzzing/persistence_parity.rb +102 -0
  130. data/lib/hecks/fuzzing/properties/corrections.rb +88 -0
  131. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +314 -0
  132. data/lib/hecks/fuzzing/properties/guards.rb +190 -0
  133. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +300 -0
  134. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +93 -0
  135. data/lib/hecks/fuzzing/properties/outbox.rb +89 -0
  136. data/lib/hecks/fuzzing/properties/querying.rb +175 -0
  137. data/lib/hecks/fuzzing/properties.rb +162 -0
  138. data/lib/hecks/fuzzing/qa_settings.rb +123 -0
  139. data/lib/hecks/fuzzing/replay.rb +505 -0
  140. data/lib/hecks/fuzzing/rotation_priority.rb +48 -0
  141. data/lib/hecks/fuzzing/rust_gap_manifest.rb +108 -0
  142. data/lib/hecks/fuzzing/self_consistency.rb +339 -0
  143. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +385 -0
  144. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +132 -0
  145. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +95 -0
  146. data/lib/hecks/fuzzing/sequence_generator/picker.rb +77 -0
  147. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +109 -0
  148. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +237 -0
  149. data/lib/hecks/fuzzing/sequence_generator.rb +189 -0
  150. data/lib/hecks/fuzzing/shrinker.rb +166 -0
  151. data/lib/hecks/fuzzing/structural_skips.rb +33 -0
  152. data/lib/hecks/fuzzing/sweep_depth.rb +43 -0
  153. data/lib/hecks/fuzzing/target_capabilities.rb +163 -0
  154. data/lib/hecks/fuzzing/value_generator.rb +216 -0
  155. data/lib/hecks/fuzzing.rb +23 -0
  156. data/lib/hecks/grammar/evolve.rb +336 -0
  157. data/lib/hecks/hecks/adapters/codebase/codemods.rb +45 -0
  158. data/lib/hecks/hecks/adapters/codebase/conformance.rb +71 -0
  159. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +202 -0
  160. data/lib/hecks/hecks/adapters/codebase/gem_registry.rb +80 -0
  161. data/lib/hecks/hecks/adapters/codebase/kernel_tables.rb +54 -0
  162. data/lib/hecks/hecks/adapters/codebase/language.rb +240 -0
  163. data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +102 -0
  164. data/lib/hecks/hecks/adapters/codebase/publishing.rb +142 -0
  165. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +67 -0
  166. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +90 -0
  167. data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +83 -0
  168. data/lib/hecks/hecks/adapters/codebase/secret_vault.rb +36 -0
  169. data/lib/hecks/hecks/adapters/codebase/source_tree.adapter +7 -0
  170. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +189 -0
  171. data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +71 -0
  172. data/lib/hecks/hecks/adapters/codebase/style.rb +171 -0
  173. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +55 -0
  174. data/lib/hecks/hecks/adapters/codebase/test_suite.rb +165 -0
  175. data/lib/hecks/hecks/adapters/codebase/tree.rb +111 -0
  176. data/lib/hecks/hecks/adapters/console_capture.rb +79 -0
  177. data/lib/hecks/hecks/adapters/deploy_toolchain.adapter +7 -0
  178. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +108 -0
  179. data/lib/hecks/hecks/adapters/git.adapter +7 -0
  180. data/lib/hecks/hecks/adapters/git.rb +102 -0
  181. data/lib/hecks/hecks/adapters/host_http.adapter +7 -0
  182. data/lib/hecks/hecks/adapters/host_http.rb +65 -0
  183. data/lib/hecks/hecks/adapters/in_process_boot.adapter +7 -0
  184. data/lib/hecks/hecks/adapters/in_process_boot.rb +197 -0
  185. data/lib/hecks/hecks/adapters/in_process_operations.rb +202 -0
  186. data/lib/hecks/hecks/adapters/journal_store/changes.rb +143 -0
  187. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +145 -0
  188. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +132 -0
  189. data/lib/hecks/hecks/adapters/journal_store/examination.rb +135 -0
  190. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +163 -0
  191. data/lib/hecks/hecks/adapters/journal_store/readings.rb +187 -0
  192. data/lib/hecks/hecks/adapters/journal_store.adapter +7 -0
  193. data/lib/hecks/hecks/adapters/journal_store.rb +64 -0
  194. data/lib/hecks/hecks/adapters/local_files.adapter +7 -0
  195. data/lib/hecks/hecks/adapters/local_files.rb +46 -0
  196. data/lib/hecks/hecks/adapters/pg_admin.adapter +7 -0
  197. data/lib/hecks/hecks/adapters/pg_admin.rb +245 -0
  198. data/lib/hecks/hecks/adapters/process_pool.adapter +7 -0
  199. data/lib/hecks/hecks/adapters/process_pool.rb +119 -0
  200. data/lib/hecks/hecks/adapters/rust_toolchain.adapter +7 -0
  201. data/lib/hecks/hecks/adapters/rust_toolchain.rb +179 -0
  202. data/lib/hecks/hecks/adapters/rust_workspace.rb +150 -0
  203. data/lib/hecks/hecks/adapters/shell.adapter +7 -0
  204. data/lib/hecks/hecks/adapters/shell.rb +49 -0
  205. data/lib/hecks/hecks/adapters/terminal.adapter +7 -0
  206. data/lib/hecks/hecks/adapters/terminal.rb +74 -0
  207. data/lib/hecks/hecks/codebase.behaviors +172 -0
  208. data/lib/hecks/hecks/codebase.bluebook +2453 -0
  209. data/lib/hecks/hecks/context_map.hecksagon +13 -0
  210. data/lib/hecks/hecks/custodian.behaviors +179 -0
  211. data/lib/hecks/hecks/custodian.bluebook +1847 -0
  212. data/lib/hecks/hecks/deploy.behaviors +74 -0
  213. data/lib/hecks/hecks/deploy.world +12 -0
  214. data/lib/hecks/hecks/environments/memory.world +10 -0
  215. data/lib/hecks/hecks/governance.world +7 -0
  216. data/lib/hecks/hecks/hecks.behaviors +62 -0
  217. data/lib/hecks/hecks/hecks.bluebook +124 -0
  218. data/lib/hecks/hecks/hecks.hecksagon +390 -0
  219. data/lib/hecks/hecks/hecks.world +42 -0
  220. data/lib/hecks/kernel_capabilities.rb +194 -0
  221. data/lib/hecks/language/bluebook/bluebook.bluebook +11 -0
  222. data/lib/hecks/language/bluebook/query.bluebook +7 -0
  223. data/lib/hecks/language/bluebook/vocabulary.bluebook +4 -4
  224. data/lib/hecks/language/hecksagon/domain_port.bluebook +10 -0
  225. data/lib/hecks/language/hecksagon/hecksagon.bluebook +20 -0
  226. data/lib/hecks/mcp_stdio_guard.rb +2 -2
  227. data/lib/hecks/persistence_legacy_fixture/regenerate.rb +161 -0
  228. data/lib/hecks/persistence_legacy_fixture.rb +113 -0
  229. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +8 -5
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +4 -1
  231. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +15 -2
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +1 -1
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +1 -1
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +1 -1
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +32 -15
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +6 -3
  237. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +227 -0
  238. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +1 -1
  239. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +43 -24
  240. data/lib/hecks/ports/persistence/plugins/era/translation.rb +1 -0
  241. data/lib/hecks/ports/persistence/state_codec.rb +24 -5
  242. data/lib/hecks/projection_files.rb +285 -0
  243. data/lib/hecks/projections/bootstrap_table.rb +1 -1
  244. data/lib/hecks/projections/deploy/fargate.rb +5 -5
  245. data/lib/hecks/projections/deploy/lambda.rb +43 -32
  246. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  247. data/lib/hecks/projections/deploy/preview/template.rb +1 -1
  248. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +1 -1
  249. data/lib/hecks/projections/deploy/scripts/expected-era.header +1 -1
  250. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +3 -3
  251. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +1 -1
  252. data/lib/hecks/projections/deploy/shared.rb +1 -1
  253. data/lib/hecks/projections/diagrams.rb +3 -3
  254. data/lib/hecks/projections/model/deviations.rb +1 -1
  255. data/lib/hecks/projections/parser_table.rb +4 -4
  256. data/lib/hecks/projections/reference.rb +2 -3
  257. data/lib/hecks/projections/rust_vocabulary.rb +1 -1
  258. data/lib/hecks/projector/cli_projector.rb +106 -37
  259. data/lib/hecks/projector.rb +1 -1
  260. data/lib/hecks/quality_control/adapters/agent.adapter +7 -0
  261. data/lib/hecks/quality_control/adapters/agent.rb +58 -0
  262. data/lib/hecks/quality_control/adapters/angle_tools.adapter +7 -0
  263. data/lib/hecks/quality_control/adapters/angle_tools.rb +15 -0
  264. data/lib/hecks/quality_control/adapters/clearance_recorder.rb +36 -0
  265. data/lib/hecks/quality_control/adapters/clearance_tools.adapter +7 -0
  266. data/lib/hecks/quality_control/adapters/clearance_tools.rb +17 -0
  267. data/lib/hecks/quality_control/adapters/git_pr.adapter +7 -0
  268. data/lib/hecks/quality_control/adapters/git_pr.rb +226 -0
  269. data/lib/hecks/quality_control/adapters/github_checks.adapter +9 -0
  270. data/lib/hecks/quality_control/adapters/github_checks.rb +83 -0
  271. data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +56 -0
  272. data/lib/hecks/quality_control/adapters/github_issues.adapter +7 -0
  273. data/lib/hecks/quality_control/adapters/github_issues.rb +49 -0
  274. data/lib/hecks/quality_control/adapters/qa_tool.rb +59 -0
  275. data/lib/hecks/quality_control/adapters/sweep_tools.adapter +7 -0
  276. data/lib/hecks/quality_control/adapters/sweep_tools.rb +51 -0
  277. data/lib/hecks/quality_control/adapters/target_tools.adapter +7 -0
  278. data/lib/hecks/quality_control/adapters/target_tools.rb +43 -0
  279. data/lib/hecks/quality_control/cli/child.rb +45 -0
  280. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +45 -0
  281. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +248 -0
  282. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +163 -0
  283. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +450 -0
  284. data/lib/hecks/quality_control/cli/qa_log_bug.rb +195 -0
  285. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +220 -0
  286. data/lib/hecks/quality_control/cli/qa_open_pr.rb +305 -0
  287. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +154 -0
  288. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +159 -0
  289. data/lib/hecks/quality_control/cli/qa_pr_check.rb +232 -0
  290. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +142 -0
  291. data/lib/hecks/quality_control/cli/qa_seed_targets.rb +69 -0
  292. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +280 -0
  293. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +329 -0
  294. data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +92 -0
  295. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +117 -0
  296. data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +175 -0
  297. data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +60 -0
  298. data/lib/hecks/quality_control/cli/qa_sweep.rb +611 -0
  299. data/lib/hecks/quality_control/cli/qa_tick.rb +274 -0
  300. data/lib/hecks/quality_control/quality_control.bluebook +2224 -0
  301. data/lib/hecks/quality_control/quality_control.hecksagon +13 -0
  302. data/lib/hecks/quality_control/quality_control.ports.hecksagon +54 -0
  303. data/lib/hecks/query_ir.rb +346 -0
  304. data/lib/hecks/query_ir_mcp.rb +198 -0
  305. data/lib/hecks/release/runner/ci_publisher.rb +2 -2
  306. data/lib/hecks/release/runner/clean_tree.rb +42 -0
  307. data/lib/hecks/release/runner/console.rb +6 -0
  308. data/lib/hecks/release/runner/gem_publisher.rb +14 -7
  309. data/lib/hecks/release/runner/npm_publisher.rb +6 -36
  310. data/lib/hecks/release/runner/preflight.rb +26 -2
  311. data/lib/hecks/release/runner/published.rb +8 -26
  312. data/lib/hecks/release/runner/tagger.rb +30 -15
  313. data/lib/hecks/release/runner.rb +57 -6
  314. data/lib/hecks/router/namespace_installer.rb +3 -3
  315. data/lib/hecks/runtime/adapter_lookup.rb +124 -0
  316. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +4 -0
  317. data/lib/hecks/runtime/dependency_planning.rb +20 -2
  318. data/lib/hecks/runtime/dispatcher.rb +53 -3
  319. data/lib/hecks/runtime/interpreting.rb +3 -0
  320. data/lib/hecks/runtime/loader.rb +65 -12
  321. data/lib/hecks/runtime/outbox.rb +1 -1
  322. data/lib/hecks/runtime/policy_interpreter.rb +5 -2
  323. data/lib/hecks/runtime/port_operation_interpreter.rb +2 -10
  324. data/lib/hecks/runtime/query_interpreter.rb +77 -0
  325. data/lib/hecks/runtime/reaction_outcome.rb +89 -0
  326. data/lib/hecks/runtime/refusal_wording.rb +23 -1
  327. data/lib/hecks/runtime/registry/verification.rb +163 -4
  328. data/lib/hecks/runtime/registry.rb +57 -3
  329. data/lib/hecks/runtime/remote_dispatcher.rb +31 -2
  330. data/lib/hecks/runtime/value/coercion.rb +23 -135
  331. data/lib/hecks/runtime/value/field_checks.rb +172 -0
  332. data/lib/hecks/runtime/value.rb +4 -1
  333. data/lib/hecks/runtime.rb +20 -6
  334. data/lib/hecks/rust_build/conformance.rb +158 -0
  335. data/lib/hecks/rust_build/conformance_fuzz.rb +71 -0
  336. data/lib/hecks/rust_build/coverage.rb +218 -0
  337. data/lib/hecks/rust_build/native_build.rb +124 -0
  338. data/lib/hecks/rust_build/project_rust/root_mod.rb +120 -0
  339. data/lib/hecks/rust_build/project_rust.rb +332 -0
  340. data/lib/hecks/rust_build/wasm.rb +105 -0
  341. data/lib/hecks/rust_build/wasm_browser.rb +113 -0
  342. data/lib/hecks/rust_build.rb +144 -0
  343. data/lib/hecks/storehouse.rb +16 -16
  344. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +2 -2
  345. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +1 -1
  346. data/lib/hecks/three_zero/forms.yml +127 -0
  347. data/lib/hecks/three_zero.rb +12 -0
  348. data/lib/hecks/tools/argument_gate_matrix.rb +316 -0
  349. data/lib/hecks/tools/comment_style.rb +1070 -0
  350. data/lib/hecks/tools/corpus_report.rb +87 -0
  351. data/lib/hecks/tools/deploy_recipe.rb +255 -0
  352. data/lib/hecks/tools/deploy_recipe_lint.rb +282 -0
  353. data/lib/hecks/tools/drop_implicit_append_fields.rb +95 -0
  354. data/lib/hecks/tools/era_projection_backfill.rb +94 -0
  355. data/lib/hecks/tools/era_reattest.rb +146 -0
  356. data/lib/hecks/tools/evolve_run.rb +189 -0
  357. data/lib/hecks/tools/fuzz_sweep.rb +373 -0
  358. data/lib/hecks/tools/heki_compaction.rb +100 -0
  359. data/lib/hecks/tools/hoist_local_givens.rb +246 -0
  360. data/lib/hecks/tools/journal_compaction.rb +89 -0
  361. data/lib/hecks/tools/oidc_manifests.rb +70 -0
  362. data/lib/hecks/tools/query_ir_run.rb +81 -0
  363. data/lib/hecks/tools/regeneration_run.rb +166 -0
  364. data/lib/hecks/tools/rust_comment_style.rb +642 -0
  365. data/lib/hecks/tools/sequence_script.rb +62 -0
  366. data/lib/hecks/tools/tail_merge.rb +117 -0
  367. data/lib/hecks/tools/tenant_provisioning.rb +142 -0
  368. data/lib/hecks/tools/translation_audit.rb +208 -0
  369. data/lib/hecks/tools/translation_scaffold.rb +131 -0
  370. data/lib/hecks/tools.rb +86 -0
  371. data/lib/hecks/version.rb +1 -1
  372. data/lib/hecks.rb +32 -15
  373. data/qa/settings.yml +66 -0
  374. data/rust/.cargo/config.toml +6 -0
  375. data/rust/.gitignore +2 -0
  376. data/rust/Cargo.lock +54 -0
  377. data/rust/Cargo.toml +61 -0
  378. data/rust/build/.cargo/config.toml +3 -0
  379. data/rust/build/.gitignore +1 -0
  380. data/rust/build/Cargo.lock +7 -0
  381. data/rust/build/Cargo.toml +19 -0
  382. data/rust/build/src/build_artifact.rs +47 -0
  383. data/rust/build/src/cargo_sync.rs +248 -0
  384. data/rust/build/src/json.rs +421 -0
  385. data/rust/build/src/lineage_pass.rs +285 -0
  386. data/rust/build/src/main.rs +51 -0
  387. data/rust/build/src/optional_pass.rs +150 -0
  388. data/rust/build/src/pipeline.rs +264 -0
  389. data/rust/build/src/reserved_names.rs +76 -0
  390. data/rust/build/src/resolve.rs +124 -0
  391. data/rust/build/src/root.rs +25 -0
  392. data/rust/build/src/sidecars.rs +73 -0
  393. data/rust/build/src/subprocess.rs +51 -0
  394. data/rust/build/src/tmp.rs +31 -0
  395. data/rust/codegen/.cargo/config.toml +3 -0
  396. data/rust/codegen/.gitignore +1 -0
  397. data/rust/codegen/Cargo.lock +7 -0
  398. data/rust/codegen/Cargo.toml +20 -0
  399. data/rust/codegen/src/attr.rs +32 -0
  400. data/rust/codegen/src/bridging.rs +481 -0
  401. data/rust/codegen/src/commands.rs +1226 -0
  402. data/rust/codegen/src/constraints.rs +109 -0
  403. data/rust/codegen/src/dependency_planning.rs +209 -0
  404. data/rust/codegen/src/domain_generator.rs +1288 -0
  405. data/rust/codegen/src/exemplar.rs +230 -0
  406. data/rust/codegen/src/expr_emitter.rs +87 -0
  407. data/rust/codegen/src/fielded.rs +192 -0
  408. data/rust/codegen/src/hecks_naming.rs +33 -0
  409. data/rust/codegen/src/json.rs +335 -0
  410. data/rust/codegen/src/json_codec.rs +697 -0
  411. data/rust/codegen/src/literal.rs +207 -0
  412. data/rust/codegen/src/main.rs +290 -0
  413. data/rust/codegen/src/manifest.rs +227 -0
  414. data/rust/codegen/src/mutations.rs +784 -0
  415. data/rust/codegen/src/naming.rs +286 -0
  416. data/rust/codegen/src/ports.rs +208 -0
  417. data/rust/codegen/src/prelude.rs +116 -0
  418. data/rust/codegen/src/queries.rs +786 -0
  419. data/rust/codegen/src/reactions.rs +532 -0
  420. data/rust/codegen/src/read_models.rs +697 -0
  421. data/rust/codegen/src/reference_specs.rs +40 -0
  422. data/rust/codegen/src/registry.rs +966 -0
  423. data/rust/codegen/src/reserved_names.rs +76 -0
  424. data/rust/codegen/src/shared.rs +93 -0
  425. data/rust/codegen/src/skip_reason.rs +25 -0
  426. data/rust/codegen/src/types.rs +362 -0
  427. data/rust/host/.cargo/config.toml +3 -0
  428. data/rust/host/.gitignore +1 -0
  429. data/rust/host/Cargo.lock +4297 -0
  430. data/rust/host/Cargo.toml +61 -0
  431. data/rust/host/HECKS_RELEASE +1 -0
  432. data/rust/host/rds-ca-bundle.pem +2736 -0
  433. data/rust/host/src/api.rs +1596 -0
  434. data/rust/host/src/approval.rs +652 -0
  435. data/rust/host/src/auth.rs +1287 -0
  436. data/rust/host/src/bin/expr_harness.rs +46 -0
  437. data/rust/host/src/bin/lineage_harness.rs +119 -0
  438. data/rust/host/src/bin/mint_harness.rs +135 -0
  439. data/rust/host/src/checkout.rs +265 -0
  440. data/rust/host/src/dispatch.rs +1443 -0
  441. data/rust/host/src/expr_json.rs +1117 -0
  442. data/rust/host/src/field_hints.rs +52 -0
  443. data/rust/host/src/ir.rs +733 -0
  444. data/rust/host/src/journal.rs +874 -0
  445. data/rust/host/src/lambda_client.rs +378 -0
  446. data/rust/host/src/log.rs +147 -0
  447. data/rust/host/src/main.rs +474 -0
  448. data/rust/host/src/mint.rs +1865 -0
  449. data/rust/host/src/payments/keystore.rs +330 -0
  450. data/rust/host/src/payments/tests.rs +1400 -0
  451. data/rust/host/src/payments.rs +767 -0
  452. data/rust/host/src/presentation.rs +698 -0
  453. data/rust/host/src/presentation_write.rs +1431 -0
  454. data/rust/host/src/query_step.rs +149 -0
  455. data/rust/host/src/rate_limit.rs +904 -0
  456. data/rust/host/src/reference_transform.rs +241 -0
  457. data/rust/host/src/reference_validate.rs +338 -0
  458. data/rust/host/src/resend.rs +296 -0
  459. data/rust/host/src/secrets.rs +106 -0
  460. data/rust/host/src/server.rs +663 -0
  461. data/rust/host/src/storage_shape.rs +175 -0
  462. data/rust/host/src/test_pg.rs +40 -0
  463. data/rust/host/src/ui_schema.rs +1351 -0
  464. data/rust/host/src/wasm_runner.rs +191 -0
  465. data/rust/host/src/web/newsletter/cooldown.rs +126 -0
  466. data/rust/host/src/web/newsletter/tests.rs +104 -0
  467. data/rust/host/src/web/newsletter.rs +310 -0
  468. data/rust/host/src/web/newsletter_send.rs +357 -0
  469. data/rust/host/src/web/registrations.rs +1505 -0
  470. data/rust/host/src/web.rs +2937 -0
  471. data/rust/lsp/.gitignore +1 -0
  472. data/rust/lsp/Cargo.lock +7 -0
  473. data/rust/lsp/Cargo.toml +16 -0
  474. data/rust/lsp/README.md +133 -0
  475. data/rust/lsp/src/diagnostics.rs +165 -0
  476. data/rust/lsp/src/json.rs +385 -0
  477. data/rust/lsp/src/main.rs +399 -0
  478. data/rust/lsp/src/outline.rs +177 -0
  479. data/rust/lsp/src/rpc.rs +67 -0
  480. data/rust/parser/.cargo/config.toml +5 -0
  481. data/rust/parser/.gitignore +1 -0
  482. data/rust/parser/Cargo.lock +7 -0
  483. data/rust/parser/Cargo.toml +21 -0
  484. data/rust/parser/src/build/closed_sets.rs +27 -0
  485. data/rust/parser/src/build/identity.rs +276 -0
  486. data/rust/parser/src/build/mod.rs +12 -0
  487. data/rust/parser/src/build/naming.rs +141 -0
  488. data/rust/parser/src/build/pattern_subset.rs +161 -0
  489. data/rust/parser/src/build/query_derive.rs +72 -0
  490. data/rust/parser/src/build/query_inference.rs +269 -0
  491. data/rust/parser/src/build/query_options.rs +100 -0
  492. data/rust/parser/src/build/read_model.rs +94 -0
  493. data/rust/parser/src/build/references.rs +32 -0
  494. data/rust/parser/src/canonical.rs +114 -0
  495. data/rust/parser/src/diag.rs +55 -0
  496. data/rust/parser/src/emit.rs +953 -0
  497. data/rust/parser/src/expr/ast_json.rs +147 -0
  498. data/rust/parser/src/expr/evaluator.rs +81 -0
  499. data/rust/parser/src/expr/mod.rs +84 -0
  500. data/rust/parser/src/expr/resolver.rs +497 -0
  501. data/rust/parser/src/ir.rs +387 -0
  502. data/rust/parser/src/keywords.rs +585 -0
  503. data/rust/parser/src/lex.rs +662 -0
  504. data/rust/parser/src/main.rs +252 -0
  505. data/rust/parser/src/parse/aggregate.rs +612 -0
  506. data/rust/parser/src/parse/chapter.rs +517 -0
  507. data/rust/parser/src/parse/command.rs +637 -0
  508. data/rust/parser/src/parse/domain_port.rs +127 -0
  509. data/rust/parser/src/parse/entity.rs +527 -0
  510. data/rust/parser/src/parse/file.rs +67 -0
  511. data/rust/parser/src/parse/hecksagon.rs +231 -0
  512. data/rust/parser/src/parse/lifecycle.rs +162 -0
  513. data/rust/parser/src/parse/mod.rs +1262 -0
  514. data/rust/parser/src/parse/policy.rs +87 -0
  515. data/rust/parser/src/parse/process_manager.rs +305 -0
  516. data/rust/parser/src/parse/query.rs +100 -0
  517. data/rust/parser/src/parse/read_model.rs +214 -0
  518. data/rust/parser/src/parse/value_object.rs +250 -0
  519. data/rust/parser/src/ruby_value.rs +422 -0
  520. data/rust/project/bridging.rb +257 -0
  521. data/rust/project/commands.rb +877 -0
  522. data/rust/project/constraints.rb +86 -0
  523. data/rust/project/dependency_planning.rb +178 -0
  524. data/rust/project/domain_generator.rb +980 -0
  525. data/rust/project/exemplar.rb +148 -0
  526. data/rust/project/expr_emitter.rb +72 -0
  527. data/rust/project/fielded.rb +141 -0
  528. data/rust/project/json_codec.rb +641 -0
  529. data/rust/project/mutations.rb +721 -0
  530. data/rust/project/naming.rb +205 -0
  531. data/rust/project/ports.rb +65 -0
  532. data/rust/project/queries.rb +493 -0
  533. data/rust/project/reactions.rb +317 -0
  534. data/rust/project/read_models.rb +690 -0
  535. data/rust/project/reference_specs.rb +27 -0
  536. data/rust/project/registry.rb +396 -0
  537. data/rust/project/skip_reason.rb +29 -0
  538. data/rust/project/types.rb +225 -0
  539. data/rust/project/write_if_changed.rb +83 -0
  540. data/rust/project.rb +30 -0
  541. data/rust/project_rust_pipeline.rb +383 -0
  542. data/rust/src/exemplar/commands.rs +279 -0
  543. data/rust/src/exemplar/constraints.rs +18 -0
  544. data/rust/src/exemplar/fielded.rs +280 -0
  545. data/rust/src/exemplar/json.rs +277 -0
  546. data/rust/src/exemplar/mod.rs +17 -0
  547. data/rust/src/exemplar/mutations.rs +80 -0
  548. data/rust/src/exemplar/queries.rs +33 -0
  549. data/rust/src/exemplar/reactions.rs +88 -0
  550. data/rust/src/exemplar/read_models.rs +48 -0
  551. data/rust/src/exemplar/registry.rs +166 -0
  552. data/rust/src/exemplar/types.rs +44 -0
  553. data/rust/src/kernel/attribute_shapes/composite.rs +88 -0
  554. data/rust/src/kernel/attribute_shapes/list.rs +18 -0
  555. data/rust/src/kernel/attribute_shapes/mod.rs +44 -0
  556. data/rust/src/kernel/attribute_shapes/optional.rs +9 -0
  557. data/rust/src/kernel/attribute_shapes/scalar.rs +39 -0
  558. data/rust/src/kernel/cli.rs +585 -0
  559. data/rust/src/kernel/dispatch.rs +1090 -0
  560. data/rust/src/kernel/expr.rs +458 -0
  561. data/rust/src/kernel/expression_operators/arithmetic.rs +128 -0
  562. data/rust/src/kernel/expression_operators/comparison.rs +48 -0
  563. data/rust/src/kernel/expression_operators/enumeration.rs +232 -0
  564. data/rust/src/kernel/expression_operators/logical.rs +13 -0
  565. data/rust/src/kernel/expression_operators/membership.rs +18 -0
  566. data/rust/src/kernel/expression_operators/mod.rs +68 -0
  567. data/rust/src/kernel/expression_operators/pattern_match.rs +73 -0
  568. data/rust/src/kernel/expression_operators/positional.rs +126 -0
  569. data/rust/src/kernel/expression_operators/presence.rs +101 -0
  570. data/rust/src/kernel/expression_operators/sign_test.rs +16 -0
  571. data/rust/src/kernel/expression_operators/sized.rs +35 -0
  572. data/rust/src/kernel/expression_operators/text.rs +79 -0
  573. data/rust/src/kernel/expression_operators/to_string.rs +18 -0
  574. data/rust/src/kernel/json.rs +779 -0
  575. data/rust/src/kernel/mod.rs +133 -0
  576. data/rust/src/kernel/named_query.rs +219 -0
  577. data/rust/src/kernel/orchestrate.rs +1810 -0
  578. data/rust/src/kernel/pattern.rs +307 -0
  579. data/rust/src/kernel/query_comparators.rs +206 -0
  580. data/rust/src/kernel/query_ordering.rs +103 -0
  581. data/rust/src/kernel/read_model.rs +914 -0
  582. data/rust/src/kernel/reference_lookup.rs +149 -0
  583. data/rust/src/kernel/repository.rs +468 -0
  584. data/rust/src/kernel/routing.rs +612 -0
  585. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +118 -0
  586. data/rust/src/kernel/vocab/entity_dispatch_order.rs +114 -0
  587. data/rust/src/kernel/vocab/field_hint.rs +70 -0
  588. data/rust/src/kernel/vocab/mod.rs +20 -0
  589. data/rust/src/kernel/vocab/query_comparator.rs +65 -0
  590. data/rust/src/kernel/vocab/refusal_template.rs +1592 -0
  591. data/rust/src/lib.rs +8 -0
  592. data/rust/src/main.rs +16 -0
  593. data/rust/web/.gitignore +1 -0
  594. data/rust/web/Cargo.lock +119 -0
  595. data/rust/web/Cargo.toml +18 -0
  596. data/rust/web/src/lib.rs +8 -0
  597. metadata +477 -12
  598. data/lib/hecks/facade/cli_door.rb +0 -96
  599. data/lib/hecks/facade/cli_runner.rb +0 -127
  600. data/lib/hecks/mcp_door_scope.rb +0 -95
@@ -0,0 +1,642 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "optparse"
5
+ require_relative "../tools"
6
+ require_relative "comment_style"
7
+
8
+ module Hecks
9
+ module Tools
10
+ # Checks Rust files against docs/COMMENT_STYLE_GUIDE_RUST.md, sharing its
11
+ # prose vocabulary with `CommentStyle`, the Ruby-side linter.
12
+ module RustCommentStyle
13
+ MAX_LINE = CommentStyle::MAX_LINE
14
+ MAX_HEADING_WORDS = CommentStyle::MAX_HEADING_WORDS
15
+
16
+ CATEGORIES = {
17
+ "missing_doc" => "public item has no /// doc comment",
18
+ "missing_module_doc" => "file has no //! module doc at its top",
19
+ "all_caps" => "all-caps emphasis",
20
+ "long_bold" => "bold heading over #{MAX_HEADING_WORDS} words reads as shouting",
21
+ "bare_constant" => "capitalised word names a const/static in this file: backtick or lowercase it",
22
+ "unknown_capitals" => "capitalised token is not an English word: backtick it or list it as an acronym",
23
+ "design_history" => "narrates design history instead of citing an ADR",
24
+ "long_line" => "comment line exceeds #{MAX_LINE} characters"
25
+ }.freeze
26
+
27
+ FIXABLE = %w[all_caps long_bold long_line].freeze
28
+
29
+ MARKER = %r{\A/{2,3}!?\s*}
30
+ DOC_LINE = %r{\A\s*(?:///(?!/)|//!)}
31
+
32
+ # `// TMPL:name BEGIN`/`END` sentinels are parsed verbatim by
33
+ # `rust/project/exemplar.rb`; every prose rule skips them outright.
34
+ TMPL_MARKER = /\bTMPL:\S+\s+(?:BEGIN|END)\b/i
35
+ MOD_DOC_LINE = %r{\A\s*//!}
36
+ ATTR_LINE = /\A\s*#\[/
37
+ BOLD_HEADING = %r{\A(/{2,3}!?\s*(?:[-*]\s+)?)\*\*([^*]+)\*\*}
38
+ HEADING_END = CommentStyle::HEADING_END
39
+ GAP = CommentStyle::GAP
40
+ FILLER = CommentStyle::FILLER
41
+
42
+ # `main` is a pub fn entry point, not an API surface to document.
43
+ SELF_EVIDENT_FNS = %w[main].to_set.freeze
44
+
45
+ # Rust-specific acronyms beyond the Ruby linter's list.
46
+ RUST_ACRONYMS = %w[EOF RAII RHS SQS DLQ JIT LSP NFA DFA CR].to_set.freeze
47
+
48
+ RUST_PROPER_NOUNS = {
49
+ "EMBRYONAUT" => "Embryonaut"
50
+ }.freeze
51
+
52
+ RUST_EXTRA_WORDS = %w[
53
+ cardinality routability prepend safer diff diffed stats earliest unbracketed
54
+ variadic ordering orderings reenter dereference entrypoint wildcard
55
+ sandbox sandboxed unsandboxed expr enum codegen serialise serialises
56
+ preprocessing backslash
57
+ ].freeze
58
+
59
+ PUB_ITEM = /
60
+ \A\s*pub(?:\([^)]*\))?\s+
61
+ (?:async\s+|unsafe\s+|extern\s+"[^"]*"\s+|const\s+)*
62
+ (fn|struct|enum|trait|mod|type)\s+
63
+ ([A-Za-z_][A-Za-z0-9_]*)
64
+ /x
65
+
66
+ Violation = Struct.new(:path, :line, :category, :message)
67
+ Comment = Struct.new(:line, :col, :text, :full_line)
68
+
69
+ # Finds Rust source's line comments, distinguishing them from `//` inside
70
+ # code, strings, raw strings, and char literals. Block comments are not recognised.
71
+ class Tokenizer
72
+ # Lexes `source` for its line comments in one pass.
73
+ def self.comments(source)
74
+ new(source).run
75
+ end
76
+
77
+ def initialize(source)
78
+ @src = source
79
+ @len = source.length
80
+ @comments = []
81
+ end
82
+
83
+ # Walks the whole source once, collecting every line comment.
84
+ def run
85
+ pos = 0
86
+ line = 1
87
+ col = 0
88
+ while pos < @len
89
+ case @src[pos]
90
+ when "\n"
91
+ line += 1
92
+ col = 0
93
+ pos += 1
94
+ when "/" then pos, col = consume_slash(pos, col, line)
95
+ when '"' then pos, col, line = consume_string(pos, col, line)
96
+ when "'" then pos, col = consume_quote(pos, col)
97
+ when "r" then pos, col, line = consume_maybe_raw_string(pos, col, line)
98
+ else
99
+ pos += 1
100
+ col += 1
101
+ end
102
+ end
103
+ @comments
104
+ end
105
+
106
+ private
107
+
108
+ def consume_slash(pos, col, line)
109
+ return advance_plain(pos, col) unless @src[pos, 2] == "//"
110
+
111
+ finish = @src.index("\n", pos) || @len
112
+ text = @src[pos...finish]
113
+ last_newline = @src.rindex("\n", pos.zero? ? 0 : pos - 1)
114
+ prefix = last_newline && last_newline < pos ? @src[(last_newline + 1)...pos] : @src[0...pos]
115
+ full_line = prefix.strip.empty?
116
+ @comments << Comment.new(line, col, text, full_line)
117
+ [finish, col + text.length]
118
+ end
119
+
120
+ def consume_string(pos, col, line)
121
+ idx = pos + 1
122
+ idx += (@src[idx] == "\\" ? 2 : 1) while idx < @len && @src[idx] != '"'
123
+ advance_span(pos, idx + 1, col, line)
124
+ end
125
+
126
+ def consume_quote(pos, col)
127
+ match = @src[pos..].match(/\A'(?:\\.|[^'\\\n]){1}'/)
128
+ match ? [pos + match[0].length, col + match[0].length] : [pos + 1, col + 1]
129
+ end
130
+
131
+ def consume_maybe_raw_string(pos, col, line)
132
+ idx = pos + 1
133
+ hashes = 0
134
+ hashes += 1 while @src[idx + hashes] == "#"
135
+ return [pos + 1, col + 1, line] unless @src[idx + hashes] == '"'
136
+
137
+ close = @src.index("\"#{'#' * hashes}", idx + hashes + 1) || @len
138
+ advance_span(pos, close + 1 + hashes, col, line)
139
+ end
140
+
141
+ def advance_span(from, to, col, line)
142
+ segment = @src[from...to]
143
+ newlines = segment.count("\n")
144
+ if newlines.positive?
145
+ col = segment.length - segment.rindex("\n") - 1
146
+ line += newlines
147
+ else
148
+ col += segment.length
149
+ end
150
+ [to, col, line]
151
+ end
152
+
153
+ def advance_plain(pos, col)
154
+ [pos + 1, col + 1]
155
+ end
156
+ end
157
+
158
+ # Folds Rust jargon into the shared word list so `Dictionary.english?`
159
+ # already covers these words' plurals, gerunds, and past tense too.
160
+ CommentStyle::Dictionary.words&.merge(RUST_EXTRA_WORDS)
161
+
162
+ # One parsed `.rs` file and the violations found in it.
163
+ class SourceFile
164
+ attr_reader :path, :lines, :comments
165
+
166
+ def initialize(path, source = nil)
167
+ @path = path
168
+ @source = source || File.read(path)
169
+ @lines = @source.lines
170
+ @comments = generated? ? [] : Tokenizer.comments(@source)
171
+ @by_line = @comments.to_h { |c| [c.line, c] }
172
+ @open_spans = open_spans
173
+ @constants = code_constants
174
+ end
175
+
176
+ # Finds every place this file departs from the style guide.
177
+ def violations(only: nil)
178
+ return [] if generated?
179
+
180
+ found = text_violations + structure_violations
181
+ found = found.select { |v| only.include?(v.category) } if only
182
+ found.sort_by { |v| [v.line, v.category] }
183
+ end
184
+
185
+ # Rewrites the mechanical categories.
186
+ def fixed(only: nil)
187
+ return @source if generated?
188
+
189
+ wanted = (only || FIXABLE) & FIXABLE
190
+ @lines.map.with_index(1) { |line, number| fix_line(line, number, wanted) }.join
191
+ end
192
+
193
+ # A generated file's comments belong to its generator: `hecks project_rust`
194
+ # and friends stamp this exact banner on line one.
195
+ def generated?
196
+ @lines.first.to_s.start_with?("// GENERATED by")
197
+ end
198
+
199
+ private
200
+
201
+ def open_spans
202
+ inside = false
203
+ @comments.each_with_object({}) do |comment, open|
204
+ above = @by_line[comment.line - 1]
205
+ continues = comment.full_line && above&.full_line && !above.text.match?(%r{\A/{2,3}!?\s*\z})
206
+ inside = false unless continues
207
+ open[comment.line] = inside
208
+ inside ^= comment.text.count("`").odd?
209
+ end
210
+ end
211
+
212
+ # SCREAMING_SNAKE names this file defines: `const`/`static` items, plus
213
+ # enum variants and macro-visible names that read the same way.
214
+ def code_constants
215
+ code_only = @source.dup
216
+ Tokenizer.comments(@source).each { |c| code_only[c.line - 1] ||= "" }
217
+ names = Set.new
218
+ code_only.scan(/\b(?:const|static)\s+(?:mut\s+)?([A-Z][A-Z0-9_]*)\b/) { |(n)| names << n }
219
+ code_only.scan(/"([A-Z][A-Z0-9_]{2,})"/) { |(n)| names << n }
220
+ names
221
+ end
222
+
223
+ def text_violations
224
+ @comments.flat_map do |comment|
225
+ next [] if comment.text.match?(TMPL_MARKER)
226
+
227
+ caps_violations(comment) + constant_violations(comment) + unknown_violations(comment) +
228
+ bold_violations(comment) + history_violations(comment) + length_violations(comment)
229
+ end
230
+ end
231
+
232
+ def mask(comment)
233
+ text = comment.text
234
+ head = ""
235
+ if @open_spans[comment.line]
236
+ lead = text[MARKER].to_s.length
237
+ close = text.index("`", lead)
238
+ cut = close ? close + 1 : text.length
239
+ head = text[0, lead] + (FILLER * (cut - lead))
240
+ text = text[cut..]
241
+ end
242
+ body = text.gsub(CommentStyle::PROTECTED_SPANS) { |span| quoted_heading?(span) ? span : FILLER * span.length }
243
+ open = body.rindex("`")
244
+ body = body[0, open] + (FILLER * (body.length - open)) if open
245
+ head + body
246
+ end
247
+
248
+ def quoted_heading?(span)
249
+ return false unless span.start_with?('"') && !span.match?(CommentStyle::SQL_PHRASES)
250
+
251
+ capitals = span.scan(/\b[A-Z]{2,}\b/).size
252
+ capitals >= 3 || (capitals == 2 && !span.match?(/[a-z]/))
253
+ end
254
+
255
+ def caps_words(comment)
256
+ masked = mask(comment)
257
+ matches = []
258
+ masked.scan(CommentStyle::CAPS_WORD) { matches << Regexp.last_match }
259
+ sql = sql_keywords(comment, matches.map { |m| m[0] })
260
+ found = matches.filter_map do |match|
261
+ word = match[0]
262
+ next if word != "A" && (keep_capitals?(word) || sql.include?(word) || unknown?(word))
263
+
264
+ [match.begin(0), word, recase(word, sentence_start?(comment, masked, match.begin(0)))]
265
+ end
266
+ (drop_stray_articles(found) + possessives(masked)).sort_by(&:first)
267
+ end
268
+
269
+ def caps_violations(comment)
270
+ caps_words(comment).map do |_, word, replacement|
271
+ Violation.new(path, comment.line, "all_caps", "#{word} -> #{replacement}")
272
+ end
273
+ end
274
+
275
+ def constant_violations(comment)
276
+ words = mask(comment).scan(CommentStyle::CAPS_WORD).uniq
277
+ words.select { |w| @constants.include?(w) && !CommentStyle::ACRONYMS.include?(w) }
278
+ .map { |w| Violation.new(path, comment.line, "bare_constant", w) }
279
+ end
280
+
281
+ def unknown_violations(comment)
282
+ words = mask(comment).scan(CommentStyle::CAPS_WORD).uniq - ["A"]
283
+ words.reject { |w| keep_capitals?(w) }.select { |w| unknown?(w) }
284
+ .map { |w| Violation.new(path, comment.line, "unknown_capitals", w) }
285
+ end
286
+
287
+ def bold_violations(comment)
288
+ heading = comment.text[BOLD_HEADING, 2]
289
+ return [] unless heading && heading.split.size > MAX_HEADING_WORDS
290
+
291
+ [Violation.new(path, comment.line, "long_bold", "#{heading.split.size} words")]
292
+ end
293
+
294
+ def history_violations(comment)
295
+ masked = comment.text.gsub(/`[^`]*`/) { |span| " " * span.length }
296
+ pattern = CommentStyle::HISTORY.find { |candidate| masked.match?(candidate) }
297
+ return [] unless pattern
298
+
299
+ [Violation.new(path, comment.line, "design_history", %("#{masked[pattern]}"))]
300
+ end
301
+
302
+ def length_violations(comment)
303
+ length = @lines[comment.line - 1].to_s.chomp.length
304
+ return [] if length <= MAX_LINE
305
+
306
+ [Violation.new(path, comment.line, "long_line", "#{length} characters")]
307
+ end
308
+
309
+ def sql_keywords(comment, words)
310
+ bare = words.map { |w| w.sub(/'[A-Z]{1,2}\z/, "") } - ["A"]
311
+ strong = bare.select { |w| CommentStyle::SQL_STRONG.include?(w) }
312
+ weak = bare.select { |w| CommentStyle::SQL_WEAK.include?(w) }
313
+ database_file = path.match?(CommentStyle::SQL_PATH)
314
+ return bare.to_set if reads_as_sql?(bare, strong, weak, database_file)
315
+ return (strong + weak).to_set if comment.text.match?(CommentStyle::SQL_PHRASES)
316
+
317
+ database_file ? strong.to_set : Set.new
318
+ end
319
+
320
+ def reads_as_sql?(bare, strong, weak, database_file)
321
+ only_sql = bare.all? do |w|
322
+ [CommentStyle::SQL_STRONG, CommentStyle::SQL_WEAK, CommentStyle::SQL_NEUTRAL, CommentStyle::ACRONYMS].any? { |set| set.include?(w) }
323
+ end
324
+ return false unless only_sql
325
+
326
+ database_file ? (strong.any? || weak.any?) : (strong.any? && (strong + weak).size >= 2)
327
+ end
328
+
329
+ def possessives(masked)
330
+ found = []
331
+ masked.scan(/(?<=#{FILLER})'S\b/) { found << [Regexp.last_match.begin(0) + 1, "S", "s"] }
332
+ found
333
+ end
334
+
335
+ def drop_stray_articles(found)
336
+ found.each_with_index.reject do |(offset, word, _), index|
337
+ before = index.positive? ? found[index - 1] : nil
338
+ after = found[index + 1]
339
+ beside = (before && before[0] + before[1].length + 1 == offset && before[1] != "A") ||
340
+ (after && after[0] == offset + 2 && after[1] != "A")
341
+ word == "A" && !beside
342
+ end.map(&:first)
343
+ end
344
+
345
+ def recase(word, sentence_start)
346
+ bare, suffix = word.split("'", 2)
347
+ proper = proper_name(bare)
348
+ return [proper, suffix&.downcase].compact.join("'") if proper
349
+
350
+ sentence_start ? word.capitalize : word.downcase
351
+ end
352
+
353
+ def proper_name(bare)
354
+ CommentStyle::PROPER_NOUNS[bare] || RUST_PROPER_NOUNS[bare]
355
+ end
356
+
357
+ def unknown?(word)
358
+ bare = word.split("'", 2).first
359
+ !CommentStyle::Dictionary.english?(word) && !proper_name(bare)
360
+ end
361
+
362
+ def keep_capitals?(word)
363
+ bare = word.sub(/'[A-Z]{1,2}\z/, "")
364
+ return false if CommentStyle::PROPER_NOUNS.key?(bare) || RUST_PROPER_NOUNS.key?(bare)
365
+
366
+ CommentStyle::ACRONYMS.include?(bare) || RUST_ACRONYMS.include?(bare) ||
367
+ @constants.include?(bare) || bare.match?(/\A[IVX]+\z/)
368
+ end
369
+
370
+ def sentence_start?(comment, masked, offset)
371
+ before = masked[0, offset].sub(MARKER, "")
372
+ return before.match?(/[.!?]["')\]]*\s+\z/) if before.match?(/[[:alnum:]#{FILLER}]/)
373
+ return true if before.match?(/\A\s*(?:[-*•]|─+|\d+[.)])\s+\z/)
374
+
375
+ previous = comment.full_line ? @by_line[comment.line - 1] : nil
376
+ return true unless previous&.full_line
377
+
378
+ body = previous.text.sub(MARKER, "").strip
379
+ body.empty? || body.match?(/[.!?]["')\]]*\z/)
380
+ end
381
+
382
+ def fix_line(line, number, wanted)
383
+ comment = @by_line[number]
384
+ return line if comment.nil? || comment.text.match?(TMPL_MARKER)
385
+
386
+ text = wanted.include?("all_caps") ? fix_caps(comment) : comment.text
387
+ text = unbold(text) if wanted.include?("long_bold")
388
+ code = line.byteslice(0, comment.col)
389
+ return wrap(code, text) if wanted.include?("long_line") && comment.full_line
390
+
391
+ "#{code}#{text}#{"\n" if line.end_with?("\n")}"
392
+ end
393
+
394
+ def unbold(text)
395
+ heading = text[BOLD_HEADING, 2]
396
+ return text unless heading && heading.split.size > MAX_HEADING_WORDS
397
+
398
+ text.sub(BOLD_HEADING) { "#{Regexp.last_match(1)}#{heading}" }
399
+ end
400
+
401
+ def fix_caps(comment)
402
+ text = comment.text.dup
403
+ found = caps_words(comment)
404
+ found.reverse_each { |offset, word, replacement| text[offset, word.length] = replacement }
405
+ first, last = heading_run(comment, found)
406
+ return text unless first
407
+
408
+ text.insert(last, "**").insert(first, "**")
409
+ end
410
+
411
+ def heading_run(comment, found)
412
+ masked = mask(comment)
413
+ return unless found.size >= 2 && paragraph_start?(comment, masked, found.first[0])
414
+
415
+ run = found.slice_when { |(a, word, _), (b, _, _)| !masked[(a + word.length)...b].match?(GAP) }.first
416
+ finish = run.last[0] + run.last[1].length
417
+ finish += 2 if masked[finish, 2] == "'s"
418
+ return unless run.size >= 2 && masked[finish..].match?(HEADING_END)
419
+ return if masked[run.first[0]...finish].split.size > MAX_HEADING_WORDS
420
+
421
+ [run.first[0], finish]
422
+ end
423
+
424
+ def paragraph_start?(comment, masked, offset)
425
+ return false if masked[0, offset].sub(MARKER, "").match?(/[[:alnum:]#{FILLER}]/)
426
+
427
+ above = comment.full_line ? @by_line[comment.line - 1] : nil
428
+ !above&.full_line || above.text.match?(%r{\A/{2,3}!?\s*\z})
429
+ end
430
+
431
+ # Splits one over-long prose comment line in two. Lines with no safe
432
+ # break point (a long URL, an unbroken code span) are left for a person.
433
+ def wrap(indent, text)
434
+ whole = "#{indent}#{text}\n"
435
+ return whole if whole.chomp.length <= MAX_LINE
436
+
437
+ lead = text[MARKER].to_s
438
+ cut = text[0, MAX_LINE - indent.length + 1].rindex(" ")
439
+ return whole if cut.nil? || cut <= lead.length || text[0, cut].count("`").odd?
440
+
441
+ "#{indent}#{text[0, cut].rstrip}\n#{wrap(indent, "#{lead}#{text[cut..].strip}")}"
442
+ end
443
+
444
+ # `pub` items with no `///` doc above, and files with no `//!` module
445
+ # doc at the top. Attribute lines between the doc and item are skipped.
446
+ def structure_violations
447
+ (test_tree? ? [] : missing_doc_violations) + missing_module_doc_violation
448
+ end
449
+
450
+ def test_tree?
451
+ path.split("/").include?("tests")
452
+ end
453
+
454
+ def missing_doc_violations
455
+ tracker = TestCfgTracker.new
456
+ violations = []
457
+ @lines.each_with_index do |line, index|
458
+ tracker.observe(line)
459
+ next if tracker.inside?
460
+
461
+ match = PUB_ITEM.match(line)
462
+ next unless match
463
+ next if match[1] == "fn" && SELF_EVIDENT_FNS.include?(match[2])
464
+
465
+ line_number = index + 1
466
+ next if doc_above?(line_number)
467
+
468
+ violations << Violation.new(path, line_number, "missing_doc", "#{match[1]} #{match[2]}")
469
+ end
470
+ violations
471
+ end
472
+
473
+ # Tracks whether the line just observed sits inside a `#[cfg(test)]`
474
+ # module, by brace depth.
475
+ class TestCfgTracker
476
+ TEST_CFG = /\A\s*#\[cfg\(test\)\]/
477
+
478
+ # Starts outside every module, at brace depth zero.
479
+ def initialize
480
+ @depth = 0
481
+ @test_depth = nil
482
+ end
483
+
484
+ # Updates the tracked brace depth with one more source line.
485
+ def observe(line)
486
+ @depth += line.count("{") - line.count("}")
487
+ @test_depth = @depth if line.match?(TEST_CFG)
488
+ @test_depth = nil if @test_depth && @depth < @test_depth
489
+ end
490
+
491
+ # Answers whether the line just observed sits inside a test module.
492
+ def inside?
493
+ !@test_depth.nil?
494
+ end
495
+ end
496
+
497
+ def doc_above?(line_number)
498
+ cursor = line_number - 1
499
+ cursor -= 1 while cursor.positive? && @lines[cursor - 1].to_s.match?(ATTR_LINE)
500
+ @lines[cursor - 1].to_s.match?(DOC_LINE)
501
+ end
502
+
503
+ def missing_module_doc_violation
504
+ return [] if test_tree?
505
+ return [] unless ["mod.rs", "lib.rs", "main.rs"].include?(File.basename(path))
506
+ return [] if @lines.first(3).any? { |l| l.match?(MOD_DOC_LINE) }
507
+
508
+ [Violation.new(path, 1, "missing_module_doc", File.basename(path))]
509
+ end
510
+ end
511
+
512
+ # Lints a set of paths and renders the result.
513
+ class Run
514
+ # @param paths [Array<String>] files or directories
515
+ # @param only [Array<String>, nil] categories to keep
516
+ def initialize(paths, only: nil)
517
+ @files = paths.flat_map { |p| File.directory?(p) ? rust_files(p) : [p] }.sort.uniq
518
+ @only = only
519
+ end
520
+
521
+ # Every violation across the whole file set, computed once.
522
+ #
523
+ # @return [Array<Violation>]
524
+ def violations
525
+ @violations ||= begin
526
+ found = @files.flat_map { |path| SourceFile.new(path).violations(only: @only) }
527
+ @only ? found.select { |v| @only.include?(v.category) } : found
528
+ end
529
+ end
530
+
531
+ # Rewrites every file's fixable categories in place.
532
+ #
533
+ # @return [Integer] number of files rewritten
534
+ def fix!
535
+ if wants_caps? && !CommentStyle::Dictionary.words
536
+ abort "fixing all_caps needs the word list CommentStyle::Dictionary uses"
537
+ end
538
+
539
+ @files.count do |path|
540
+ original = File.read(path)
541
+ output = SourceFile.new(path, original).fixed(only: @only)
542
+ next false if output == original
543
+
544
+ File.write(path, output)
545
+ true
546
+ end
547
+ end
548
+
549
+ # Renders the summary tables `--report` prints.
550
+ #
551
+ # @param top [Integer] rows to show in the per-file table
552
+ # @return [String]
553
+ def report(top: 20)
554
+ by_file = violations.group_by(&:path)
555
+ [
556
+ "#{violations.size} violations in #{by_file.size} of #{@files.size} files\n",
557
+ "By category:", *category_rows,
558
+ "\nBy directory (top #{top}):", *directory_rows.first(top),
559
+ "\nWorst #{top} files:", *file_rows(by_file, top)
560
+ ].join("\n")
561
+ end
562
+
563
+ private
564
+
565
+ def wants_caps?
566
+ @only.nil? || @only.include?("all_caps")
567
+ end
568
+
569
+ def rust_files(dir)
570
+ Dir[File.join(dir, "**", "*.rs")].reject { |p| p.include?("/target/") }
571
+ end
572
+
573
+ def category_rows
574
+ counts = violations.group_by(&:category).transform_values(&:size)
575
+ CATEGORIES.map do |name, description|
576
+ suffix = FIXABLE.include?(name) ? " [fixable]" : ""
577
+ " #{counts.fetch(name, 0).to_s.rjust(6)} #{name.ljust(24)} #{description}#{suffix}"
578
+ end
579
+ end
580
+
581
+ def directory_rows
582
+ violations.group_by { |v| v.path.split("/")[0, 3].join("/") }
583
+ .sort_by { |_, list| -list.size }
584
+ .map { |dir, list| " #{list.size.to_s.rjust(6)} #{dir}" }
585
+ end
586
+
587
+ def file_rows(by_file, top)
588
+ by_file.sort_by { |_, list| -list.size }.first(top).map do |file, list|
589
+ category, worst = list.group_by(&:category).max_by { |_, group| group.size }
590
+ " #{list.size.to_s.rjust(6)} #{file} (mostly #{category}: #{worst.size})"
591
+ end
592
+ end
593
+ end
594
+
595
+ # Builds the CLI's option parser, filling `options` as flags are seen.
596
+ def self.option_parser(options)
597
+ OptionParser.new do |opts|
598
+ opts.banner = "Usage: hecks check_rust_comments [--report|--check|--fix] [--only a,b] PATH..."
599
+ opts.on("--report", "summary tables (default)") { options[:mode] = :report }
600
+ opts.on("--check", "list every violation, exit 1 if any") { options[:mode] = :check }
601
+ opts.on("--fix", "rewrite fixable categories: #{FIXABLE.join(', ')}") { options[:mode] = :fix }
602
+ opts.on("--only LIST", Array, "limit to: #{CATEGORIES.keys.join(', ')}") { |list| options[:only] = list }
603
+ opts.on("--json", "machine-readable output") { options[:json] = true }
604
+ opts.on("--top N", Integer, "rows in the worst-files table") { |n| options[:top] = n }
605
+ end
606
+ end
607
+
608
+ # Prints `--report`, `--check`, or `--json` output for a finished run.
609
+ def self.render(run, options)
610
+ found = run.violations
611
+ if options[:json]
612
+ puts JSON.pretty_generate(found.map(&:to_h))
613
+ elsif options[:mode] == :check
614
+ found.each { |v| puts "#{v.path}:#{v.line}: [#{v.category}] #{v.message}" }
615
+ else
616
+ puts run.report(top: options[:top])
617
+ end
618
+ options[:mode] == :check && found.any? ? 1 : 0
619
+ end
620
+
621
+ # The CLI entry point: parses `argv` and runs the requested mode.
622
+ #
623
+ # Paths after a `--` are never read as flags, and a path that does not exist is refused.
624
+ def self.main(argv, **)
625
+ options = { mode: :report, top: 20 }
626
+ parser = option_parser(options)
627
+ paths = parser.parse(argv)
628
+ unknown = Array(options[:only]) - CATEGORIES.keys
629
+ abort "unknown categories: #{unknown.join(', ')}" unless unknown.empty?
630
+ abort parser.help if paths.empty?
631
+ missing = paths.reject { |path| File.exist?(path) }
632
+ abort "no such path: #{missing.join(', ')}" unless missing.empty?
633
+
634
+ run = Run.new(paths, only: options[:only])
635
+ case options[:mode]
636
+ when :fix then puts("rewrote #{run.fix!} files") || 0
637
+ else render(run, options)
638
+ end
639
+ end
640
+ end
641
+ end
642
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../tools"
5
+
6
+ module Hecks
7
+ module Tools
8
+ # Prints one generated, valid dispatch sequence for a domain, using the generator `hecks fuzz`
9
+ # drives.
10
+ #
11
+ # hecks generate_sequence <domain> [--seed N] [--steps N] [--adversarial fraction]
12
+ #
13
+ # `--adversarial` mutates the sequence as `hecks quality_control ask run` does; 0 (off) by
14
+ # default.
15
+ module SequenceScript
16
+ USAGE = "usage: hecks generate_sequence <domain> [--seed N] [--steps N] [--adversarial FRACTION]"
17
+
18
+ module_function
19
+
20
+ # Prints the sequence as a replayable script.
21
+ #
22
+ # @param argv [Array<String>] the domain, then the flags
23
+ # @param root [String] the checkout (unused: the domain is named by path)
24
+ # @return [Integer] 0
25
+ # @raise [SystemExit] with the usage line when an argument is missing or unknown
26
+ def main(argv, root: Tools::ROOT)
27
+ _ = root
28
+ require "hecks"
29
+ require "hecks/fuzzing"
30
+ args = argv.dup
31
+ domain = args.shift or abort USAGE
32
+ seed, steps, adversarial = flags(args)
33
+
34
+ generated = Hecks::Fuzzing::SequenceGenerator.generate(domain, seed: seed, steps: steps,
35
+ adversarial: adversarial)
36
+ puts JSON.pretty_generate(
37
+ name: "#{File.basename(domain)}-generated",
38
+ note: "generated by hecks generate_sequence — seed #{seed}, #{steps} steps requested (#{generated.length} produced)",
39
+ steps: generated
40
+ )
41
+ 0
42
+ end
43
+
44
+ # @param args [Array<String>] the flags, consumed
45
+ # @return [Array(Integer, Integer, Float)] the seed, the step count and the fraction
46
+ def flags(args)
47
+ seed = 1
48
+ steps = 30
49
+ adversarial = 0.0
50
+ until args.empty?
51
+ case args.shift
52
+ when "--seed" then seed = Integer(args.shift)
53
+ when "--steps" then steps = Integer(args.shift)
54
+ when "--adversarial" then adversarial = Float(args.shift)
55
+ else abort USAGE
56
+ end
57
+ end
58
+ [seed, steps, adversarial]
59
+ end
60
+ end
61
+ end
62
+ end