quality_gate 0.2.2 → 0.3.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/README.md +292 -21
  4. data/docs/codex.md +37 -9
  5. data/docs/releasing.md +16 -4
  6. data/lib/generators/quality_gate/install/install_generator.rb +18 -3
  7. data/lib/generators/quality_gate/install/templates/agents_section.md.tt +13 -2
  8. data/lib/generators/quality_gate/install/templates/codex_fast.rb.tt +36 -0
  9. data/lib/generators/quality_gate/install/templates/codex_hooks.json.tt +1 -0
  10. data/lib/generators/quality_gate/install/templates/codex_verify_stop.rb.tt +28 -0
  11. data/lib/generators/quality_gate/install/templates/quality_gate.yml.tt +6 -0
  12. data/lib/generators/quality_gate/install/templates/quality_gate_fast.rb.tt +16 -3
  13. data/lib/generators/quality_gate/install/templates/quality_gate_workflow.yml.tt +28 -0
  14. data/lib/generators/quality_gate/install/templates/rails_quality_gate.yml.tt +45 -0
  15. data/lib/generators/quality_gate/install/templates/ruby_agents_section.md.tt +13 -2
  16. data/lib/generators/quality_gate/install/templates/ruby_quality_gate.yml.tt +5 -0
  17. data/lib/generators/quality_gate/install/templates/simplecov.rb.tt +1 -0
  18. data/lib/quality_gate/adapters/bundler_audit.rb +28 -3
  19. data/lib/quality_gate/adapters/database_consistency.rb +51 -0
  20. data/lib/quality_gate/adapters/database_consistency_report.rb +61 -0
  21. data/lib/quality_gate/adapters/database_consistency_report_item.rb +122 -0
  22. data/lib/quality_gate/adapters/debride.rb +67 -0
  23. data/lib/quality_gate/adapters/debride_report.rb +80 -0
  24. data/lib/quality_gate/adapters/herb.rb +303 -0
  25. data/lib/quality_gate/adapters/reek.rb +21 -2
  26. data/lib/quality_gate/adapters/rubocop.rb +23 -9
  27. data/lib/quality_gate/adapters/rubycritic.rb +74 -0
  28. data/lib/quality_gate/adapters/rubycritic_report.rb +112 -0
  29. data/lib/quality_gate/adapters/test_suite.rb +12 -1
  30. data/lib/quality_gate/baseline.rb +269 -0
  31. data/lib/quality_gate/baseline_run.rb +80 -0
  32. data/lib/quality_gate/cli.rb +129 -6
  33. data/lib/quality_gate/codex_fast_hook.rb +163 -0
  34. data/lib/quality_gate/codex_patch_files.rb +95 -0
  35. data/lib/quality_gate/codex_stop_hook.rb +102 -0
  36. data/lib/quality_gate/config.rb +20 -6
  37. data/lib/quality_gate/database_consistency_runner.rb +124 -0
  38. data/lib/quality_gate/doctor.rb +217 -0
  39. data/lib/quality_gate/doctor_bounded_file.rb +74 -0
  40. data/lib/quality_gate/doctor_command.rb +130 -0
  41. data/lib/quality_gate/doctor_coverage.rb +113 -0
  42. data/lib/quality_gate/doctor_git.rb +121 -0
  43. data/lib/quality_gate/doctor_hooks.rb +99 -0
  44. data/lib/quality_gate/doctor_launchers.rb +300 -0
  45. data/lib/quality_gate/doctor_path_lookup.rb +52 -0
  46. data/lib/quality_gate/doctor_report.rb +38 -0
  47. data/lib/quality_gate/init_command.rb +3 -1
  48. data/lib/quality_gate/installation.rb +72 -17
  49. data/lib/quality_gate/installer.rb +3 -0
  50. data/lib/quality_gate/reporters/doctor.rb +57 -0
  51. data/lib/quality_gate/reporters/json.rb +18 -1
  52. data/lib/quality_gate/reporters/markdown.rb +11 -1
  53. data/lib/quality_gate/reporters/text.rb +14 -6
  54. data/lib/quality_gate/ruby_profile.rb +121 -2
  55. data/lib/quality_gate/runner.rb +15 -17
  56. data/lib/quality_gate/version.rb +1 -1
  57. data/lib/quality_gate.rb +15 -0
  58. data/sig/quality_gate.rbs +117 -2
  59. metadata +30 -1
