quality_gate 0.1.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 (62) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +14 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +571 -0
  5. data/config/37signals.yml +26 -0
  6. data/config/cops.yml +37 -0
  7. data/config/reek.yml +6 -0
  8. data/config/rubocop.yml +60 -0
  9. data/config/ruby.yml +8 -0
  10. data/docs/codex.md +31 -0
  11. data/docs/dogfood-log.md +47 -0
  12. data/docs/incidents.md +18 -0
  13. data/docs/releasing.md +46 -0
  14. data/exe/quality_gate +6 -0
  15. data/lib/generators/quality_gate/install/install_generator.rb +66 -0
  16. data/lib/generators/quality_gate/install/templates/agents_section.md.tt +15 -0
  17. data/lib/generators/quality_gate/install/templates/bullet.rb.tt +10 -0
  18. data/lib/generators/quality_gate/install/templates/claude_settings.json.tt +29 -0
  19. data/lib/generators/quality_gate/install/templates/hook_log_filesystem.rb.tt +76 -0
  20. data/lib/generators/quality_gate/install/templates/quality_gate.yml.tt +42 -0
  21. data/lib/generators/quality_gate/install/templates/quality_gate_fast.rb.tt +248 -0
  22. data/lib/generators/quality_gate/install/templates/quality_gate_verify_stop.rb.tt +466 -0
  23. data/lib/generators/quality_gate/install/templates/rubocop.yml.tt +1 -0
  24. data/lib/generators/quality_gate/install/templates/ruby_agents_section.md.tt +15 -0
  25. data/lib/generators/quality_gate/install/templates/ruby_quality_gate.yml.tt +40 -0
  26. data/lib/generators/quality_gate/install/templates/ruby_rubocop.yml.tt +1 -0
  27. data/lib/generators/quality_gate/install/templates/ruby_simplecov.rb.tt +11 -0
  28. data/lib/generators/quality_gate/install/templates/simplecov.rb.tt +10 -0
  29. data/lib/generators/quality_gate/install/templates/strong_migrations.rb.tt +10 -0
  30. data/lib/quality_gate/adapter.rb +328 -0
  31. data/lib/quality_gate/adapters/brakeman.rb +125 -0
  32. data/lib/quality_gate/adapters/bundler_audit.rb +235 -0
  33. data/lib/quality_gate/adapters/reek.rb +108 -0
  34. data/lib/quality_gate/adapters/rubocop.rb +142 -0
  35. data/lib/quality_gate/adapters/simplecov.rb +103 -0
  36. data/lib/quality_gate/adapters/test_suite.rb +81 -0
  37. data/lib/quality_gate/adapters/undercover.rb +357 -0
  38. data/lib/quality_gate/cli.rb +451 -0
  39. data/lib/quality_gate/config.rb +290 -0
  40. data/lib/quality_gate/exit_code.rb +14 -0
  41. data/lib/quality_gate/finding.rb +50 -0
  42. data/lib/quality_gate/hook_log.rb +131 -0
  43. data/lib/quality_gate/init_command.rb +90 -0
  44. data/lib/quality_gate/installation.rb +1577 -0
  45. data/lib/quality_gate/installer.rb +104 -0
  46. data/lib/quality_gate/railtie.rb +16 -0
  47. data/lib/quality_gate/reporters/field_sanitizer.rb +42 -0
  48. data/lib/quality_gate/reporters/json.rb +59 -0
  49. data/lib/quality_gate/reporters/text.rb +45 -0
  50. data/lib/quality_gate/rubocop.rb +34 -0
  51. data/lib/quality_gate/ruby_profile.rb +170 -0
  52. data/lib/quality_gate/runner.rb +150 -0
  53. data/lib/quality_gate/version.rb +5 -0
  54. data/lib/quality_gate.rb +27 -0
  55. data/lib/rubocop/cop/quality_gate/association_default_block_value.rb +46 -0
  56. data/lib/rubocop/cop/quality_gate/broadcast_in_controller.rb +64 -0
  57. data/lib/rubocop/cop/quality_gate/controller_instance_variables.rb +47 -0
  58. data/lib/rubocop/cop/quality_gate/prefer_after_save_commit.rb +84 -0
  59. data/lib/rubocop/cop/quality_gate/private_only_concern.rb +77 -0
  60. data/llm.txt +13 -0
  61. data/sig/quality_gate.rbs +226 -0
  62. metadata +260 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b549f0b3f39f5a51bc4c1321a8b0771ae355f4ad1a8da733e6f5cf9c8209a54e
