Module: RailVerdict::Reuse
- Defined in:
- lib/rail_verdict/reuse.rb
Defined Under Namespace
Classes: Result
Constant Summary collapse
- REUSABLE =
"REUSABLE"- VERIFICATION_REQUIRED =
"VERIFICATION_REQUIRED"- INVALID =
"INVALID"- UNAVAILABLE =
"UNAVAILABLE"
Class Method Summary collapse
-
.evaluate(handoff_document:, current_repository_state:, current_environment:, current_contract: {}) ⇒ Object
One canonical evaluator — CLI/MCP/CI/Repair must delegate here.
Class Method Details
.evaluate(handoff_document:, current_repository_state:, current_environment:, current_contract: {}) ⇒ Object
One canonical evaluator — CLI/MCP/CI/Repair must delegate here.
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/rail_verdict/reuse.rb', line 20 def self.evaluate(handoff_document:, current_repository_state:, current_environment:, current_contract: {}) # 1. Validate handoff structurally unless handoff_document.is_a?(Hash) && handoff_document["handoff_id"] return Result.new(decision: INVALID, reasons: ["handoff_invalid"], handoff_valid: false, receipt_fresh: false) end stored = handoff_document["handoff_id"] payload = handoff_document.reject { |k, _| k == "handoff_id" } expected = Handoff.id_for(payload) unless stored == expected return Result.new(decision: INVALID, reasons: ["handoff_tampered"], handoff_valid: false, receipt_fresh: false) end errors = SchemaValidator.validate_handoff(handoff_document) unless errors.empty? return Result.new(decision: INVALID, reasons: ["handoff_invalid"], handoff_valid: false, receipt_fresh: false) end # Size bound already enforced in Handoff.build/parse, but check if JSON.generate(handoff_document).bytesize > Handoff::MAX_DOCUMENT_BYTES return Result.new(decision: INVALID, reasons: ["handoff_too_large"], handoff_valid: false, receipt_fresh: false) end receipt = handoff_document["receipt"] unless receipt && receipt["receipt_id"] return Result.new(decision: INVALID, reasons: ["handoff_invalid"], handoff_valid: false, receipt_fresh: false) end # 2. TOCTOU guard: observe identity_before, then evaluate, then re-observe is caller's responsibility # Here we check provided current state/env vs receipt binding # Freshness via Receipt.validate_freshness equivalent — compare repository_state + environment digests freshness = evaluate_freshness(receipt, current_repository_state, current_environment) unless freshness[:fresh] return Result.new(decision: VERIFICATION_REQUIRED, reasons: freshness[:reasons], handoff_valid: true, receipt_fresh: false) end # 3. Completeness / required evidence required = Array(current_contract["required_analyzers"] || current_contract[:required_analyzers]).map(&:to_s) evidence_analyzers = Array(handoff_document.dig("evidence_set", "analyzer_results")).map { |ar| ar["analyzer"].to_s } missing = required - evidence_analyzers unless missing.empty? return Result.new(decision: VERIFICATION_REQUIRED, reasons: ["required_analyzer_missing:#{missing.join(',')}"], handoff_valid: true, receipt_fresh: true) end # 4. Incomplete evidence cannot be reused as complete handoff_document.dig("evidence_set", "analyzer_results")&.each do |ar| if !%w[succeeded success].include?(ar["execution_status"]) return Result.new(decision: VERIFICATION_REQUIRED, reasons: ["evidence_incomplete:#{ar['analyzer']}"], handoff_valid: true, receipt_fresh: true) end end # 5. Per-analyzer predicates (whole-set: all must be reusable) # RuboCop: reusable if analyzer_versions match current environment for rubocop # RSpec/Minitest/SimpleCov: never reusable (fail-closed) — forces VERIFICATION_REQUIRED # bundler-audit: requires advisory DB equivalence — unobservable → VERIFICATION_REQUIRED non_reusable = [] required.each do |analyzer| case analyzer when "rspec", "minitest", "simplecov" non_reusable << "analyzer_not_reusable:#{analyzer}" when "bundler_audit", "bundler-audit" # If advisory DB revision not observable or not proven equal, fail-closed prov = handoff_document["evidence_provenance"] current_db = current_contract["advisory_db_revision"] || current_contract[:advisory_db_revision] handoff_db = prov && (prov["advisory_db_revision"] || prov[:advisory_db_revision]) if handoff_db.nil? || current_db.nil? || handoff_db.to_s != current_db.to_s non_reusable << "advisory_database_changed" end when "rubocop" # Check analyzer version equality receipt_versions = receipt.dig("environment", "analyzer_versions") || {} current_versions = current_environment.is_a?(Hash) ? (current_environment["analyzer_versions"] || current_environment[:analyzer_versions] || {}) : {} # Also check handoff provenance if receipt_versions["rubocop"].to_s != current_versions["rubocop"].to_s && !current_versions["rubocop"].nil? non_reusable << "analyzer_version_changed:rubocop" end end end unless non_reusable.empty? return Result.new(decision: VERIFICATION_REQUIRED, reasons: non_reusable.uniq, handoff_valid: true, receipt_fresh: true) end # Scope / policy checks (current_contract may include base, mode) if current_contract["verification_mode"] || current_contract[:verification_mode] handoff_mode = handoff_document["source_scope"] && handoff_document["source_scope"]["verification_mode"] current_mode = (current_contract["verification_mode"] || current_contract[:verification_mode]).to_s if handoff_mode && handoff_mode != current_mode return Result.new(decision: VERIFICATION_REQUIRED, reasons: ["scope_changed"], handoff_valid: true, receipt_fresh: true) end end if current_contract["changed_base"] || current_contract[:changed_base] handoff_base = handoff_document.dig("source_scope", "changed_base") current_base = (current_contract["changed_base"] || current_contract[:changed_base]).to_s if handoff_base && handoff_base != current_base return Result.new(decision: VERIFICATION_REQUIRED, reasons: ["base_changed"], handoff_valid: true, receipt_fresh: true) end end Result.new(decision: REUSABLE, reasons: [], handoff_valid: true, receipt_fresh: true) end |