data/docs/releasing.md CHANGED
@@ -27,7 +27,9 @@ gem archive. `bin/console` opens an interactive session with the gem loaded.
27
27
 
28
28
  ## Release checklist
29
29
 
30
- 1. Update `lib/quality_gate/version.rb` and `CHANGELOG.md` for the intended release.
30
+ 1. Update `lib/quality_gate/version.rb` and add the matching `CHANGELOG.md` entry.
31
+ Treat these as one release record: keep the changelog heading, gemspec version,
32
+ and built artifact version synchronized.
31
33
  2. Run `bin/prepare_release` and review the generated documentation and package.
32
34
  Run `bundle exec quality_gate verify` to enforce the repository coverage budgets.
33
35
  On the initial commit, Undercover cannot compare against a parent commit, so
@@ -38,9 +40,19 @@ gem archive. `bin/console` opens an interactive session with the gem loaded.
38
40
  run normal verification, including diff coverage; no extra baseline commit is required.
39
41
  3. Confirm the archive includes runtime code, configuration, licensing, and public
40
42
  documentation, with no local paths, credentials, private logs, or internal plans.
41
- 4. Commit the reviewed release in the public repository and tag the version.
42
- 5. Publish the reviewed artifact with `gem push pkg/quality_gate-VERSION.gem`,
43
- replacing `VERSION` with the actual release version. RubyGems MFA is required.
43
+ 4. Before publishing, confirm the version matches in
44
+ `lib/quality_gate/version.rb`, the `CHANGELOG.md` heading, the gemspec, and
45
+ the built gem filename. Review the source commit and confirm it contains the
46
+ intended source and changelog entry. Separately confirm the gem artifact was
47
+ built from that reviewed commit.
48
+ 5. Commit the reviewed source, create the matching version tag, then push the
49
+ reviewed branch and tag to the public repository. Confirm the remote branch
50
+ and tag point to the reviewed commit before creating the GitHub release.
51
+ Publish the artifact with `gem push pkg/quality_gate-VERSION.gem`, replacing
52
+ `VERSION` with the actual release version; RubyGems MFA is required. Create
53
+ the GitHub release from the pushed tag, using the changelog entry as its notes.
54
+ 6. After publication, confirm the RubyGems release version and the GitHub
55
+ release title, tag, and notes match the version and changelog entry.
44
56
 
45
57
  The Rakefile also loads Bundler's gem tasks. Its `release` task can publish and
46
58
  push Git changes; use it only when those actions are intended.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../../quality_gate/installation"
4
+ require_relative "../../../quality_gate/ruby_profile"
4
5
 
5
6
  if defined?(Rails::Generators::Base)
6
7
  module QualityGate
@@ -32,15 +33,25 @@ if defined?(Rails::Generators::Base)
32
33
  class_option :skip_initializers, type: :boolean, default: false
33
34
  class_option :skip_coverage, type: :boolean, default: false
34
35
  class_option :agents, type: :boolean, default: false
36
+ class_option :codex, type: :boolean, default: false
37
+ class_option :test_framework, type: :string
38
+ class_option :test_helper, type: :string
39
+ class_option :test_command, type: :string
40
+ class_option :ci, type: :boolean, default: false
35
41
 
36
42
  # Rails discovers generator tasks from methods declared on this class.
37
43
  # Keep these forwarding methods so the shared module remains framework-neutral.
38
44
  # rubocop:disable Lint/UselessMethodDefinition
39
- def create_settings_file = super
45
+ def create_settings_file
46
+ profile if behavior == :invoke
47
+ super
48
+ end
49
+
40
50
  def create_rules_file = super
41
51
  def create_initializers = super
42
52
  def inject_coverage = super
43
53
  def create_agent_integration = super
54
+ def create_ci_workflow = super
44
55
  def print_summary = super
45
56
  # rubocop:enable Lint/UselessMethodDefinition
46
57
 
@@ -55,11 +66,15 @@ if defined?(Rails::Generators::Base)
55
66
  end
56
67
 
