Sdlc/Dry/RepeatedPathLiteral: narrow PathValue to filesystem paths #1374

Closed
opened 2026-10-25 21:23:27 +00:00 by jared · 2 comments
Owner

Problem

Sdlc/Dry/RepeatedPathLiteral treats any string with a / or a word.word part 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:38 calls 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 = every plugins/**/*.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).

shape all sweep example verdict
bare file name with a known extension 116 57 plugins/os/tests/support/repo_seeder.rb:12 "README.md" true positive
relative path whose last segment has a known extension 88 4 plugins/os/tests/worktree/start_command_pipeline_db_test.rb:14 ".sdlc/pipeline.db" true positive
path that starts with /, ./, ../, or ~/ 18 10 plugins/os-aidd-lint/tests/path_reference_scanner_test.rb:111 "/inventory/repos.md" true positive
label prefix/value 7 7 plugins/os-backlog/tests/labels_test.rb:30 "filed-by/agent" false positive
repository slug owner/name 3 1 plugins/os-sdlc/tests/tracker/forgejo_test.rb:11 "jared/widgets" false positive
row ID with a / 1 1 plugins/os-me/test/route_shadow_cli_test.rb:36 "b1/F3" false positive
string with whitespace (TSV row, text line) 4 0 plugins/os-sdlc/tests/crc/jobs_bin_test.rb:19 "lib/a.rb\tA\t\t\t" false positive
dotted method chain 3 0 plugins/os-sdlc/tests/detector/reach_through_test.rb:57 "target.test.command" false positive
relative directory string with no extension and no anchor 9 1 plugins/os-adr/tests/record/matches_test.rb:43 "lib/storage" true positive, missed after the change (see Approach)

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.join or / 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 in plugins/ 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 (real bin/lint-sweep run). 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 in plugins/os-sdlc/tests/cops/repeated_path_literal_test.rb, plugins/os-sdlc/tests/lint/lint_sweep_test.rb, and plugins/os-sdlc/tests/lint/lint_worklist_repeated_path_literal_test.rb pass with the rule (simulated).

