os-sdlc lint rule: query-named method reaches a write (same-file CQS) #89
Labels
No labels
P0
P1
P2
P3
bug
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
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#89
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?
Context
Sandi Metz review of
plugins/os_sdlc/lib/os_sdlc/issue_source.rbfound a query-namedpublic entry point that transitively performs a write, via a same-file call chain — a
narrow, mechanically-detectable form of CQS violation.
Problem
plugins/os-sdlc/lib/os_sdlc/issue_source.rb:26-44IssueSource.forreads like a pure lookup ("give me the issue sourceforthis project")but its call chain (
for→resolve_and_persist) reachesproject.save_tracker— a write —inside the same file, one hop away, hidden behind a method name (
resolve_and_persist) thatat least admits the write, but the caller-facing name (
for) does not.Detection
for,find*,fetch*,get*,build*(and does NOT end in?or!, which have their ownconventions).
calls to other methods defined in the same file, followed transitively, same file only
— do not cross file/class boundaries).
/save|persist|write|update/i."IssueSource.for (query-named) calls resolve_and_persist, which calls project.save_tracker (a write) -- separate the query (locating/returning a tracker) from the command (persisting a newly-resolved tracker) into two methods, or rename the entry point so its name admits the write (e.g. resolve_and_persist_tracker) instead of reading as a pure lookup."Correction
(Minimal fix: rename so the write is visible in the name. A deeper fix would split
forinto an explicit
resolvequery plus a caller-drivenpersistcommand, but that changesfor's public contract and is reviewer territory, not this narrow rule's scope.)Pass/fail examples
IssueSource.for, whose same-file call chain (for→resolve_and_persist)reaches
project.save_tracker, as in the current file.IssueSource.forcalling onlyresolve_and_persist_tracker(name admitsthe write) — the rule only flags query-shaped names, so renaming out of the query
convention clears it.
through another file/class (e.g. calling into a service object that itself writes) — out
of scope for this rule, which is deliberately limited to same-file chains; cross-file CQS
violations remain reviewer territory (false-positive risk: this rule cannot see writes
that happen outside the file being linted, and will not report them).
Provenance
Sandi Metz review of
issue_source.rb, cc-os session 2026-07-22 (organic finding — not acouncil/run finding).
Implementation plan
Custom RuboCop cop (batch-2 custom-cop slot; no stock cop does same-file call-graph
reachability analysis). Scope is deliberately narrow (same-file only) to keep false
positives low; implemented directly by a Fable agent, with a Codex review/audit before
merge — the audit should specifically check for false positives on query methods whose
write reachability requires this narrow scope to correctly not-fire.
Implementation note (2026-07-22): internal inconsistency found while implementing from this ticket alone. The 'must pass' example says renaming the ENTRY POINT out of the query convention clears the offense, but the Correction diff instead renames the private helper (resolve_and_persist -> resolve_and_persist_tracker), which still matches /persist/ and would NOT clear the offense under the stated algorithm. The cop honors the algorithm + pass-example rationale (verified: renaming
forclears it). Cop implemented as Sdlc/QueryMethodReachesWrite.Covered: Sdlc/Structural/QueryMethodReachesWrite shipped and enabled in .rubocop.yml with tests. ADR-0061 finalizes the os-sdlc lint program; closing per commit
8e88bbe.