57
68
  def template_for(name)
58
- name
69
+ @profile ? @profile.template_for(name) : name
59
70
  end
60
71
 
61
72
  def test_helper_path
62
- "test/test_helper.rb"
73
+ profile.test_helper
74
+ end
75
+
76
+ def profile
77
+ @profile ||= RailsProfile.new(destination_root:, options:)
63
78
  end
64
79
  end
65
80
  end
@@ -1,7 +1,7 @@
1
1
  <!-- quality_gate agent contract — start -->
2
2
  ## Quality Gate agent contract
3
3
 
4
- Run `bundle exec quality_gate fast` after Ruby edits. Installed Claude Code hooks can run `bundle exec quality_gate verify` automatically at session end; other clients, including Codex, must run `bundle exec quality_gate verify` manually before you finish a change. Run `bundle exec quality_gate audit` before you merge security-sensitive work.
4
+ Run `bundle exec quality_gate fast` after Ruby edits. Run `bundle exec quality_gate verify` before you finish a change, and `bundle exec quality_gate audit` before you merge security-sensitive work.
5
5
 
6
6
  Fix findings before you continue editing.
7
7
 
@@ -11,5 +11,16 @@ Fix findings before you continue editing.
11
11
 
12
12
  The default fast path keeps feedback tight with a 10-second RuboCop timeout. Verify and audit keep the broader suite work on the 120-second default timeout.
13
13
 
