Sdlc/Structural/DiscardedErrorCode: carve out predicate conditions and per-branch exception classes; state the fix #1353

Closed
opened 2026-10-25 21:22:40 +00:00 by jared · 2 comments
Owner

Problem

The cop Sdlc/Structural/DiscardedErrorCode flags a two-argument raise inside an if/case whose condition names code, error_code, status, or status_code.
It flags cases where no code exists to carry, and its message names no correction.

Found by the #1325 zero-false-positive audit (docs/plans/class-length-family/sdlc-cop-audit.md).

Observed

Counterexample 1: a boolean predicate has no code to carry. The only hit in plugins/ is plugins/os-adr/lib/adr/judge/claude_command.rb:32 (unless status.success?).

Counterexample 2: per-branch exception classes already encode the code.

class HttpErrors
  def check(response)
    code = response.code
    case code
    when 404 then raise NotFound, "missing page"
    when 401 then raise Unauthorized, "login needed"
    end
  end
end

The message (discarded_error_code.rb:15) names no correction.

Reproduce

n/a: matcher is plugins/os-sdlc/lib/os_sdlc/cops/structural/discarded_error_code/raise_site.rb:13-40; run the cop on the sample above.

Approach

Carve-outs go in the cop matcher, never in a config Exclude list or a test-path exclusion.
Skip when the discriminator appears only as the receiver of a ? predicate.
Skip case raises whose exception class differs per when.
The message names the correction, for example raise Error.new(msg, code: code).

Required work:

  1. Name the correction in the message — check: present code: in plugins/os-sdlc/lib/os_sdlc/cops/structural/discarded_error_code.rb

Acceptance criteria

  1. Given unless status.success? guarding a two-argument raise, when the cop runs, then it reports no offense.
  2. Given a case code whose when branches raise different classes, when the cop runs, then it reports no offense.
  3. Given an offense, when the message prints, then it names the correction.
  4. Given if status.code == 1 (or if status.failed? || code == 5) guarding a two-argument raise, when the cop runs, then it still reports the offense: the discriminator is read as a value, not only as the receiver of a ? predicate.
  5. Given a case code whose when branches all raise the same class, when the cop runs, then it still reports each raise.

