Redesign RepeatedLiteralArgument into RepeatedPathLiteral (whole-path cop) #1014
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#1014
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/Dry/RepeatedLiteralArgumentgroups each string argument ofFile.joinby value and fires at 3 or more calls. It counts path segments, not whole paths. It ignores the same path written as a plain string. Analysis: docs/research/lint-lessons/sdlc-dry-repeated-literal-argument.md, sections 2, 3, 6, and 7.Two results from #1004 (
eval/ticket-takedown/test/command/final_suite_test.rb):"lib"in 4File.joincalls. The segment"lib"is shared by different paths. The real fact is the whole pathlib/y.rb, typed 9 times in two spellings..sdlc/project.yamlappears in 6 places, one of them as the string".sdlc/project.yaml". The cop counts the segment".sdlc"only.Commit
d3cfd3e6also shows a dodge. Two classes each definedef project_file = File.join(@repo, ".sdlc", "project.yaml"), and a string form of the same path remains. The helper hides the repeat from a per-call count.Reproduce
n/a: design ticket. The cop source is plugins/os-sdlc/lib/os_sdlc/cops/repeated_literal_argument.rb.
Approach
Design ruled by Jared on 2026-10-15. Count whole path values, string forms included, in path positions only.
File.join(@repo, "lib", "y.rb")has the valuelib/y.rb.["lib/y.rb"]in a path array, or a path-typed argument, has the same valuelib/y.rb.File.join(@repo, "lib", "z.rb")has a different value,lib/z.rb.Sdlc/Dry/RepeatedPathLiteral, with its config block, require line, source directory, and test file. The lesson key becomessdlc-dry-repeated-path-literal.Must fire:
lib/y.rbin 9 places (analysis section 2)..sdlc/project.yamlin 6 places including a string form.Must pass: the shared segment
libacross different paths. A path inside a log or message string, for example"wrote lib/y.rb".RULED 2026-10-16 (Jared): "path position" is defined by the value, not by the consumer. There is no list of path-taking calls.
A path value is: a string literal, taken whole, with no space and no "://", that either contains "/" or ends in a file extension (for example
project.yaml,y.rb); OR aFile.join/Pathname#/expression whose trailing literal segments join to such a value (File.join(@repo, "lib", "y.rb")has the valuelib/y.rb). Interpolated strings and heredocs are not counted. Group by value per file; fire at 3 or more places. A literal that is a path but sits in a non-file call (deleted: ["lib/y.rb"]) counts: it is the same fact. A string that contains a path inside other text ("wrote lib/y.rb") does not count: it is not equal to the value.Tier: this cop is judgment-tier, not must-pass. It will have some false positives. It must NOT be enabled in the gate config
plugins/os-sdlc/.rubocop.yml, and it must never reach the fix-lints worklist. It runs only on occasional sweep passes (see the "Sweeps" section of the fix-lints skill) through a separate sweep config and runner that this ticket adds. The lint lessonsdlc-dry-repeated-path-literalis the fix guidance; when the agent finds no smell at a reported site it files a false-positive ticket.Before enabling the cop in the sweep config, run it over
plugins/and classify every hit by hand; tighten the value rule for each false hit, never add an allowlist.Required work:
RepeatedPathLiteralinplugins/os-sdlc/lib/os_sdlc/cops/*.rbRepeatedLiteralArgumentinplugins/os-sdlc/.rubocop.ymlRepeatedPathLiteralinplugins/os-sdlc/.rubocop.ymlRepeatedPathLiteralinplugins/os-sdlc/.rubocop-sweep.ymlrubocopinplugins/os-sdlc/bin/lint-sweepAcceptance criteria
File.join(@repo, "lib", "y.rb")in 3 places and["lib/y.rb"]in 6 other path positions of one file, when the cop runs, then it reports the valuelib/y.rb.File.join(@repo, ".sdlc", "project.yaml")in 5 places and".sdlc/project.yaml"in a path position, when the cop runs, then it reports the value.sdlc/project.yaml.File.join(@repo, "lib", "y.rb"),File.join(@repo, "lib", "z.rb"), andFile.join(@repo, "lib", "w.rb"), when the cop runs, then it reports nothing."lib/y.rb"inside a log or message string 3 times, when the cop runs, then it reports nothing.def project_file = File.join(@repo, ".sdlc", "project.yaml")and one string form of the same path, when the cop runs, then it reports the value.deleted: ["lib/y.rb"]in 3 places of one file, when the cop runs, then it reports the valuelib/y.rb.bin/lint-worklistruns, thenRepeatedPathLiteraloffenses do not appear in any phase.bin/lint-sweepis run on a directory, when the cop finds a repeated path value, then the offense is printed with the cop name and nothing exits non-zero as a gate.Owner sketch
Non-goals
Fixes at the #1004 sites. The lesson text (drafted separately).
Origin
Ruling 2026-10-16 (Jared, via session 983d18f5): path position is defined by value, not consumer; the cop is judgment-tier and runs only in a sweep pass, never in the gate. Approach, required work, and acceptance criteria updated in the body. Build starts in worktree lint-lessons.
ADR constraints for the sweep runner (from /os-adr:find, 2026-10-16; no new ADR needed): (1) ADR-0082: bin/lint-sweep resolves the target from the invoking project's .sdlc/project.yaml and layers plugin .rubocop.yml first, target config second, the same way bin/lint-worklist does; it installs nothing into the target. (2) ADR-0066: the sweep reports only; it never writes Exclude entries into a target .rubocop.yml. (3) ADR-0179: lint_config default is nil and a configured file that is missing raises HarnessError. The sweep config plugins/os-sdlc/.rubocop-sweep.yml inherits the gate config's cop requires and enables only judgment-tier cops. Adding a non-gating tier to Worklist::CopPhase is a habit change, no ADR.
Work started 2026-10-16 in worktree lint-lessons (branch lint-lessons, based on prism-architect). Pipeline: /os-sdlc:implement 1014.
Resolution
Done: Sdlc/Dry/RepeatedPathLiteral replaces RepeatedLiteralArgument: whole path values grouped per file, string and File.join spellings together, 3+ places, once per source line. Judgment tier: absent from the gate config, run by bin/lint-sweep with .rubocop-sweep.yml (advisory, exit 0 on offenses, exit 3 on rubocop failure). Lint lesson sdlc-dry-repeated-path-literal written. Value rule tightened after a probe over plugins/.
Evidence: Branch lint-lessons commits
5d6c440e(cop, sweep, tests) and1cd2fe5b(lesson). Suite 1579 runs 0 failures 0 errors. Rubocop 0 offenses on changed files. Probe: 197 hits before tightening, 164 after, none in lib/. Design audit: 4 findings, all fixed. Implementation 51 escalated on a test defect (missing require in the cop test) and was hand-finished per the escalation procedure.Follow-ups: #1020 lint-worklist UnmappedCop on os-sdlc target (filed, CREATE). Remaining judgment-tier false positives are lowercase slash identifiers such as project/cc-os; no syntactic rule separates them, accepted as the tier's residue. The 164 real hits are fixture paths repeated in test files; fixing them belongs to #1019 fixture strategy.