14
- If the automatic hook becomes unavailable, inspect `log/quality_gate_hooks.jsonl`, run `bundle install`, and reinstall the hook with `bin/rails generate quality_gate:install --agents`.
14
+ <% if agent_option?(:agents) %>The Claude Code `PostToolUse` fast hook runs after native `Edit` and `Write` calls; Codex's optional patch fast hook runs after native `apply_patch` calls. Installed Claude Code hooks can run `bundle exec quality_gate verify` automatically at session end. If those hooks become unavailable, inspect `log/quality_gate_hooks.jsonl`, run `bundle install`, and reinstall them with `bin/rails generate quality_gate:install --agents`.
15
+ <% end %><% if agent_option?(:codex) %>
16
+ The optional Codex patch hook runs `quality_gate fast` after native `apply_patch` calls. It checks changed Ruby files and ERB files when Herb is configured; it does not run after shell writes or other tools. Findings appear as advisory context and never roll back the completed patch. If the gate or its tools are unavailable, Codex is asked to tell you and to run `bundle exec quality_gate fast` manually. Keep the direct manual gate and CI checks as enforcement evidence.
17
+
18
+ The final Codex Stop hook still runs the full `verify` gate, including changes made by shell commands. Findings request one repair continuation; Codex reruns verification on the resumed Stop, and remaining findings are reported while allowing the turn to finish. Unavailable checks also allow the turn to finish. This full gate can add substantial latency at every Stop. The patch hook's native timeout is 30 seconds; the shipped fast RuboCop timeout is 10 seconds, and a longer project fast budget can exceed Codex's hook limit and require manual verification.
19
+
20
+ Enable these project hooks only in a trusted project. In Codex, review `.codex/hooks/quality_gate_fast.rb`, `.codex/hooks/quality_gate_verify_stop.rb`, and both generated command definitions through `/hooks`, then explicitly trust them. Review both again after hook configuration changes. Quality Gate never changes Codex trust settings. Doctor can recognize installed files, but cannot confirm trust, activation, or live execution; Codex provides no execution history for Quality Gate to inspect.
21
+
22
+ To refresh the generated Codex hooks after a Quality Gate upgrade, run `bin/rails generate quality_gate:install --codex` and review both commands in `/hooks` again. If the project moves, update both absolute commands in `.codex/hooks.json` and review them again. The installer upgrades only the exact previous Stop-only configuration generated for this same project root. Resolve customized configuration or a snapshot from another location manually using the proposal printed by the installer. To remove the integration, remove both Quality Gate hook entries and scripts; remove `.codex/hooks.json` only when it contains solely generated Quality Gate configuration. Remove only the Codex-specific instructions from `AGENTS.md` when it also contains Claude guidance, or remove the Quality Gate section when Codex is its only client.
23
+ <% else %>
24
+ Other clients, including Codex, must run `bundle exec quality_gate verify` manually before you finish a change. Codex can also use `bundle exec quality_gate fast` after edits for file-scoped feedback.
25
+ <% end %>
15
26
  <!-- quality_gate agent contract — end -->
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "stringio"
6
+
7
+ begin
8
+ root = File.expand_path("../..", __dir__)
9
+ Dir.chdir(root)
10
+ gemfile = File.join(root, "Gemfile")
11
+ ENV["BUNDLE_GEMFILE"] = gemfile
12
+ raise LoadError, "project Gemfile is missing" unless File.file?(gemfile)
13
+
14
+ original_stdout, original_stderr = $stdout, $stderr
15
+ $stdout = $stderr = StringIO.new
16
+ begin
17
+ require "bundler"
18
+ Bundler.setup
19
+ ensure
20
+ $stdout, $stderr = original_stdout, original_stderr
21
+ end
22
+ require "quality_gate"
23
+ require "quality_gate/codex_fast_hook"
24
+ input = JSON.parse(STDIN.read)
25
+ puts JSON.generate(QualityGate::CodexFastHook.new(dir: root).call(input))
26
+ rescue StandardError, LoadError => error
27
+ message = "Quality Gate fast feedback unavailable (#{error.class}); " \
28
+ "run `bundle exec quality_gate fast` manually."
29
+ puts JSON.generate(
30
+ "systemMessage" => message,
31
+ "hookSpecificOutput" => {
32
+ "hookEventName" => "PostToolUse",
33
+ "additionalContext" => "Quality Gate fast feedback is unavailable. Run `bundle exec quality_gate fast` manually."
34
+ }
35
+ )
36
+ end
@@ -0,0 +1 @@
1
+ <%= JSON.pretty_generate("hooks" => {"PostToolUse" => [{"matcher" => "^apply_patch$", "hooks" => [{"type" => "command", "command" => Shellwords.join(["ruby", File.expand_path(File.join(destination_root, ".codex/hooks/quality_gate_fast.rb"))]), "timeout" => 30}]}], "Stop" => [{"hooks" => [{"type" => "command", "command" => Shellwords.join(["ruby", File.expand_path(File.join(destination_root, ".codex/hooks/quality_gate_verify_stop.rb"))]), "timeout" => 600}]}]}) %>
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "stringio"
6
+
7
+ begin
8
+ root = File.expand_path("../..", __dir__)
9
+ Dir.chdir(root)
10
+ gemfile = File.join(root, "Gemfile")
11
+ ENV["BUNDLE_GEMFILE"] = gemfile
12
+ raise LoadError, "project Gemfile is missing" unless File.file?(gemfile)
13
+
14
+ original_stdout, original_stderr = $stdout, $stderr
15
+ $stdout = $stderr = StringIO.new
16
+ begin
17
+ require "bundler"
18
+ Bundler.setup
19
+ ensure
20
+ $stdout, $stderr = original_stdout, original_stderr
21
+ end
22
+ require "quality_gate"
23
+ require "quality_gate/codex_stop_hook"
24
+ input = JSON.parse(STDIN.read)
25
+ puts JSON.generate(QualityGate::CodexStopHook.new(dir: root).call(input))
26
+ rescue StandardError, LoadError => error
27
+ puts JSON.generate("systemMessage" => "Quality Gate verification unavailable (#{error.class}); check the project bundle and run `bundle exec quality_gate verify --format json` manually.")
28
+ end
@@ -15,6 +15,8 @@
15
15
  # audit:
16
16
  # - brakeman
17
17
  # - bundler_audit
18
+ # deep:
19
+ # - rubycritic
18
20
  #
19
21
  # # Command overrides by gate. Empty mappings use adapter defaults.
20
22
  # commands:
@@ -24,6 +26,7 @@
24
26
  # - bin/rails
25
27
  # - test
26
28
  # audit: {}
29
+ # deep: {}
27
30
  #
28
31
  # # Command timeouts in seconds. Adapter-specific values override the default.
29
32
  # timeouts:
@@ -40,3 +43,6 @@
40
43
  #
41
44
  # # Explicit RuboCop config path; blank uses host config when present, otherwise the shipped config.
