Widen QueryMethodReachesWrite to follow calls into same-file classes #1449
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#1449
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
The cop
Sdlc/Structural/QueryMethodReachesWritefollows calls only to same-file defs.A query that calls a write through another same-file class is invisible to it.
The real site still writes from a query behind a class named "lookup":
TrackerLookup.new(project).call.Observed
Analysis:
docs/lint-lessons/analyses/sdlc-structural-query-method-reaches-write.md, option B.The cop sees names of same-file defs only. It does not follow a send into another class in the same file.
Reproduce
n/a: this is a widening of a cop; the matcher is
plugins/os-sdlc/lib/os_sdlc/cops/structural/query_method_reaches_write.rb(helperswrite_chain.rb,write_chain_offense.rbin thequery_method_reaches_write/folder).Approach
#1380 shipped (main
311ab69a):QUERY_NAMEandWRITE_NAMEare anchored, and a def counts as a write when its body sends a write verb.Follow a send into a class or module defined in the same file, and report when a method of that class reaches a write.
Value rule (judgment-tier ruling, 2026-10-28): the query promise belongs to the caller's name, not to the callee's class name. So the cop follows every same-file class or module, with no class-name list:
Const.new(...).mfollows to the instance defmin the same-file class whose name isConst(last name segment).Const.mfollows to the singleton defself.min the same-file class or moduleConst.selfresolve as today.Rule from Jared: every new hit must be a true positive, and no Exclude lists. Carve-outs go in the matcher.
The existing test
test_passes_when_the_write_is_reached_only_through_another_classasserts no offense for a same-fileTrackerService.new(...).resolve_and_persist. That shape is now a must-fire; the test changes to assert the offense.WriteChain#body_mutates?andWriteChainOffense#concrete_write_callboth scan a def body for a write send. If the change touches both, give that scan one owner.Required work:
constructed_classinplugins/os-sdlc/lib/os_sdlc/cops/structural/query_method_reaches_write/call_keys.rbAcceptance criteria
Lookup.new(x).call, whereLookupis a class in the same file and its instance methodcallcalls a private method whose body sendssave_tracker, when the cop runs, then it reports one offense on the query-named method, and the message namesLookup#callas the first call.Lookup.call(x), whereLookupis a module in the same file and itsdef self.callsendsFile.write, when the cop runs, then it reports one offense on the query-named method.WRITE_THROUGH_ANOTHER_CLASS_SOURCEin the cop test (IssueSource.forcallsTrackerService.new(runner: runner).resolve_and_persist(project), same file), when the cop runs, then it reports one offense onIssueSource.for.Orchestrator check after the run (not a test): run the cop over all of
plugins/, including extensionless scripts; every new hit is a true positive and is fixed in this unit. Must-not-fire regression cases (a same-fileLookup#callthat only computes; a constant from another file) are added by the orchestrator after the run.Owner sketch
Origin
.cc-os/os/handoffs/2026-10-27-1542-lint-cops-track.md).VERDICT: CREATE
Blocked by #1380 (verb-position anchoring on the same matchers). Remove
waitingwhen #1380 closes.#1380 is closed (main
311ab69a): QUERY_NAME and WRITE_NAME are anchored, and a chain def also counts as a write when its body sends an anchored write verb. #1449 is unblocked.Work started on branch ticket-1449 (worktree .claude/worktrees/ticket-1449), os-sdlc pipeline, target os-sdlc. Ticket body corrected first: the required-work path now has the structural/ folder; ACs are must-fire only; value rule recorded in Approach.
Follow-up dropped by ticket-skeptic: IssueSource.for still reaches a write through TrackerLookup#call (cross-file, so the widened cop stays silent), but IssueSource.for has no production caller and open #1216 deletes it. TrackerLookup#call still persists for its live caller tracker/forgejo.rb:11; fold that note into #1216.
Follow-up dropped by ticket-skeptic (speculative hardening): known cop gaps after #1449, none a regression against main and no real site missed. Data.define/Struct.new block defs get owner Object; DefKey keys by last name segment so A::Helper and B::Helper collide (main had the same flaw); module_function/extend self, self.class.m and Const.new into initialize are not followed.
Resolution
Done: QueryMethodReachesWrite follows Const.m and Const.new(...).m into same-file classes and modules (value rule: follow every same-file class/module, no class-name list). Defs keyed by owner/kind/name (DefKey, SameFileDefs, CallKeys); a bare/self call falls back to the name-only match main used, so mixin and superclass helpers still resolve. Lesson signal and analysis row updated.
Evidence: main
5ad0d5f1pushed. os-sdlc suite 2877 runs, 0 failures. Lint clean for the change (5 pre-existing RawFileContentAssertion offenses on main, filed #1456). Probe of the cop over 2752 plugins/ files incl. extensionless scripts: 0 offenses, 0 errors. 18 cop tests incl. must-not-fire (computing Lookup#call, constant from another file) and regressions (class << self helper, Data.define block def crash, mixin/superclass, top-level label). Pipeline impl 208 escalated on a brittle required-work check, hand-finished with one design audit; run abandoned.Follow-ups: #1456 (lint red on main). Dropped by skeptic, noted on this ticket: IssueSource.for/TrackerLookup write (fold into #1216); cop keying gaps (Data.define owners, last-segment collisions, module_function).