Sdlc/Structural/UnreachableBranchAfterGuard: stop guards at receiver-mutating calls and handle unless #1361
Labels
No labels
P0
P1
P2
P3
bug
category/code-change
category/cross-repo
category/eval-run
category/investigation
category/no-test-target
category/owner-ruling
category/prose-artifact
category/recurring-chore
category/structure
category/umbrella
crc-refactor
create
delete
enhancement
filed-by/agent
filed-by/user
frozen
lint-rule
needs-info
needs-triage
next
plugin/cc-architect
plugin/os
plugin/os-adr
plugin/os-aidd-lint
plugin/os-backlog
plugin/os-context
plugin/os-doc-hygiene
plugin/os-sdlc
plugin/os-vault
project/cc-os
ready-for-agent
ready-for-human
recurring
review
rule-proposal
update
waiting
wayfinder:grilling
wayfinder:map
wayfinder:map
wayfinder:research
wayfinder:task
wayfinder:task
wontfix
worklist/deviations
worklist/lint-rule
worklist/new-implement-build
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
jared/cc-os#1361
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
Rewritten 2026-10-27 from docs/lint-lessons/analyses/sdlc-structural-unreachable-branch-after-guard.md.
Sdlc/Structural/UnreachableBranchAfterGuardkeeps a guard active after the code changes the guarded variable through a send (q.clear,log(q.shift)) or passes it as an argument (drain(q)). The cop then calls a live branch unreachable. Forunless, and forif/ternary with anelse, the message says "unreachable" but does not name the dead side, so an author can delete the code that always runs.Approach
Fix by shape, in the matcher. Use no Exclude list and no list of method names. Rule:
docs/lint-lessons/analyses/sdlc-structural-unreachable-branch-after-guard.mdstep 6.Required work:
?, or passes the variable as an argument to a send or yield.ifwith no else: "delete the conditional". Forif/ternary with else: "the then side is dead; keep the else side". Forunless: "the condition always passes; keep the body".clear, nestedshift, and argument use, and must-fire fixtures for each message form.Acceptance criteria
queue.empty?, thenqueue.clear, thenif queue.empty?, when the cop runs, then it reports no offense.list.empty?, thenlog(list.shift), thenif list.empty?, when the cop runs, then it reports no offense.list.empty?, thendrain(list), thenlist.empty? ? a : b, when the cop runs, then it reports no offense.list.empty?, thenunless list.empty?, when the cop runs, then the message says the condition always passes and tells the author to keep the body.x.empty? ? a : b, when the cop runs, then the message says to keep the else side.ifwith no use of the variable between them, when the cop runs, then it still reports an offense.Owner sketch
Origin
lvasgnonly, and the message ignoresunlesspolarity ← DESIGN (reassignment.rb:9)Body rewritten 2026-10-27 from the false-positive process; analysis and lint lesson merged to main (docs/lint-lessons/analyses/, plugins/os-sdlc/lint-lessons/).
Work started on branch ticket-1361 (os-sdlc implement pipeline).
Resolution
Done: Sdlc/Structural/UnreachableBranchAfterGuard ends a guard at a later statement that sends the variable a non-predicate message (at any depth) or passes it as an argument to a send, yield, or super (GuardRelease composes Reassignment). Branch#message names the live side by form: if without else, if/ternary with else, unless. Orchestrator follow-up commit
5c000777: the branch is checked before its own statement releases the guard, so a use inside the dead branch body no longer hides the offense.Evidence: Pipeline implementation 202, 8 dispatches all pass (
bfe1cd4e); fix5c000777with must-fire test; ruby plugins/os-sdlc/tests/all.rb 2842 runs 0 failures (36 s); cop across plugins/: 0 offenses before and after (2751 files); merged to main and pushed (5c000777). Lint lesson YAML already describes the new forms; unchanged.Follow-ups: Pipeline gate gap captured separately: no gate probed for a new false negative introduced by a cop release rule.
Gate-gap note (capture dropped by ticket-skeptic, depth-1): behavior-verifier checks only the listed acceptance criteria, so the new GuardRelease false negative (branch body use released its own guard) passed all 8 gates; fixed by the orchestrator in
5c000777. A cop ticket that adds a suppression rule should list must-fire cases in its acceptance criteria.