4
+ data.tar.gz: a0adfdf9647271909cac928c6056ee59114a5eefee5c8173c4ecf8816bbbb5ba
5
+ SHA512:
6
+ metadata.gz: 6f58b91553cbf70f2224831b4c21f8b2aa279a3e76dba5638445c2d11982363d4820a8d70b54b99fcc83d5b97e1d898292e472c0427b7f2e34c764b2979ab796
7
+ data.tar.gz: eb7bbe5a088b832b60f28751f6dfd1009a4d6ed1ce291e42c27dcd2abbbb9ccc1310e4d45992bff589723aaf1dd37c47ff9763f1edb5599aace9fbc730621d0e
data/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ ## [0.1.0] - 2026-09-08
2
+
3
+ - Layered fast, verify, and audit gates with structured findings for Ruby projects.
4
+ - RuboCop, Reek, test-suite, coverage, and security-tool integrations.
5
+ - Rails installer and agent hooks with host-owned configuration support.
6
+ - Plain Ruby setup with Minitest/RSpec detection, command overrides, coverage wiring, preview, and a Ruby RuboCop preset.
7
+ - Opt-in Claude hooks and agent contracts for Ruby and Rails projects.
8
+ - Command help, structured JSON input errors, and per-check status, scope, and timing metadata.
9
+ - Selected-path validation, severity and multiline diagnostics, and complete failed-test output retained in local logs.
10
+ - Gradual adoption using existing tool configuration and explicit adapter selection.
11
+ - Four optional Rails convention cops and a 37signals RuboCop preset.
12
+ - Sandi Metz's four rules in the shipped config: method, class, and parameter budgets, plus a `QualityGate/ControllerInstanceVariables` cop for the one-object controller action.
13
+ - The gem lints itself with the config it ships, with pre-existing structural debt frozen in `.rubocop_todo.yml`.
14
+ - Local release preparation and generated LLM documentation index.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Lucian Ghinda
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,571 @@
1
+ # QualityGate
2
+
3
+ QualityGate gives Ruby projects one workflow for checking changes: quick feedback while editing, verification before finishing, and a separate security audit. Set up a plain Ruby project with `quality_gate init`, or a Rails application with the Rails generator.
4
+
5
+ The `fast` gate runs RuboCop. `verify` runs Reek, the test suite, and Undercover in order. For `audit`, Rails defaults run Brakeman followed by bundler-audit; the Ruby setup uses bundler-audit alone.
6
+
7
+ ## Quick start
8
+
9
+ Add QualityGate to your Gemfile using the [installation instructions](#installation). For a Ruby gem or application with an existing test suite, run:
10
+
11
+ ```sh
12
+ bundle install
13
+ bundle exec quality_gate init --profile ruby
14
+ bundle exec quality_gate fast
15
+ bundle exec quality_gate verify
16
+ ```
17
+
18
+ For Rails, replace the init command with `bin/rails generate quality_gate:install`.
19
+
20
+ Review the installer summary: existing configuration is preserved, and conflicts require manual integration. Agent hooks are optional; add `--agents` to the setup command to install them. Use `--pretend` to preview filesystem changes. See [Ruby setup](#plain-ruby-and-other-test-runners) for test-runner selection and command overrides.
21
+
22
+ The same commands work from your terminal and CI. QualityGate runs existing tools, collects their findings, and distinguishes code findings from tools that could not complete. A clean result only describes the configured checks and their scope.
23
+
24
+ ## Commands
25
+
26
+ Run from the project root. Discover commands and scope without running checks:
27
+
28
+ ```sh
29
+ bundle exec quality_gate --help
30
+ bundle exec quality_gate verify --help
31
+ ```
32
+
33
+ Run the installed `quality_gate` executable through Bundler:
34
+
35
+ ```sh
36
+ bundle exec quality_gate version
37
+ bundle exec quality_gate fast
38
+ bundle exec quality_gate verify
39
+ bundle exec quality_gate audit
40
+ ```
41
+
42
+ The built-in fast path is meant for changed files:
43
+
44
+ ```sh
45
+ bundle exec quality_gate fast --files app/models/user.rb
46
+ ```
47
+
48
+ `verify` records coverage while it runs the full test suite, then checks changed code with Undercover. A clean `verify` or `audit` run prints this clean summary:
49
+
50
+ ```text
51
+ 0 findings, 0 tool failures
52
+ ```
53
+
54
+ Use `--files` to select paths for RuboCop and Reek; the first path follows the option and further paths follow it as separate arguments:
55
+
56
+ ```sh
57
+ bundle exec quality_gate fast --files lib/quality_gate.rb test/test_quality_gate.rb
58
+ ```
59
+
60
+ Paths must exist, including paths from the configuration file. Any missing path returns exit `2` before tools run. A bare `fast` scans the project; it does not discover changed files automatically.
61
+
62
+ | Adapter | Scope with `--files` |
63
+ | --- | --- |
64
+ | RuboCop, Reek | Selected files/directories, subject to the tool's exclusions |
65
+ | Test suite | Full configured suite |
66
+ | Undercover | Git changes against the comparison point |
67
+ | SimpleCov | Aggregate coverage summary |
68
+ | Brakeman | Whole application |
69
+ | bundler-audit | Gemfile.lock |
70
+
71
+ `verify --files app/models/user.rb` therefore narrows Reek, but still runs the full suite and checks Git changes. JSON includes each adapter's scope and status. When stderr is a terminal, progress names each tool before it starts; redirected output and hook invocations remain quiet except for diagnostics.
72
+
73
+ Choose text or JSON output with `--format text` or `--format json`:
74
+
75
+ ```sh
76
+ bundle exec quality_gate verify --format json
77
+ ```
78
+
79
+ The shorter form `quality_gate version` works when the installed executable is already on your `PATH`.
80
+
81
+ The repository config must also use exactly `text` or `json` for `format`; any other configured value is rejected as a configuration error before a gate runs.
82
+
83
+ ## Output contract
84
+
85
+ Gate text output includes tool, severity, location when available, rule, and message:
86
+
87
+ ```text
88
+ <tool> <severity> <file>:<line> <rule> <message>
89
+ ```
90
+
91
+ Multiline messages retain indented detail lines. Findings without a source location omit it rather than printing `:0`. The final gate output line is the summary:
92
+
93
+ ```text
94
+ <n> findings, <m> tool failures
95
+ ```
96
+
97
+ JSON output is one object plus a trailing newline. The top-level keys are `"findings"`, `"summary"`, and `"checks"`. Each finding object has exactly these keys:
98
+
99
+ - `"tool"`
100
+ - `"file"`
101
+ - `"line"`
102
+ - `"rule"`
103
+ - `"severity"`
104
+ - `"message"`
105
+
106
+ The summary object has exactly these keys:
107
+
108
+ - `"findings"`
109
+ - `"tool_failures"`
110
+ - `"failed_tools"`
111
+
112
+ Example:
113
+
114
+ ```json
115
+ {
116
+ "findings": [
117
+ {
118
+ "tool": "rubocop",
119
+ "file": "lib/example.rb",
120
+ "line": 7,
121
+ "rule": "Layout/LineLength",
122
+ "severity": "warning",
123
+ "message": "Line is too long"
124
+ },
125
+ {
126
+ "tool": "brakeman",
127
+ "file": "",
128
+ "line": 0,
129
+ "rule": "tool_failure",
130
+ "severity": "error",
131
+ "message": "timed out"
132
+ }
133
+ ],
134
+ "summary": {
135
+ "findings": 2,
136
+ "tool_failures": 1,
137
+ "failed_tools": ["brakeman"]
138
+ },
139
+ "checks": [
140
+ {"tool": "rubocop", "status": "findings", "scope": "project", "requested_files": [], "duration_ms": 120},
141
+ {"tool": "brakeman", "status": "tool_failure", "scope": "project", "requested_files": [], "duration_ms": 120000}
142
+ ]
143
+ }
144
+ ```
145
+
146
+ `checks` lists invoked adapters in execution order. Each entry has `tool`, `status`, `scope`, `requested_files`, and `duration_ms`. Status is `clean`, `findings`, `tool_failure`, or `skipped`; scope is `selected_files`, `project`, `test_suite`, `git_diff`, `coverage_summary`, `lockfile`, or `unknown`. `requested_files` describes selection input for RuboCop/Reek, not an inventory of files actually examined: tool exclusions still apply. Empty adapter lists produce `checks: []` and run no checks. Durations are observations, not performance guarantees.
147
+
148
+ When `--format json` is requested, input/configuration failures use the same report envelope with a `quality_gate` tool failure and exit `2`. Use an explicit format flag if malformed YAML might prevent loading your configured format. Inspect exit status as well as the report; a process that cannot start or write stdout cannot produce a JSON report. Help and version remain plain text.
149
+
150
+ Consumers should tolerate additive JSON fields. Existing finding and summary fields retain their meanings. An informational Undercover skip remains a finding and returns exit `1`; it is not proof that changed code was covered.
151
+
152
+ ## Configuration
153
+
154
+ QualityGate looks for `.quality_gate.yml` in the working repository. Values in that file override the shipped defaults for known top-level keys:
155
+
156
+ ```yaml
157
+ format: text
158
+ files:
159
+ - lib/quality_gate.rb
160
+ adapters:
161
+ fast:
162
+ - rubocop
163
+ verify:
164
+ - reek
165
+ - test_suite
166
+ - undercover
167
+ audit:
168
+ - brakeman
169
+ - bundler_audit
170
+ commands:
171
+ fast: {}
172
+ verify:
173
+ test_suite:
174
+ - bin/rails
175
+ - test
176
+ audit: {}
177
+ timeouts:
178
+ default: 120
179
+ rubocop: 10
180
+ test_suite: 120
181
+ undercover: 120
182
+ compare_point:
183
+ rubocop_config:
184
+ ```
185
+
186
+ Default adapters when no project configuration overrides them (the Rails setup uses these; Ruby init writes its own test command and omits Brakeman):
187
+
188
+ - `fast` => `rubocop`
189
+ - `verify` => `reek`, then `test_suite`, then `undercover`
190
+ - `audit` => `brakeman`, then `bundler_audit`
191
+
192
+ Default timeouts:
193
+
194
+ - `default` => `120`
195
+ - `rubocop` => `10`
196
+ - `test_suite` => `120`
197
+ - `undercover` => `120`
198
+
199
+ `adapters` lists adapter names per gate. The built-in registry knows `rubocop`, `reek`, `test_suite`, `undercover`, `simplecov`, `brakeman`, and `bundler_audit`. SimpleCov is registry-known but not a default adapter: the default verify adapters are Reek, the test suite, and Undercover, in that order. Unknown adapter names still become reported tool failures instead of being ignored.
200
+
201
+ ### Aggregate coverage budgets
202
+
203
+ To opt in to aggregate coverage enforcement, add `simplecov` to `adapters.verify` and configure at least one budget:
204
+
205
+ ```yaml
206
+ adapters:
207
+ verify:
208
+ - test_suite
209
+ - undercover
210
+ - simplecov
211
+ coverage:
212
+ minimum_line: 90
213
+ minimum_branch: 80
214
+ ```
215
+
216
+ `coverage.minimum_line` and `coverage.minimum_branch` are independent numeric percentage budgets in the inclusive range `0..100`. At least one is required when the `simplecov` adapter is enabled, and equality with the configured minimum passes. A valid `coverage` mapping alone is inert without `simplecov` in `adapters.verify`.
217
+
218
+ After the test suite runs, the adapter reads SimpleCov's `coverage/.last_run.json` summary without rerunning tests. A missing or unusable record is a tool failure. A branch budget without usable branch data is also a tool failure and names `enable_coverage :branch` as the required SimpleCov setup. Coverage below a configured budget produces a stable error finding: `line_coverage_below_minimum` for the line budget and `branch_coverage_below_minimum` for the branch budget.
219
+
220
+ `timeouts` sets the default adapter timeout in seconds and allows per-tool entries. The shipped RuboCop timeout is 10 seconds, while the test suite and Undercover each have an explicit 120-second timeout. Brakeman and bundler-audit inherit the 120-second default.
221
+
222
+ ### Projects with longer test suites
223
+
224
+ Measure a full test run with coverage enabled, then allow enough time for it in
225
+ `.quality_gate.yml`. For example, this gem uses a 240-second test-suite budget:
226
+
227
+ ```yaml
228
+ timeouts:
229
+ test_suite: 240
230
+ undercover: 120
231
+ ```
232
+
233
+ Raising only `timeouts.default` does not override the explicit `test_suite` or
234
+ `undercover` values. Adapters run sequentially, so the Claude Stop timeout must
235
+ exceed the combined budgets of the selected verify adapters, with extra time
236
+ for startup and reporting. For the example above, change the generated Stop
237
+ handler's `timeout` from `300` to `600` in `.claude/settings.json`. This also
238
+ leaves room for the optional SimpleCov adapter's inherited 120-second budget:
239
+
240
+ ```json
241
+ {
242
+ "type": "command",
243
+ "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/quality_gate_verify_stop.rb",
244
+ "args": [],
245
+ "timeout": 600
246
+ }
247
+ ```
248
+
249
+ Edit that handler inside the existing `hooks.Stop` entry, preserving other
250
+ hooks. This makes the settings developer-owned: future installer runs report
251
+ `Needs a person` when the file differs from known shipped templates. Review and
252
+ apply future settings updates manually while retaining the longer timeout.
253
+
254
+ An adapter timeout becomes `verify_unavailable` and allows the session to end.
255
+ Claude enforces the outer timeout itself and may terminate the hook before it
256
+ can write a log record. Check `log/quality_gate_hooks.jsonl` and run
257
+ `bundle exec quality_gate verify` manually after changing the budgets.
258
+
259
+ ### Configuration validation
260
+
261
+ `adapters` must be a mapping whose only permitted keys are `fast`, `verify`, and `audit`, and each layer value must be an array of non-empty strings. The mapping may specify any subset of those gates; unspecified gates keep the shipped defaults. A misspelled layer key such as `fasst` is rejected as a configuration error instead of silently falling back to a clean run.
262
+
263
+ `commands` follows the same three gate layers. Every command is an argv array of non-empty strings, never a shell command string. `commands.verify.test_suite` defaults to `["bin/rails", "test"]`.
264
+
265
+ `compare_point` may be `nil` or a non-empty String. Set it to a Git ref or commit when CI has too little history to find a common ancestor automatically.
266
+
267
+ `rubocop_config` is a known top-level key. It must be `nil` or a non-empty String. Use it when you want QualityGate to pass an explicit `rubocop_config` path to RuboCop.
268
+
269
+ Config selection works like this:
270
+
271
+ 1. If `rubocop_config` is set, QualityGate passes that path to RuboCop.
272
+ 2. Otherwise, if the host project already has a RuboCop config such as `.rubocop.yml`, QualityGate lets RuboCop use the host config.
273
+ 3. Otherwise, QualityGate passes the shipped `config/rubocop.yml` from this gem.
274
+
275
+ The shipped config loads `rubocop-rails`, `rubocop-performance`, and `rubocop-minitest`. It also sets the default metric budgets that the built-in fast gate enforces.
276
+
277
+ ### Sandi Metz rules
278
+
279
+ The shipped config enforces Sandi Metz's four rules.
280
+
281
+ | Rule | Enforced by | Budget |
282
+ | --- | --- | --- |
283
+ | A class stays under a hundred lines | `Metrics/ClassLength` | `Max: 100` |
284
+ | A method says one thing in five lines | `Metrics/MethodLength` | `Max: 5` |
285
+ | Pass no more than four parameters | `Metrics/ParameterLists` | `Max: 4` |
286
+ | A controller action instantiates one object | `QualityGate/ControllerInstanceVariables` | `Max: 1` |
287
+
288
+ The first three are ordinary RuboCop budgets, so their upstream options apply unchanged. The fourth is a cop this gem ships, and unlike the convention cops below it is enabled by default. It counts the distinct instance variables a public controller action assigns, including `||=`, `+=`, multiple assignment, and assignments made inside a block. Private and protected helpers, class methods, nested classes, and classes whose name does not end in `Controller` are all ignored. It reads only the action's own body, so an instance variable set by a `before_action` callback does not count against the budget. Configure it like any other cop:
289
+
290
+ ```yaml
291
+ QualityGate/ControllerInstanceVariables:
292
+ Max: 2
293
+ ```
294
+
295
+ Test suites read as linear setup, action, assertion, so the two length rules earn little there. Exempt them from your own `.rubocop.yml` rather than expecting the shipped config to do it. A relative path inside an inherited gem config resolves against that gem's directory, so a `test/**/*` written there would name this gem's tests and never yours:
296
+
297
+ ```yaml
298
+ Metrics/MethodLength:
299
+ Exclude:
300
+ - test/**/*
301
+ - spec/**/*
302
+ ```
303
+
304
+ ### Optional 37signals conventions
305
+
306
+ Besides the controller rule above, the shipped configuration registers four Rails convention cops, disabled by default pending validation on real applications. To enable the recommended set, inherit the optional preset in your application's `.rubocop.yml`:
307
+
308
+ ```yaml
309
+ inherit_gem:
310
+ quality_gate: config/37signals.yml
311
+ ```
312
+
313
+ | Cop | Reports | Correction |
314
+ | --- | --- | --- |
315
+ | `QualityGate/AssociationDefaultBlockValue` | Eager `Current` reads in `belongs_to` defaults, such as `default: Current.user` | Manual: use `default: -> { Current.user }` |
316
+ | `QualityGate/PreferAfterSaveCommit` | Literal `after_commit` callbacks for exactly create and update | Safe autocorrection to `after_save_commit` |
317
+ | `QualityGate/PrivateOnlyConcern` | Recognized concerns containing only private instance methods | Manual architectural review |
318
+ | `QualityGate/BroadcastInController` | Explicit Turbo and ActionCable broadcast calls in controller files | Manual architectural review |
319
+
320
+ The preset also enables existing `Rails/AttributeDefaultBlockValue`, `Rails/StrongParametersExpect` (Rails 8+), `Rails/Pluck`, and `Rails/SelectMap`. RuboCop Rails applies each cop's version and correction-safety restrictions. `Rails/SaveBang` remains a separate project policy.
321
+
322
+ Projects keeping their own configuration can load individual cops without inheriting the preset:
323
+
324
+ ```yaml
325
+ plugins:
326
+ - quality_gate/rubocop:
327
+ plugin_class_name: QualityGate::RuboCopPlugin
328
+
329
+ QualityGate/AssociationDefaultBlockValue:
330
+ Enabled: true
331
+ ```
332
+
333
+ The adapter respects host configurations; it does not inject this plugin into an existing configuration. Enable it as above, or inherit `config/rubocop.yml` and enable selected cops.
334
+
335
+ `PrivateOnlyConcern` recognizes `extend ActiveSupport::Concern`; configure `ConcernPaths: ['**/app/models/card/*.rb']` to recognize domain concern paths too. Modules with inclusion hooks, class behavior, public/protected methods, or unclear declarations are exempt. `BroadcastInController` defaults to `Include: ['**/app/controllers/**/*.rb']`; configure `AllowedMethods: ['broadcast_refresh_later']` or ordinary RuboCop `Exclude` patterns for intentional exceptions. It does not flag `render turbo_stream:` responses. These two convention cops identify code for review; they cannot determine the correct architecture.
336
+
337
+ Configuration is read with safe YAML loading. Unknown top-level keys produce a warning on standard error and are ignored; they do not pollute standard output.
338
+
339
+ The CLI rejects missing selected paths before dispatch with exit `2`. Deleted-file hook events remain cheap skips. Tool exclusions can still exclude existing selected files; selection is not proof that every requested file was analyzed.
340
+
341
+ RuboCop severities are normalized into QualityGate severities:
342
+
343
+ - `info` and `refactor` => `info`
344
+ - `convention` and `warning` => `warning`
345
+ - `error` and `fatal` => `error`
346
+
347
+ Syntax errors in Ruby files are reported as normal findings with severity `error`; they are not downgraded into tool failures.
348
+
349
+ ### Verify behavior
350
+
351
+ The default verify adapters always run in order: Reek reports code smells, the test suite runs with `COVERAGE=1`, then Undercover reads `coverage/coverage.json`. A failing test creates a `test_failure` finding but does not stop Undercover, so test failures and uncovered changed regions appear together. The finding retains the last 20 output lines and points to a unique full-output log under `log/quality_gate/`. Clean test runs create no log. If saving fails, the test failure still includes its tail and a log-write diagnostic. Timeout failures do not guarantee a complete test log. Keep `log/` out of version control and manage retained logs with your project's usual log retention policy.
352
+
353
+ Reek uses an existing host `.reek.yml` when present and otherwise uses the shipped configuration. It reports smells as warnings and respects `--files`; without selected files it scans the project. Missing selected files are rejected by the CLI before any adapter runs.
354
+
355
+ Undercover compares against the configured `compare_point` when present. Otherwise it uses the merge base of `HEAD` and the detected default branch: `refs/remotes/origin/HEAD` when available, then local `main`, then local `master`. If history is shallow, the default branch is missing, there is no earlier commit, or a detached checkout has no shared ancestor, verify reports an informational `undercover_skipped` finding with the reason instead of silently passing.
356
+
357
+ Each uncovered region is a warning that names its file, first line, full line range, node, uncovered lines, and uncovered branch context. If the coverage record is absent, verify reports a tool failure explaining that the SimpleCov wiring is missing. Install it with `quality_gate init --profile ruby` or the Rails generator. The test-suite and Undercover adapters ignore `--files`: the suite and the Git change define their scope.
358
+
359
+ ### Security audit behavior
360
+
361
+ The security adapters deliberately ignore `--files`. Brakeman scans the whole application because its data-flow analysis crosses file boundaries. bundler-audit always checks `Gemfile.lock`, and dependency advisories use that file with line `0` rather than inventing a source location.
362
+
363
+ bundler-audit tries to update its advisory database first. If that update fails and a real, usable local database exists, QualityGate scans the cached database and writes exactly one fallback warning to standard error. If no usable advisory database exists, bundler-audit returns a tool failure and exit code `2`; an unchecked dependency audit is never reported as clean.
364
+
365
+ ### Coverage template
366
+
367
+ QualityGate packages coverage templates for Rails/Minitest and plain Ruby. Both setup paths render coverage into a marked block after the Ruby source prologue and before host application code is required.
368
+
369
+ The host-facing coverage templates are Undercover-only. They activate only when `ENV["COVERAGE"] == "1"`, load SimpleCov and the Undercover formatter, start branch coverage, and write `coverage/coverage.json`. The Rails template filters `test`; the Ruby template filters both `test` and `spec`. HTML output is only part of this gem's self-dogfood setup and is not configured for generated hosts.
370
+
371
+ ## Exit codes
372
+
373
+ - `0` — clean gate or completed setup
374
+ - `1` — at least one finding and no tool failures, or setup conflicts requiring manual integration
375
+ - `2` — invalid input, configuration error, unknown adapter, or another tool failure
376
+
377
+ For the built-in RuboCop adapter, findings include rule names such as `Lint/Syntax` or `Metrics/MethodLength`. Invalid explicit RuboCop config paths or other RuboCop startup failures are reported as one `tool_failure` finding for `rubocop` and return exit code `2`.
378
+
379
+ ## Dependency range
380
+
381
+ QualityGate currently supports these runtime dependency ranges:
382
+
383
+ - `rubocop ~> 1.90`
384
+ - `reek ~> 6.5`
385
+ - `rubocop-rails ~> 2.37`
386
+ - `rubocop-performance ~> 1.27`
387
+ - `rubocop-minitest ~> 0.40`
388
+ - `brakeman ~> 8.0`
389
+ - `bullet ~> 8.2.0`
390
+ - `bundler-audit ~> 0.9.3`
391
+ - `simplecov ~> 1.1.1`
392
+ - `strong_migrations ~> 2.5.2`
393
+ - `undercover ~> 0.8.5`
394
+
395
+ Those are runtime dependencies, not optional extras. If your application pins older or incompatible RuboCop plugins, Bundler may report a dependency conflict when you add QualityGate.
396
+
397
+ Undercover uses Rugged for Git access. When Bundler cannot use a platform-specific Rugged gem, a clean source build may require CMake and libgit2 build tooling on the host.
398
+
399
+ ## Adopting QualityGate in an existing project
400
+
401
+ Start with your current `.rubocop.yml` and `.reek.yml`; QualityGate uses host configuration when present. Run `fast`, inspect the output, and fix configuration problems before adding more checks. The installer never silently replaces custom policy. To adopt the shipped rules later, merge this into your RuboCop configuration deliberately:
402
+
403
+ ```yaml
404
+ inherit_gem:
405
+ quality_gate: config/rubocop.yml
406
+ ```
407
+
408
+ The shipped five-line method and hundred-line class budgets are strict. Review findings and use ordinary tool configuration for intentional exceptions or an existing baseline. Do not treat style compliance or coverage percentages as proof of application correctness.
409
+
410
+ Select adapters explicitly to introduce checks gradually. For example, start verification with tests alone, then add Reek and Undercover after coverage wiring and Git history are ready:
411
+
412
+ ```yaml
413
+ adapters:
414
+ verify:
415
+ - test_suite
416
+ ```
417
+
418
+ Omitted gate mappings keep defaults; an explicit empty list disables all checks for that gate. The former `gates` key had no effect and has been removed; delete it from existing configuration. It now produces the ordinary unknown-key warning. Use `adapters` to choose what runs.
419
+
420
+ ### Plain Ruby and other test runners
421
+
422
+ Run init from the root of an existing Ruby project:
423
+
424
+ ```sh
425
+ bundle exec quality_gate init --profile ruby
426
+ bundle exec quality_gate fast
427
+ bundle exec quality_gate verify
428
+ bundle exec quality_gate audit
429
+ ```
430
+
431
+ The Ruby profile installs `.quality_gate.yml`, a `.rubocop.yml` inheriting `quality_gate: config/ruby.yml`, and a marked coverage block before application code in the selected helper. Rails cops and the controller instance-variable rule are disabled in this preset. It preserves existing RuboCop and Reek configuration, and creates no Rails initializers. All detector gems remain runtime dependencies, including Rails-oriented tools; this setup does not change the dependency footprint.
432
+
433
+ Init detects `test/test_helper.rb` for Minitest or `spec/spec_helper.rb` for RSpec. If both exist, select a framework explicitly. The default commands are `bundle exec rake test` and `bundle exec rspec`; your project must already provide the selected test runner and task. Use a command override for a different arrangement:
434
+
435
+ ```sh
436
+ bundle exec quality_gate init --test-framework rspec
437
+ bundle exec quality_gate init --test-helper test/helper.rb --test-command 'bundle exec ruby -Itest test/all_test.rb'
438
+ bundle exec quality_gate init --pretend
439
+ bundle exec quality_gate init --agents
440
+ bundle exec quality_gate init --help
441
+ ```
442
+
443
+ `--profile ruby` is the default. Helper paths are relative to the project root. A custom helper defaults to Minitest unless `--test-framework` is supplied. Command overrides use shell-style quoting to produce an argv array; shell operators and expansions are not executed. Invalid options, ambiguous detection, and missing helpers fail before writing files. `--skip-coverage` allows setup without a helper and omits Undercover from the generated verify configuration. It does not rewrite an existing configuration file.
444
+
445
+ Setup reports written, unchanged, skipped, and conflicting files. It never replaces custom configuration automatically; merge the displayed template where needed. Repeating setup with unchanged inputs is idempotent. Init uses a text summary; `--format json` applies to gate commands.
446
+
447
+ Coverage starts only with `COVERAGE=1`, which the test-suite adapter supplies, and filters both `test` and `spec`. Undercover requires a Git comparison point; set `compare_point` in shallow CI checkouts or fetch the base branch/history. Agent hooks and contracts are installed only with `--agents`.
448
+
449
+ ## Installation
450
+
451
+ QualityGate has not been published to RubyGems yet. Until it is released, install it from GitHub with Bundler:
452
+
453
+ ```ruby
454
+ gem "quality_gate", github: "lucianghinda/quality_gate"
455
+ ```
456
+
457
+ Then install for Ruby or Rails:
458
+
459
+ ```sh
460
+ bundle install
461
+ bundle exec quality_gate init --profile ruby
462
+ ```
463
+
464
+ ```sh
465
+ bin/rails generate quality_gate:install
466
+ ```
467
+
468
+ The Rails generator manages these host artifacts:
469
+
470
+ - `.quality_gate.yml`
471
+ - `.rubocop.yml`
472
+ - `config/initializers/bullet.rb`
473
+ - `config/initializers/strong_migrations.rb`
474
+ - a marked coverage block in `test/test_helper.rb`, before executable host code
475
+
476
+ With `--agents`, it additionally manages:
477
+
478
+ - `.claude/hooks/quality_gate_fast.rb`
479
+ - `.claude/hooks/quality_gate_verify_stop.rb`
480
+ - `.claude/settings.json`
481
+ - one marker-owned Quality Gate contract section in `CLAUDE.md`
482
+ - one marker-owned Quality Gate contract section in `AGENTS.md`
483
+
484
+ Installation is idempotent. A missing file is written, while a byte-identical file is left untouched. For wholly owned files such as `.quality_gate.yml`, `.rubocop.yml`, both files under `.claude/hooks/`, and `.claude/settings.json`, if an existing file has different content the generator never overwrites or merges it unless it is the exact known settings snapshot described next: otherwise, the installer prints the current template for manual use, marks the conflict as needing a person, and continues with the remaining artifacts. The sole upgrade exception is `.claude/settings.json` whose bytes exactly match the previously shipped PostToolUse-only template; rerunning the installer safely replaces that known snapshot with the current PostToolUse-and-Stop template. Any customized settings file remains a manual conflict and is not overwritten.
485
+
486
+ The generated Strong Migrations initializer begins with the exact provenance line `# Generated by Quality Gate.`. On its first install, QualityGate records the newest existing migration as the baseline, or records that no migration exists yet. On later runs, that provenance makes the recorded baseline take precedence, so newer migrations leave the generated initializer unchanged. An unmarked initializer is always developer-owned and therefore follows the conflict/manual policy; when it already contains `StrongMigrations.start_after`, the printed marked template preserves that established baseline instead of advancing it.
487
+
488
+ Use `--skip-initializers` when the host does not use Bullet or Strong Migrations. Use `--skip-coverage` to leave the Minitest helper alone. Use `--pretend` to preview the run without changing the filesystem; pending writes are reported as skipped.
489
+
490
+ ```sh
491
+ bin/rails generate quality_gate:install --skip-initializers
492
+ bin/rails generate quality_gate:install --skip-coverage
493
+ bin/rails generate quality_gate:install --pretend
494
+ ```
495
+
496
+ For coverage, the generator preserves a UTF-8 BOM and shebang, then leading blank lines, ordinary comments, all Ruby and Emacs directives, and complete `=begin`/`=end` comment blocks before the marked block. It inserts coverage immediately before the first executable host code. Inserted lines use the helper's existing LF or CRLF convention.
497
+
498
+ ### Claude Code agent hooks
499
+
500
+ Agent installation is opt-in. A default rerun neither updates nor removes previously installed agent files. To refresh an older installation, pass `--agents` explicitly and review any manual conflicts. Hooks provide feedback: a session ending does not guarantee verification passed. Run the CLI directly in CI and enforce its exit status.
501
+
502
+ Plain Ruby projects use `bundle exec quality_gate init --profile ruby --agents` to install or refresh the same integration. The Rails commands below apply to Rails applications.
503
+
504
+ For agent integration, run `bin/rails generate quality_gate:install --agents`. The generator writes `.claude/hooks/quality_gate_fast.rb` and `.claude/hooks/quality_gate_verify_stop.rb`, installs the exact Claude Code `PostToolUse` and `Stop` entries in `.claude/settings.json`, and manages one marker-owned Quality Gate contract block inside `CLAUDE.md` and `AGENTS.md`. The Stop command has a 300-second Claude Code timeout. The marker-owned contract sections are narrower than the wholly owned files: a single stale Quality Gate block is replaced in place, the surrounding bytes are preserved, and any unmatched or multiple marker cases fall back to manual installation. If a hook file is byte-identical but has lost its executable mode, reinstalling repairs the mode without rewriting the file. After `bundle install` or a gem upgrade, rerun `bin/rails generate quality_gate:install --agents` to reinstall the generated hooks and contract files.
505
+
506
+ The `PostToolUse` hook runs file-scoped `quality_gate fast` after Ruby edits. Findings exit 2 and return the gate's JSON report to Claude as feedback. If an attempted fast run is unavailable, the first attempt in that unavailable streak exits 2 with one stderr line naming `bundle exec quality_gate fast` and `log/quality_gate_hooks.jsonl`; the already-applied edit stands and is not rolled back. Repeated unavailable attempts exit 0 silently. Non-Ruby paths and deleted files remain cheap skips.
507
+
508
+ The `Stop` hook checks for Ruby working-tree edits and automatically runs `quality_gate verify` before Claude finishes. With no Ruby edits it skips without starting the verifier. A clean result from the same `session_id` is debounced when no Ruby edit was logged at or after that result. Findings exit 2 with the machine-readable JSON report only after the matching `verify_blocked` record is safely appended, so Claude receives the feedback and continues working. If prior hook history cannot be read safely or that record cannot be written, the hook suppresses the feedback and fails open so the retry cap cannot deadlock. Invalid verifier output, a tool failure, a verifier timeout, or a command exception also fails open as unavailable. After three consecutive blocked finish attempts in one session, the next attempt is capped and exits 0.
509
+
510
+ Hook activity is appended to `log/quality_gate_hooks.jsonl`. Stop records include `session_id` and use the outcomes `verify_skipped`, `verify_debounced`, `verify_clean`, `verify_blocked`, `verify_unavailable`, and `verify_cap`. The CLI warns on stderr when the last 20 valid records include either fast-hook `unavailable` or Stop-hook `verify_unavailable` outcomes. The warning does not change the gate's JSON output or exit code.
511
+
512
+ Codex does not consume the Claude Code hook automatically. Use the manual Codex workflow in `docs/codex.md` to run the same `fast`, `verify`, and `audit` commands yourself.
513
+
514
+ For the Rails generator, when `test/test_helper.rb` is missing, it warns that Minitest coverage wiring was skipped, installs the other files, and exits successfully. Plain Ruby init instead requires an existing helper or `--skip-coverage`. Every successful install or preview run ends by naming the next command:
515
+
516
+ ```sh
517
+ bundle exec quality_gate fast
518
+ ```
519
+
520
+ `bin/rails destroy quality_gate:install` is intentionally read-only. Automatic removal is not supported because coverage is embedded in a developer-owned helper and the agent contract files may contain developer-owned text around the managed markers; the command changes nothing and directs you to the manual removal steps below instead of printing an install summary or next command.
521
+
522
+ To undo a clean installation, delete the files that the generator wrote (`.quality_gate.yml`, `.rubocop.yml`, `config/initializers/bullet.rb`, `config/initializers/strong_migrations.rb`, `.claude/hooks/quality_gate_fast.rb`, `.claude/hooks/quality_gate_verify_stop.rb`, and `.claude/settings.json`). Then remove the complete block in `test/test_helper.rb` from `# quality_gate coverage — start` through `# quality_gate coverage — end`, including both marker lines, and remove the complete Quality Gate block from `CLAUDE.md` and `AGENTS.md` from `<!-- quality_gate agent contract — start -->` through `<!-- quality_gate agent contract — end -->`. Do not delete a file that predated the generator or contains developer changes outside the managed markers.
523
+
524
+ ## Acceptance evidence
525
+
526
+ The [incident catalogue](docs/incidents.md) maps executable acceptance fixtures to
527
+ four representative classes: N+1 queries, unsafe migrations, complexity creep,
528
+ and untested changed code. The [validation guide](docs/dogfood-log.md) provides
529
+ commands for reproducing the incident, coverage, and optional latency checks.
530
+
531
+ Fixture results do not guarantee compatibility or performance in every application.
532
+ Timeouts, unavailable tools, and skipped checks must be distinguished from clean
533
+ gates. Validate installation and configured tools in the target application before
534
+ relying on the results; no production adoption or review-time improvements are claimed.
535
+
536
+ ## Development
537
+
538
+ Clone the repository and install its dependencies:
539
+
540
+ ```sh
541
+ bin/setup
542
+ ```
543
+
544
+ Run the tests, lint checks, or complete default task:
545
+
546
+ ```sh
547
+ bundle exec rake test
548
+ bundle exec rubocop
549
+ bundle exec rake
550
+ ```
551
+
552
+ The gem is linted by the configuration it ships, so the Sandi Metz budgets apply to its own code. Violations that predate those budgets are frozen file by file in `.rubocop_todo.yml`, which keeps the gate green while forcing new code to meet the budgets. That file is a ratchet: it may only shrink. When you change a file listed there, bring it under the budget and delete its entry; never add one. Regenerate it only after such a burn-down:
553
+
554
+ ```sh
555
+ bundle exec rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit \
556
+ --no-offense-counts --no-auto-gen-timestamp
557
+ ```
558
+
559
+ `--auto-gen-only-exclude` keeps RuboCop from raising a `Max` to the worst value it finds, which would relax the budget everywhere at once instead of naming the files that owe work.
560
+
561
+ Prepare the release locally with:
562
+
563
+ ```sh
564
+ bin/prepare_release
565
+ ```
566
+
567
+ This runs tests and lint, generates [llm.txt](llm.txt), and builds the gem in `pkg/`. It does not publish or push. Maintainer tools live in `bin/`; the installed `quality_gate` command lives in `exe/quality_gate`. See [release preparation](docs/releasing.md) for the workflow and publication steps.
568
+
569
+ Bug reports and pull requests are welcome at [the QualityGate repository](https://github.com/lucianghinda/quality_gate). Contributors should follow the [code of conduct](https://github.com/lucianghinda/quality_gate/blob/main/CODE_OF_CONDUCT.md).
570
+
571
+ QualityGate is available under the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,26 @@
1
+ inherit_from: rubocop.yml
2
+
3
+ # Opt into this preset only after reviewing the conventions for your application.
4
+ QualityGate/AssociationDefaultBlockValue:
5
+ Enabled: true
6
+
7
+ QualityGate/PreferAfterSaveCommit:
8
+ Enabled: true
9
+
10
+ QualityGate/PrivateOnlyConcern:
11
+ Enabled: true
12
+
13
+ QualityGate/BroadcastInController:
14
+ Enabled: true
15
+
16
+ Rails/StrongParametersExpect:
17
+ Enabled: true
18
+
19
+ Rails/AttributeDefaultBlockValue:
20
+ Enabled: true
21
+
22
+ Rails/Pluck:
23
+ Enabled: true
24
+
25
+ Rails/SelectMap:
26
+ Enabled: true