42
45
  # rubocop_config:
46
+
47
+ # # Optional warning/info baseline for the fast and verify gates; leave empty to disable.
48
+ # baseline: {}
@@ -4,6 +4,7 @@
4
4
  require "json"
5
5
  require "open3"
6
6
  require "time"
7
+ require "yaml"
7
8
 
8
9
  begin
9
10
  require "fiddle/import"
@@ -71,9 +72,21 @@ def outcome_for(exit_status, findings)
71
72
  "findings" if exit_status == 1 && findings.any?
72
73
  end
73
74
 
74
- def initial_outcome(file)
75
+ def herb_configured?(project_root)
76
+ path = File.join(project_root, ".quality_gate.yml")
77
+ return false unless File.file?(path)
78
+
79
+ document = YAML.safe_load(File.read(path))
80
+ gate_adapters = document.fetch("adapters", nil) if document.is_a?(Hash)
81
+ adapters = gate_adapters.fetch("fast", nil) if gate_adapters.is_a?(Hash)
82
+ adapters.is_a?(Array) && adapters.include?("herb")
83
+ rescue Psych::Exception, SystemCallError
84
+ false
85
+ end
86
+
87
+ def initial_outcome(file, project_root)
75
88
  return "no_file" unless file
76
- return "skipped" unless file.end_with?(".rb")
89
+ return "skipped" unless file.end_with?(".rb") || (file.end_with?(".erb") && herb_configured?(project_root))
77
90
  return "deleted" unless File.file?(file)
78
91
 
79
92
  "unavailable"
@@ -208,7 +221,7 @@ project_root = Dir.pwd unless project_root.is_a?(String) && !project_root.empty?
208
221
  tool_input = input["tool_input"]
209
222
  file = tool_input["file_path"] if tool_input.is_a?(Hash)
210
223
  file = nil unless file.is_a?(String) && !file.empty?
211
- outcome = initial_outcome(file)
224
+ outcome = initial_outcome(file, project_root)
212
225
  attempt_was_made = outcome == "unavailable"
213
226
  exit_code = 0
214
227
  feedback = nil
@@ -0,0 +1,28 @@
1
+ name: Quality Gate
2
+ # Customize the Ruby version, matrix, services, and environment for your application.
3
+
4
+ on:
5
+ push:
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ quality-gate:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v7
16
+ with:
17
+ fetch-depth: 0
18
+ persist-credentials: false
19
+ - uses: ruby/setup-ruby@v1
20
+ with:
21
+ ruby-version: "<%= RUBY_VERSION %>"
22
+ bundler-cache: true
23
+ - name: Fast checks
24
+ run: bundle exec quality_gate fast
25
+ - name: Verify checks
26
+ run: bundle exec quality_gate verify
27
+ - name: Security audit
28
+ run: bundle exec quality_gate audit
@@ -0,0 +1,45 @@
1
+ # # Output format: "text", "json", or "markdown".
2
+ # format: text
3
+ #
4
+ # # Optional file paths passed to RuboCop and Reek; empty means the project scope.
5
+ # files: []
6
+ #
7
+ # # Rails adapters run by each gate, in order.
8
+ # adapters:
9
+ # fast:
10
+ # - rubocop
11
+ # verify:
12
+ # - reek
13
+ # - test_suite
14
+ <% if @profile.coverage? %># - undercover
15
+ <% end %># audit:
16
+ # - brakeman
17
+ # - bundler_audit
18
+ #
19
+ # The installer-selected test command runs during verify with coverage enabled.
20
+ commands:
21
+ verify:
22
+ test_suite:
23
+ <% @profile.test_command.each do |argument| %> - <%= JSON.generate(argument) %>
24
+ <% end %>
25
+ #
26
+ # # Command timeouts in seconds.
27
+ # timeouts:
28
+ # default: 120
29
+ # rubocop: 10
30
+ # test_suite: 120
31
+ # undercover: 120
32
+ #
33
+ # # Aggregate SimpleCov budgets. Leave the entire mapping commented to disable coverage budgeting.
34
+ # coverage:
35
+ #
36
+ # # Git ref or commit for Undercover; blank selects the comparison point automatically.
37
+ # compare_point:
38
+ #
39
+ # # Explicit RuboCop config path; blank uses host config when present, otherwise the shipped config.
40
+ # rubocop_config:
41
+
42
+ # # Optional warning/info baseline for fast and verify; omitted means disabled.
43
+ # baseline:
44
+ # fast: config/quality_gate-fast-baseline.json
45
+ # verify: config/quality_gate-verify-baseline.json
@@ -1,7 +1,7 @@
1
1
  <!-- quality_gate agent contract — start -->