Owner sketch

  • DiscardedErrorCode::RaiseSite (plugins/os-sdlc/lib/os_sdlc/cops/structural/discarded_error_code/raise_site.rb) owns the decision "does this raise discard the discriminator". It gains both carve-outs: a condition whose discriminator name appears only as the receiver of a ? predicate yields no discriminator; a case ancestor whose when bodies raise more than one distinct class is not a discarding site.
  • DiscardedErrorCode (discarded_error_code.rb) owns MESSAGE; it names the correction raise Error.new(msg, code: code).
  • Lesson plugins/os-sdlc/lint-lessons/sdlc-structural-discarded-error-code.yml: update the signal to state both carve-outs.
  • Out of scope: widening the cop (#1451).

Origin

  • Trigger: #1325 zero-false-positive audit of the Sdlc cops
  • Improvised this session: none
  • Chain: false positive ← matcher keys on condition names only ← DESIGN (plugins/os-sdlc/lib/os_sdlc/cops/structural/discarded_error_code.rb)
  • Root candidate: none found; searched DiscardedErrorCode
  • Where: plugins/os-sdlc/lib/os_sdlc/cops/structural/discarded_error_code/raise_site.rb
  • Session: eb98cdcd-6783-46a0-b7df-cf18a8fe2a56
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os--claude-worktrees-ticket-1325/eb98cdcd-6783-46a0-b7df-cf18a8fe2a56.jsonl
    VERDICT: CREATE
## Problem The cop `Sdlc/Structural/DiscardedErrorCode` flags a two-argument `raise` inside an `if`/`case` whose condition names `code`, `error_code`, `status`, or `status_code`. It flags cases where no code exists to carry, and its message names no correction. Found by the #1325 zero-false-positive audit (docs/plans/class-length-family/sdlc-cop-audit.md). ## Observed Counterexample 1: a boolean predicate has no code to carry. The only hit in plugins/ is `plugins/os-adr/lib/adr/judge/claude_command.rb:32` (`unless status.success?`). Counterexample 2: per-branch exception classes already encode the code. ```ruby class HttpErrors def check(response) code = response.code case code when 404 then raise NotFound, "missing page" when 401 then raise Unauthorized, "login needed" end end end ``` The message (`discarded_error_code.rb:15`) names no correction. ## Reproduce n/a: matcher is `plugins/os-sdlc/lib/os_sdlc/cops/structural/discarded_error_code/raise_site.rb:13-40`; run the cop on the sample above. ## Approach Carve-outs go in the cop matcher, never in a config Exclude list or a test-path exclusion. Skip when the discriminator appears only as the receiver of a `?` predicate. Skip `case` raises whose exception class differs per `when`. The message names the correction, for example `raise Error.new(msg, code: code)`. Required work: 1. Name the correction in the message — check: present `code:` in `plugins/os-sdlc/lib/os_sdlc/cops/structural/discarded_error_code.rb` ## Acceptance criteria 1. Given `unless status.success?` guarding a two-argument raise, when the cop runs, then it reports no offense. 2. Given a `case code` whose `when` branches raise different classes, when the cop runs, then it reports no offense. 3. Given an offense, when the message prints, then it names the correction. 4. Given `if status.code == 1` (or `if status.failed? || code == 5`) guarding a two-argument raise, when the cop runs, then it still reports the offense: the discriminator is read as a value, not only as the receiver of a `?` predicate. 5. Given a `case code` whose `when` branches all raise the same class, when the cop runs, then it still reports each raise. ## Owner sketch - `DiscardedErrorCode::RaiseSite` (`plugins/os-sdlc/lib/os_sdlc/cops/structural/discarded_error_code/raise_site.rb`) owns the decision "does this raise discard the discriminator". It gains both carve-outs: a condition whose discriminator name appears only as the receiver of a `?` predicate yields no discriminator; a `case` ancestor whose `when` bodies raise more than one distinct class is not a discarding site. - `DiscardedErrorCode` (`discarded_error_code.rb`) owns `MESSAGE`; it names the correction `raise Error.new(msg, code: code)`. - Lesson `plugins/os-sdlc/lint-lessons/sdlc-structural-discarded-error-code.yml`: update the signal to state both carve-outs. - Out of scope: widening the cop (#1451). ## Origin - Trigger: #1325 zero-false-positive audit of the Sdlc cops - Improvised this session: none - Chain: false positive ← matcher keys on condition names only ← DESIGN (plugins/os-sdlc/lib/os_sdlc/cops/structural/discarded_error_code.rb) - Root candidate: none found; searched DiscardedErrorCode - Where: plugins/os-sdlc/lib/os_sdlc/cops/structural/discarded_error_code/raise_site.rb - Session: eb98cdcd-6783-46a0-b7df-cf18a8fe2a56 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os--claude-worktrees-ticket-1325/eb98cdcd-6783-46a0-b7df-cf18a8fe2a56.jsonl VERDICT: CREATE
Author
Owner

Work started on branch ticket-1353 (os-sdlc implement, AFK unit).

Work started on branch ticket-1353 (os-sdlc implement, AFK unit).
Author
Owner

Resolution

Done: Sdlc/Structural/DiscardedErrorCode no longer reports a two-argument raise whose discriminator appears only as the receiver of a ? predicate (unless status.success?), or a raise inside a case whose when branches raise different classes. The message now names the correction: raise Error.new(msg, code: code). Lesson YAML signal states both carve-outs. Pipeline implementation 204, commit 79736144, merged to main and pushed.

Evidence: Tests: 5 new in plugins/os-sdlc/tests/cops/discarded_error_code_test.rb, 2 suppression + 3 must-fire (if status.code == 1; if status.failed? || code == 5; case code with the same class in each when). Full os-sdlc suite 2855 runs, 0 failures, 33.9s. Probe: cop over plugins/ 0 offenses before and after (the old FP file claude_command.rb was removed by #1437); scratch true case still 2 offenses; real site ~/dev/ruby-gems/shortpixel/lib/shortpixel/resource.rb still 25 offenses. #1451 (widening) is now unblocked except for Jared's ruling.

Follow-ups: #1452 (required-work check with a glob that matches no file loops programmer-repair; seen in this run). Known miss, not filed: a case where some when branches share a class and one differs is skipped as a whole.

## Resolution **Done:** Sdlc/Structural/DiscardedErrorCode no longer reports a two-argument raise whose discriminator appears only as the receiver of a ? predicate (unless status.success?), or a raise inside a case whose when branches raise different classes. The message now names the correction: raise Error.new(msg, code: code). Lesson YAML signal states both carve-outs. Pipeline implementation 204, commit 79736144, merged to main and pushed. **Evidence:** Tests: 5 new in plugins/os-sdlc/tests/cops/discarded_error_code_test.rb, 2 suppression + 3 must-fire (if status.code == 1; if status.failed? || code == 5; case code with the same class in each when). Full os-sdlc suite 2855 runs, 0 failures, 33.9s. Probe: cop over plugins/ 0 offenses before and after (the old FP file claude_command.rb was removed by #1437); scratch true case still 2 offenses; real site ~/dev/ruby-gems/shortpixel/lib/shortpixel/resource.rb still 25 offenses. #1451 (widening) is now unblocked except for Jared's ruling. **Follow-ups:** #1452 (required-work check with a glob that matches no file loops programmer-repair; seen in this run). Known miss, not filed: a case where some when branches share a class and one differs is skipped as a whole.
jared closed this issue 2026-10-27 22:59:48 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#1353
No description provided.