Required work:

  1. In plugins/os-sdlc/lib/os_sdlc/cops/dry/repeated_path_literal/path_value.rb, apply the plain-string rule above to a str node only; File.join and / 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"), to plugins/os-sdlc/tests/cops/repeated_path_literal_test.rb.
    check: present filed-by/agent in plugins/os-sdlc/tests/cops/repeated_path_literal_test.rb
  2. Add must-pass fixtures for a dotted method chain ("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 three File.join(@repo, "docs", "crc") calls, to plugins/os-sdlc/tests/cops/repeated_path_literal_test.rb.
    check: present target.test.command in plugins/os-sdlc/tests/cops/repeated_path_literal_test.rb
  3. In the lesson plugins/os-sdlc/lint-lessons/sdlc-dry-repeated-path-literal.yml, state in signal that a plain string counts only when it starts with /, ./, ../, or ~/, or ends in a known file extension.
    check: present known file extension in plugins/os-sdlc/lint-lessons/sdlc-dry-repeated-path-literal.yml
  4. Add one History row for this change to docs/implementation-status/os-sdlc.md.
    check: present #1374 in docs/implementation-status/os-sdlc.md

Acceptance criteria

  1. Given 3 uses of "filed-by/agent", "jared/widgets", or "b1/F3", when the cop runs, then it reports no offense.
  2. Given 3 uses of "target.test.command" or "@a.b.c", when the cop runs, then it reports no offense.
  3. Given 3 uses of "lib/a.rb\tA" or "lib/revisit.rb\n", when the cop runs, then it reports no offense.
  4. Given 3 uses of "lib/os_sdlc/boot.rb", "SKILL.md", or "/repo", when the cop runs, then it reports one offense.
  5. Given 3 calls File.join(@repo, "docs", "crc"), when the cop runs, then it reports one offense, as today.
  6. Given plugins/os-sdlc/bin/lint-sweep plugins/<p> for os-backlog, os, os-adr, os-aidd-lint, and os-me, when it runs, then every Sdlc/Dry/RepeatedPathLiteral offense names a value that starts with /, ./, ../, or ~/, or ends in a listed extension (81 offenses on 4a0fc41d become 71).

Owner sketch

  • RuboCop::Cop::Sdlc::Dry::RepeatedPathLiteral::PathValue (existing) owns the decision: it already knows if its node is a str, a File.join, or a / call, and it already decides path?. It adds the plain-string rule for the str case. No new object.

In scope / out of scope

  • In scope: the plain-string rule, its tests, and the lesson signal.
  • Out of scope (accepted miss, 9 sites): relative directory strings with no extension and no anchor. A rule that counts such a string when the same file builds the same value with File.join or / 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.
  • Out of scope (cut on 2026-10-26, no site in plugins/): extensions not in the list (for example csv, sh, erb, html).
  • Out of scope: the threshold, the message, and the grouping in repeated_path_literal.rb.

Blocking edges

None. One implement run.

Origin

  • Trigger: #1325 zero-false-positive audit of the Sdlc cops
  • Improvised this session: none
  • Chain: false positive ← PATH pattern matches labels, slugs, row IDs, method chains, and rows with whitespace ← DESIGN (plugins/os-sdlc/lib/os_sdlc/cops/dry/repeated_path_literal/path_value.rb)
  • Root candidate: none found; searched RepeatedPathLiteral
  • Where: plugins/os-sdlc/lib/os_sdlc/cops/dry/repeated_path_literal/path_value.rb
  • Session: eb98cdcd-6783-46a0-b7df-cf18a8fe2a56
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os--claude-worktrees-ticket-1325/eb98cdcd-6783-46a0-b7df-cf18a8fe2a56.jsonl

VERDICT: CREATE

## Problem `Sdlc/Dry/RepeatedPathLiteral` treats any string with a `/` or a `word.word` part 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:38` calls 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 = every `plugins/**/*.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). | shape | all | sweep | example | verdict | |---|---|---|---|---| | bare file name with a known extension | 116 | 57 | `plugins/os/tests/support/repo_seeder.rb:12` `"README.md"` | true positive | | relative path whose last segment has a known extension | 88 | 4 | `plugins/os/tests/worktree/start_command_pipeline_db_test.rb:14` `".sdlc/pipeline.db"` | true positive | | path that starts with `/`, `./`, `../`, or `~/` | 18 | 10 | `plugins/os-aidd-lint/tests/path_reference_scanner_test.rb:111` `"/inventory/repos.md"` | true positive | | label `prefix/value` | 7 | 7 | `plugins/os-backlog/tests/labels_test.rb:30` `"filed-by/agent"` | false positive | | repository slug `owner/name` | 3 | 1 | `plugins/os-sdlc/tests/tracker/forgejo_test.rb:11` `"jared/widgets"` | false positive | | row ID with a `/` | 1 | 1 | `plugins/os-me/test/route_shadow_cli_test.rb:36` `"b1/F3"` | false positive | | string with whitespace (TSV row, text line) | 4 | 0 | `plugins/os-sdlc/tests/crc/jobs_bin_test.rb:19` `"lib/a.rb\tA\t\t\t"` | false positive | | dotted method chain | 3 | 0 | `plugins/os-sdlc/tests/detector/reach_through_test.rb:57` `"target.test.command"` | false positive | | relative directory string with no extension and no anchor | 9 | 1 | `plugins/os-adr/tests/record/matches_test.rb:43` `"lib/storage"` | true positive, missed after the change (see Approach) | ## 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.join` or `/` 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 in `plugins/` 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 (real `bin/lint-sweep` run). 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 in `plugins/os-sdlc/tests/cops/repeated_path_literal_test.rb`, `plugins/os-sdlc/tests/lint/lint_sweep_test.rb`, and `plugins/os-sdlc/tests/lint/lint_worklist_repeated_path_literal_test.rb` pass with the rule (simulated). Required work: 1. In `plugins/os-sdlc/lib/os_sdlc/cops/dry/repeated_path_literal/path_value.rb`, apply the plain-string rule above to a `str` node only; `File.join` and `/` 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"`), to `plugins/os-sdlc/tests/cops/repeated_path_literal_test.rb`. check: present `filed-by/agent` in `plugins/os-sdlc/tests/cops/repeated_path_literal_test.rb` 2. Add must-pass fixtures for a dotted method chain (`"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 three `File.join(@repo, "docs", "crc")` calls, to `plugins/os-sdlc/tests/cops/repeated_path_literal_test.rb`. check: present `target.test.command` in `plugins/os-sdlc/tests/cops/repeated_path_literal_test.rb` 3. In the lesson `plugins/os-sdlc/lint-lessons/sdlc-dry-repeated-path-literal.yml`, state in `signal` that a plain string counts only when it starts with `/`, `./`, `../`, or `~/`, or ends in a known file extension. check: present `known file extension` in `plugins/os-sdlc/lint-lessons/sdlc-dry-repeated-path-literal.yml` 4. Add one History row for this change to `docs/implementation-status/os-sdlc.md`. check: present `#1374` in `docs/implementation-status/os-sdlc.md` ## Acceptance criteria 1. Given 3 uses of `"filed-by/agent"`, `"jared/widgets"`, or `"b1/F3"`, when the cop runs, then it reports no offense. 2. Given 3 uses of `"target.test.command"` or `"@a.b.c"`, when the cop runs, then it reports no offense. 3. Given 3 uses of `"lib/a.rb\tA"` or `"lib/revisit.rb\n"`, when the cop runs, then it reports no offense. 4. Given 3 uses of `"lib/os_sdlc/boot.rb"`, `"SKILL.md"`, or `"/repo"`, when the cop runs, then it reports one offense. 5. Given 3 calls `File.join(@repo, "docs", "crc")`, when the cop runs, then it reports one offense, as today. 6. Given `plugins/os-sdlc/bin/lint-sweep plugins/<p>` for os-backlog, os, os-adr, os-aidd-lint, and os-me, when it runs, then every `Sdlc/Dry/RepeatedPathLiteral` offense names a value that starts with `/`, `./`, `../`, or `~/`, or ends in a listed extension (81 offenses on 4a0fc41d become 71). ## Owner sketch - `RuboCop::Cop::Sdlc::Dry::RepeatedPathLiteral::PathValue` (existing) owns the decision: it already knows if its node is a `str`, a `File.join`, or a `/` call, and it already decides `path?`. It adds the plain-string rule for the `str` case. No new object. ## In scope / out of scope - In scope: the plain-string rule, its tests, and the lesson signal. - Out of scope (accepted miss, 9 sites): relative directory strings with no extension and no anchor. A rule that counts such a string when the same file builds the same value with `File.join` or `/` 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. - Out of scope (cut on 2026-10-26, no site in `plugins/`): extensions not in the list (for example `csv`, `sh`, `erb`, `html`). - Out of scope: the threshold, the message, and the grouping in `repeated_path_literal.rb`. ## Blocking edges None. One implement run. ## Origin - Trigger: #1325 zero-false-positive audit of the Sdlc cops - Improvised this session: none - Chain: false positive ← PATH pattern matches labels, slugs, row IDs, method chains, and rows with whitespace ← DESIGN (plugins/os-sdlc/lib/os_sdlc/cops/dry/repeated_path_literal/path_value.rb) - Root candidate: none found; searched RepeatedPathLiteral - Where: plugins/os-sdlc/lib/os_sdlc/cops/dry/repeated_path_literal/path_value.rb - Session: eb98cdcd-6783-46a0-b7df-cf18a8fe2a56 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os--claude-worktrees-ticket-1325/eb98cdcd-6783-46a0-b7df-cf18a8fe2a56.jsonl VERDICT: CREATE
Author
Owner

Work started on branch ticket-1374 via /os-sdlc:implement.

Work started on branch ticket-1374 via /os-sdlc:implement.
Author
Owner

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 186444b2 on 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.

## 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 186444b2 on 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.
jared closed this issue 2026-10-26 15:54:52 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#1374
No description provided.