2
2
  ## Quality Gate agent contract
3
3
 
4
- Run `bundle exec quality_gate fast` after Ruby edits. Installed Claude Code hooks can run `bundle exec quality_gate verify` automatically at session end; other clients, including Codex, must run `bundle exec quality_gate verify` manually before you finish a change. Run `bundle exec quality_gate audit` before you merge security-sensitive work.
4
+ Run `bundle exec quality_gate fast` after Ruby edits. Run `bundle exec quality_gate verify` before you finish a change, and `bundle exec quality_gate audit` before you merge security-sensitive work.
5
5
 
6
6
  Fix findings before you continue editing.
7
7
 
@@ -11,5 +11,16 @@ Fix findings before you continue editing.
11
11
 
12
12
  The default fast path keeps feedback tight with a 10-second RuboCop timeout. Verify and audit keep the broader suite work on the 120-second default timeout.
13
13
 
14
- To reinstall the Ruby profile's agent integration, run `bundle exec quality_gate init --profile ruby --agents`.
14
+ <% if agent_option?(:agents) %>The Claude Code `PostToolUse` fast hook runs after native `Edit` and `Write` calls; Codex's optional patch fast hook runs after native `apply_patch` calls. Installed Claude Code hooks can run `bundle exec quality_gate verify` automatically at session end. If those hooks become unavailable, inspect `log/quality_gate_hooks.jsonl`, run `bundle install`, and reinstall them with `bundle exec quality_gate init --profile ruby --agents`.
15
+ <% end %><% if agent_option?(:codex) %>
16
+ The optional Codex patch hook runs `quality_gate fast` after native `apply_patch` calls. It checks changed Ruby files and ERB files when Herb is configured; it does not run after shell writes or other tools. Findings appear as advisory context and never roll back the completed patch. If the gate or its tools are unavailable, Codex is asked to tell you and to run `bundle exec quality_gate fast` manually. Keep the direct manual gate and CI checks as enforcement evidence.
17
+
18
+ The final Codex Stop hook still runs the full `verify` gate, including changes made by shell commands. Findings request one repair continuation; Codex reruns verification on the resumed Stop, and remaining findings are reported while allowing the turn to finish. Unavailable checks also allow the turn to finish. This full gate can add substantial latency at every Stop. The patch hook's native timeout is 30 seconds; the shipped fast RuboCop timeout is 10 seconds, and a longer project fast budget can exceed Codex's hook limit and require manual verification.
19
+
20
+ Enable these project hooks only in a trusted project. In Codex, review `.codex/hooks/quality_gate_fast.rb`, `.codex/hooks/quality_gate_verify_stop.rb`, and both generated command definitions through `/hooks`, then explicitly trust them. Review both again after hook configuration changes. Quality Gate never changes Codex trust settings. Doctor can recognize installed files, but cannot confirm trust, activation, or live execution; Codex provides no execution history for Quality Gate to inspect.
21
+
22
+ To refresh the generated Codex hooks after a Quality Gate upgrade, run `bundle exec quality_gate init --profile ruby --codex` and review both commands in `/hooks` again. If the project moves, update both absolute commands in `.codex/hooks.json` and review them again. The installer upgrades only the exact previous Stop-only configuration generated for this same project root. Resolve customized configuration or a snapshot from another location manually using the proposal printed by the installer. To remove the integration, remove both Quality Gate hook entries and scripts; remove `.codex/hooks.json` only when it contains solely generated Quality Gate configuration. Remove only the Codex-specific instructions from `AGENTS.md` when it also contains Claude guidance, or remove the Quality Gate section when Codex is its only client.
23
+ <% else %>
24
+ Other clients, including Codex, must run `bundle exec quality_gate verify` manually before you finish a change. Codex can also use `bundle exec quality_gate fast` after edits for file-scoped feedback.
25
+ <% end %>
15
26
  <!-- quality_gate agent contract — end -->
