Sdlc/Dry/RepeatedPathLiteral: narrow PathValue to filesystem paths #1374
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#1374
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
Sdlc/Dry/RepeatedPathLiteraltreats any string with a/or aword.wordpart as a path. Its fix ("name the path once") is correct only for a filesystem path. Labels, repository slugs, row IDs, method chains, and TSV rows that hold a path are not paths, so the fix is wrong for them.plugins/os-sdlc/skills/fix-lints/SKILL.md:38calls these "known false positives".Found by the #1325 zero-false-positive audit (docs/plans/class-length-family/sdlc-cop-audit.md). Shapes measured on main
4a0fc41d, 2026-10-26. One offense is one value repeated 3 or more times in one file. All = everyplugins/**/*.rb(249 offenses). Sweep =plugins/os-sdlc/bin/lint-sweep plugins/<p>over os-backlog, os, os-adr, os-aidd-lint, os-me (81 offenses).plugins/os/tests/support/repo_seeder.rb:12"README.md"plugins/os/tests/worktree/start_command_pipeline_db_test.rb:14".sdlc/pipeline.db"/,./,../, or~/plugins/os-aidd-lint/tests/path_reference_scanner_test.rb:111"/inventory/repos.md"prefix/valueplugins/os-backlog/tests/labels_test.rb:30"filed-by/agent"owner/nameplugins/os-sdlc/tests/tracker/forgejo_test.rb:11"jared/widgets"/plugins/os-me/test/route_shadow_cli_test.rb:36"b1/F3"plugins/os-sdlc/tests/crc/jobs_bin_test.rb:19"lib/a.rb\tA\t\t\t"plugins/os-sdlc/tests/detector/reach_through_test.rb:57"target.test.command"plugins/os-adr/tests/record/matches_test.rb:43"lib/storage"Approach
Fix by shape in the cop matcher. Never use a config Exclude list or a test-path exclusion.
Rule: a value built by
File.joinor/keeps today's rule; the call proves it is a path. A plain string counts as a path only when it has no whitespace, and either it starts with/,./,../, or~/, or the last/segment ends in.<ext>with<ext>in this list:rb md tsv json jsonl yml yaml txt log db c4 bin. The rule adds to today's checks (PATH,NON_SEPARATOR,CAMEL_SEGMENTS,SCHEME); it does not replace them. A value with no/is its own last segment, so"SKILL.md"counts. The list is every extension that a true-positive path uses inplugins/today. The string shape cannot separate"target.test.command"from"likec4.config.json", or"plugin/os-backlog"from"plugins/os-sdlc", so the rule uses a closed extension list and an anchor.Measured effect (scratch simulation on main
4a0fc41d, the rule patched into a copy of the cop): all plugins 249 to 222 offenses; sweep 81 to 71 (realbin/lint-sweeprun). All 18 false positives go. The 222 kept offenses are all true positives. The 9 relative directory strings with no extension and no anchor (for example"lib/storage","docs/crc/run","plugins/os-sdlc") are no longer reported. This miss is accepted, as in ruling R2 (docs/plans/class-length-family/HANDOFF.md): the cop gives up recall to keep every offense true. The existing tests inplugins/os-sdlc/tests/cops/repeated_path_literal_test.rb,plugins/os-sdlc/tests/lint/lint_sweep_test.rb, andplugins/os-sdlc/tests/lint/lint_worklist_repeated_path_literal_test.rbpass with the rule (simulated).Required work:
plugins/os-sdlc/lib/os_sdlc/cops/dry/repeated_path_literal/path_value.rb, apply the plain-string rule above to astrnode only;File.joinand/values keep today's rule. Add must-pass fixtures for a label ("filed-by/agent"), a slug ("jared/widgets"), and a row ID ("b1/F3"), and must-fire fixtures for an anchored path with no extension ("/repo") and a bare file name ("SKILL.md"), toplugins/os-sdlc/tests/cops/repeated_path_literal_test.rb.check: present
filed-by/agentinplugins/os-sdlc/tests/cops/repeated_path_literal_test.rb"target.test.command","@a.b.c") and for strings with whitespace ("lib/a.rb\tA","lib/revisit.rb\n"), and a must-fire fixture for threeFile.join(@repo, "docs", "crc")calls, toplugins/os-sdlc/tests/cops/repeated_path_literal_test.rb.check: present
target.test.commandinplugins/os-sdlc/tests/cops/repeated_path_literal_test.rbplugins/os-sdlc/lint-lessons/sdlc-dry-repeated-path-literal.yml, state insignalthat a plain string counts only when it starts with/,./,../, or~/, or ends in a known file extension.check: present
known file extensioninplugins/os-sdlc/lint-lessons/sdlc-dry-repeated-path-literal.ymldocs/implementation-status/os-sdlc.md.check: present
#1374indocs/implementation-status/os-sdlc.mdAcceptance criteria
"filed-by/agent","jared/widgets", or"b1/F3", when the cop runs, then it reports no offense."target.test.command"or"@a.b.c", when the cop runs, then it reports no offense."lib/a.rb\tA"or"lib/revisit.rb\n", when the cop runs, then it reports no offense."lib/os_sdlc/boot.rb","SKILL.md", or"/repo", when the cop runs, then it reports one offense.File.join(@repo, "docs", "crc"), when the cop runs, then it reports one offense, as today.plugins/os-sdlc/bin/lint-sweep plugins/<p>for os-backlog, os, os-adr, os-aidd-lint, and os-me, when it runs, then everySdlc/Dry/RepeatedPathLiteraloffense names a value that starts with/,./,../, or~/, or ends in a listed extension (81 offenses on4a0fc41dbecome 71).Owner sketch
RuboCop::Cop::Sdlc::Dry::RepeatedPathLiteral::PathValue(existing) owns the decision: it already knows if its node is astr, aFile.join, or a/call, and it already decidespath?. It adds the plain-string rule for thestrcase. No new object.In scope / out of scope
File.joinor/would keep 3 of them (plugins/os-sdlc/tests/crc/records_bin_test.rb,plugins/os-sdlc/tests/crc/tickets_bin_test.rb,plugins/os-sdlc/tests/project/lint_target_path_test.rb). It is a file-level rule, not a string rule; file it separately if wanted.plugins/): extensions not in the list (for examplecsv,sh,erb,html).repeated_path_literal.rb.Blocking edges
None. One implement run.
Origin
VERDICT: CREATE
Work started on branch ticket-1374 via /os-sdlc:implement.
Resolution
Done: RepeatedPathLiteral counts a plain string only when it has no whitespace and starts with /, ./, ../ or ~/ or ends in a known extension; File.join and / values keep the old rule. Tests, lesson signal and History row updated.
Evidence: Implementation 171, commit
186444b2on ticket-1374; all 10 pipeline dispatches passed, including the full suite (82 s) and contract-auditor.Follow-ups: none: the directory-string recovery rule, the sdlc-cop-audit.md row and the fix-lints SKILL.md:38 note are carried in handoff 2026-10-26-1527 for the #1317 sweep step.