@@ -38,3 +38,8 @@ timeouts:
38
38
 
39
39
  # # Explicit RuboCop config path; blank uses host config when present, otherwise the shipped config.
40
40
  # rubocop_config:
41
+
42
+ # # Optional warning/info baseline for fast and verify; omitted means disabled.
43
+ # baseline:
44
+ # fast: config/quality_gate-fast-baseline.json
45
+ # verify: config/quality_gate-verify-baseline.json
@@ -6,5 +6,6 @@ if ENV['COVERAGE'] == '1'
6
6
  SimpleCov.start do
7
7
  enable_coverage :branch
8
8
  add_filter '/test/'
9
+ add_filter '/spec/'
9
10
  end
10
11
  end
@@ -9,8 +9,9 @@ module QualityGate
9
9
  # rubocop:disable Metrics/ClassLength
10
10
  class BundlerAudit < Adapter
11
11
  class InvalidReport < StandardError; end
12
+ class InvalidProcessStatus < StandardError; end
12
13
  class UnavailableDatabase < StandardError; end
13
- private_constant :InvalidReport, :UnavailableDatabase
14
+ private_constant :InvalidReport, :InvalidProcessStatus, :UnavailableDatabase
14
15
 
15
16
  LOCK_FILE = "Gemfile.lock".freeze # rubocop:disable Style/RedundantFreeze
16
17
  FALLBACK_WARNING = "bundler_audit: advisory database update failed; using cached database"
@@ -43,7 +44,9 @@ module QualityGate
43
44
  def updated_findings(tool, argv, deadline, timeout_seconds)
44
45
  stderr = +""
45
46
  stdout, stderr, status = capture(argv, remaining_timeout(deadline, timeout_seconds))
47
+ validate_normal_completion!(status)
46
48
  findings = parse_and_validate(stdout, tool)
49
+ validate_status_findings!(status, findings)
47
50
  return findings if local_database?
48
51
 
49
52
  unavailable_database_failure(tool, stderr)
@@ -51,6 +54,8 @@ module QualityGate
51
54
  return [failure_finding(tool, e, stderr)] if status&.success?
52
55
 
53
56
  fallback_findings(tool, e, stderr, deadline, timeout_seconds)
57
+ rescue InvalidProcessStatus => e
58
+ [failure_finding(tool, e, stderr)]
54
59
  rescue TimeoutError, SystemCallError, IOError, ThreadError => e
55
60
  fallback_findings(tool, e, stderr, deadline, timeout_seconds)
56
61
  end
@@ -69,10 +74,12 @@ module QualityGate
69
74
 
70
75
  def cached_findings(tool, deadline, timeout_seconds)
71
76
  cached_stderr = +""
72
- stdout, cached_stderr, = capture(cached_command, remaining_timeout(deadline, timeout_seconds))
77
+ stdout, cached_stderr, status = capture(cached_command, remaining_timeout(deadline, timeout_seconds))
78
+ validate_normal_completion!(status)
73
79
  findings = parse_and_validate(stdout, tool)
80
+ validate_status_findings!(status, findings)
74
81
  emit_fallback_warning(tool, findings, cached_stderr)
75
- rescue ParseError, TimeoutError, SystemCallError, IOError, ThreadError => e
82
+ rescue ParseError, InvalidProcessStatus, TimeoutError, SystemCallError, IOError, ThreadError => e
76
83
  [failure_finding(tool, e, cached_stderr)]
77
84
  end
78
85
 
@@ -85,6 +92,24 @@ module QualityGate
85
92
  raise TimeoutError, "timeout after #{timeout_seconds} seconds"
86
93
  end
87
94
 
95
+ def validate_normal_completion!(status)
96
+ return if status&.exited? && [0, 1].include?(status.exitstatus)
97
+
98
+ invalid_process_status!("process did not exit normally with status 0 or 1")
99
+ end
100
+
101
+ def validate_status_findings!(status, findings)
102
+ clean_status = status.exitstatus.zero?
103
+ return if clean_status == findings.empty?
104
+
105
+ expected = clean_status ? "no vulnerability findings" : "vulnerability findings"
106
+ invalid_process_status!("exit status #{status.exitstatus} conflicts with #{expected}")
107
+ end
108
+
109
+ def invalid_process_status!(reason)
110
+ raise InvalidProcessStatus, reason
111
+ end
112
+
88
113
  def unavailable_database_failure(tool, stderr)
89
114
  error = UnavailableDatabase.new("no usable advisory database is available")
90
115
  [failure_finding(tool, error, stderr)]
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+ require_relative "database_consistency_report"
5
+
6
+ module QualityGate
7
+ module Adapters
8
+ # Runs the optional project-wide database consistency bridge.
9
+ class DatabaseConsistency < Adapter
10
+ TOOL_NAME = "database_consistency"
11
+ BRIDGE_PATH = File.expand_path("../database_consistency_runner.rb", __dir__).freeze
12
+
13
+ def name = TOOL_NAME
14
+
15
+ def command
16
+ prefix = config.fetch(:commands).fetch(:audit).fetch(:database_consistency, nil)
17
+ return [RbConfig.ruby, BRIDGE_PATH] unless prefix
18
+
19
+ prefix + [BRIDGE_PATH]
20
+ end
21
+
22
+ def parse(stdout)
23
+ DatabaseConsistencyReport.new(stdout).findings
24
+ end
25
+
26
+ def call = capture_findings
27
+
28
+ private
29
+
30
+ def capture_findings
31
+ stderr = +""
32
+ execute(stderr).tap { validate_findings!(_1, expected_tool: name) }
33
+ rescue StandardError => e
34
+ [failure_finding(name, e, stderr)]
35
+ end
36
+
37
+ def execute(stderr)
38
+ stdout, captured_stderr, status = capture(validated_command, resolved_timeout(name))
39
+ stderr.replace(captured_stderr)
40
+ validate_process_status!(status)
41
+ parse(stdout)
42
+ end
43
+
44
+ def validate_process_status!(status)
45
+ return if status&.exited? && status.exitstatus.zero?
46
+
47
+ raise ParseError.new(tool: name, reason: "process did not exit normally with status 0")
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "rubygems"
5
+ require_relative "database_consistency_report_item"
6
+
7
+ module QualityGate
8
+ module Adapters
9
+ # Validates the bridge envelope and converts reports into findings.
10
+ class DatabaseConsistencyReport
11
+ VERSION = 1
12
+ VERSION_REQUIREMENT = Gem::Requirement.new("~> 3.0.14")
13
+
14
+ def initialize(json)
15
+ @envelope = JSON.parse(json)
16
+ rescue JSON::ParserError => e
17
+ raise ParseError.new(tool: "database_consistency", reason: e.message)
18
+ end
19
+
20
+ def findings
21
+ validate_envelope!
22
+ reports.filter_map { DatabaseConsistencyReportItem.new(_1).finding }
23
+ end
24
+
25
+ private
26
+
27
+ def validate_envelope!
28
+ invalid!("envelope must be an object") unless @envelope.is_a?(Hash)
29
+ validate_version!
30
+ validate_analyzer_version!
31
+ invalid!("reports must be an array") unless reports.is_a?(Array)
32
+ end
33
+
34
+ def validate_version!
35
+ value = @envelope.fetch("version", nil)
36
+ invalid!("version must be integer 1") unless value.instance_of?(Integer) && value == VERSION
37
+ end
38
+
39
+ def validate_analyzer_version!
40
+ value = @envelope.fetch("analyzer_version", nil)
41
+ return if supported_version?(value)
42
+
43
+ invalid!("analyzer_version must be a supported version")
44
+ end
45
+
46
+ def supported_version?(value)
47
+ value.is_a?(String) && VERSION_REQUIREMENT.satisfied_by?(Gem::Version.new(value))
48
+ rescue ArgumentError
49
+ false
50
+ end
51
+
52
+ def reports = @envelope.fetch("reports", nil)
53
+
54
+ def invalid!(reason)
55
+ raise ParseError.new(tool: "database_consistency", reason: reason)
56
+ end
57
+ end
58
+
59
+ private_constant :DatabaseConsistencyReport
60
+ end
61
+ end