Skip to content

fix: suppress context-drift-check false positives (generated-bridge, renamed, external-repo refs)#32

Open
Nathan Schram (nathanschram) wants to merge 1 commit into
mainfrom
fix/drift-check-false-positives
Open

fix: suppress context-drift-check false positives (generated-bridge, renamed, external-repo refs)#32
Nathan Schram (nathanschram) wants to merge 1 commit into
mainfrom
fix/drift-check-false-positives

Conversation

@nathanschram

@nathanschram Nathan Schram (nathanschram) commented Jul 4, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #31 — the Context Guard drift check (context-drift-check.sh) reported false positives for backtick-quoted references that legitimately do not exist in the local repo. Found by dogfooding: a trivial .gitignore commit on this repo triggered five "not found" warnings, all false positives.

Root cause

The broken-path check extracted every backtick-quoted `…​.(ts|js|md|…)` token and flagged it unless it existed locally — assuming every such token is a claim the file exists in this repo. Three legitimate categories break that assumption:

  1. Generated-bridge filenames the plugin emits into other projects (GEMINI.md, .windsurfrules, …)
  2. Historical rename mentions (e.g. "renamed from index.md" → the real file is faq.md)
  3. External-repo paths (e.g. "the marketing-site scripts/docs-sync.config.ts mapping")

Fix

  • Generated-bridge allowlist — skip references whose basename is a filename ContextDocs generates as a bridge.
  • Same-line cue-word skip — skip a reference when its line contains a descriptive cue (renamed, formerly, previously, marketing-site, external repo, …).
  • Genuinely missing repo-local paths (no cue, not a bridge) are still flagged — verified by a regression test.

Applied identically to both tracked copies: the canonical hooks/context-drift-check.sh (what ships to users / what the test suite runs) and the dogfooded .claude/hooks/context-drift-check.sh (kept byte-in-sync, as all 7 hooks are).

Testing

  • 4 new hook unit tests in tests/test-hooks.sh: three suppression cases (GEMINI.md / rename mention / marketing-site) + one regression guard (src/missing.ts still flagged).
  • Full suite: 120/120 passing (was 116, +4).
  • End-to-end: running the hook against this repo's real context files now returns {} (previously 5 false positives).
  • typos clean.

Not included

Two softer options from #31 (an ignore-file mechanism, repo-relative-path biasing) were left out — the allowlist + cue-word combination clears all observed false positives without them. Easy follow-ups if new categories appear.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Reduced false drift warnings for references that are expected to be mentioned but may not exist locally, including generated bridge names, rename notes, and cross-repository references.
    • Kept alerts for truly missing local paths so real issues still surface.
  • Tests

    • Added regression coverage for the updated drift-check behavior.
  • Documentation

    • Updated the changelog to note the false-positive fix.

…e, renamed, and external-repo path references

The broken-path check flagged every backtick-quoted path that did not
exist locally, producing false positives on three legitimate reference
categories: generated-bridge filenames the plugin emits into other
projects (e.g. GEMINI.md), historical rename mentions ("renamed from
`index.md`"), and paths in other repos ("marketing-site
`scripts/docs-sync.config.ts`").

Add a generated-bridge basename allowlist and a same-line cue-word skip.
Genuinely missing repo-local paths are still flagged. Applied to both the
canonical hooks/ copy and the dogfooded .claude/hooks/ copy (kept in
sync), plus 4 new hook unit tests (120/120 passing).

Closes #31

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Two copies of context-drift-check.sh gain GENERATED_BRIDGES and DESCRIPTIVE_CUE constants used to skip false-positive broken-path warnings for backtick-quoted references to generated bridge filenames, renamed files, or external-repo paths. Regression tests and a CHANGELOG entry accompany the change.

Changes

Broken-path false-positive suppression

Layer / File(s) Summary
Plugin hook detection logic
.claude/hooks/context-drift-check.sh
Adds GENERATED_BRIDGES and DESCRIPTIVE_CUE constants and reworks the broken-path check to skip reporting when the basename matches a bridge filename or the reference line contains a descriptive cue, falling back to existing existence checks otherwise.
Installed hook detection logic
hooks/context-drift-check.sh
Mirrors the same constants and broken-path skip logic in the installed copy of the hook.
Regression tests and changelog
tests/test-hooks.sh, CHANGELOG.md
Adds test cases covering generated-bridge, rename, and external-repo reference suppression alongside a still-detected missing-path case, and documents the fix under Unreleased → Fixed.

Estimated code review effort: 2 (Simple) | ~12 minutes

Sequence Diagram(s)

Not applicable — the changes are localized shell-script conditional logic, tests, and documentation without multi-component interaction flows.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main fix: suppressing false positives in context-drift-check for generated-bridge, renamed, and external-repo refs.
Linked Issues check ✅ Passed The hook changes and tests match #31 by suppressing the listed false positives while still flagging genuinely missing local paths.
Out of Scope Changes check ✅ Passed The PR stays focused on the drift-check fix, its tests, and a changelog note, with no evident unrelated code changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/drift-check-false-positives

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.claude/hooks/context-drift-check.sh (1)

86-100: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve per-occurrence context before the cue-word skip
Lines 86-100: this check is file-wide, so one descriptive mention of docs/api.md will skip every occurrence of that path in the file, including a genuinely broken one on another line. Keep line numbers through the match, or defer sort -u until after the cue check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/hooks/context-drift-check.sh around lines 86 - 100, The
reference-skip logic in context-drift-check.sh is applied too broadly because
the grep for `$REF_PATH` is file-wide and the later `sort -u` collapses
duplicate matches, so one descriptive mention can suppress a truly broken
occurrence elsewhere. Update the matching flow in the loop around `SKIP_REF`,
`REF_PATH`, and `BASENAME` so each occurrence is evaluated independently,
preserving per-match line context before applying the descriptive cue check or
deferring deduplication until after that check.
🧹 Nitpick comments (2)
tests/test-hooks.sh (1)

432-466: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Tests correctly validate the intended suppression cases.

Traced each scenario against the hook logic — bridge, rename-cue, marketing-site-cue, and genuinely-broken cases all resolve as expected. Shellcheck's SC2016 hints on the single-quoted printf lines are expected here (literal backticks, no expansion wanted) and can be ignored.

Consider adding a regression test for the basename-collision edge case flagged in hooks/context-drift-check.sh (a nested path like `docs/other/CLAUDE.md` that doesn't exist), to lock in the fix once addressed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test-hooks.sh` around lines 432 - 466, Add a regression test for the
basename-collision edge case in the broken-path suppression suite. Extend the
scenarios in tests/test-hooks.sh to cover a nested repo-local path like
`docs/other/CLAUDE.md` that does not exist, and assert the hook still reports it
as missing instead of suppressing it. Use the existing run_test pattern
alongside the current false-positive suppression cases to keep the behavior
locked in.
hooks/context-drift-check.sh (1)

41-51: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Several GENERATED_BRIDGES entries can never be matched.

The extraction regex on line 99 only captures backtick tokens that start with [a-zA-Z] and end in .(ts|js|py|go|rs|md|json|toml|yaml|yml|sh). That means .windsurfrules, .cursorrules, .clinerules (leading dot fails the alpha start), and llms.txt, llms-full.txt, agents.mdc (extension not in the allowed list) can never be extracted as REF_PATH in the first place, so their allowlist entries are dead code.

Not a functional regression (these were never flagged before either), but it's misleading — the allowlist implies coverage for exactly the file types called out in the PR description as sources of false positives.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hooks/context-drift-check.sh` around lines 41 - 51, Several entries in
GENERATED_BRIDGES are unreachable because the reference-extraction regex only
matches backtick paths with an alphabetic start and a limited extension set.
Update the allowlist in context-drift-check.sh so it only includes filenames
that can actually be captured by the REF_PATH extraction logic, or broaden that
extraction rule if those dotfiles and alternate extensions are meant to be
supported. Keep the GENERATED_BRIDGES list aligned with the regex behavior to
avoid dead entries.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/hooks/context-drift-check.sh:
- Around line 44-46: The `context-drift-check.sh` bridge allowlist includes
`agents.mdc`, but the backtick-reference extraction regex used by the script
does not match the `.mdc` extension, so that entry can never be reached. Update
the reference extraction pattern in the script’s regex logic to include `mdc`,
and ensure the `GENERATED_BRIDGES` entry for `agents.mdc` remains aligned with
the symbols used for bridge detection.

In `@hooks/context-drift-check.sh`:
- Around line 78-96: The broken-path detection in context-drift-check.sh is too
broad because GENERATED_BRIDGES and the descriptive-cue fallback are applied by
basename or by any same-path match, which can wrongly skip real bad references.
Tighten the skip logic around REF_PATH/BASENAME so generated-bridge suppression
only applies to exact bare bridge filenames, and make the descriptive cue check
evaluate the specific reference occurrence rather than any matching path
elsewhere in the CTX content.

---

Outside diff comments:
In @.claude/hooks/context-drift-check.sh:
- Around line 86-100: The reference-skip logic in context-drift-check.sh is
applied too broadly because the grep for `$REF_PATH` is file-wide and the later
`sort -u` collapses duplicate matches, so one descriptive mention can suppress a
truly broken occurrence elsewhere. Update the matching flow in the loop around
`SKIP_REF`, `REF_PATH`, and `BASENAME` so each occurrence is evaluated
independently, preserving per-match line context before applying the descriptive
cue check or deferring deduplication until after that check.

---

Nitpick comments:
In `@hooks/context-drift-check.sh`:
- Around line 41-51: Several entries in GENERATED_BRIDGES are unreachable
because the reference-extraction regex only matches backtick paths with an
alphabetic start and a limited extension set. Update the allowlist in
context-drift-check.sh so it only includes filenames that can actually be
captured by the REF_PATH extraction logic, or broaden that extraction rule if
those dotfiles and alternate extensions are meant to be supported. Keep the
GENERATED_BRIDGES list aligned with the regex behavior to avoid dead entries.

In `@tests/test-hooks.sh`:
- Around line 432-466: Add a regression test for the basename-collision edge
case in the broken-path suppression suite. Extend the scenarios in
tests/test-hooks.sh to cover a nested repo-local path like
`docs/other/CLAUDE.md` that does not exist, and assert the hook still reports it
as missing instead of suppressing it. Use the existing run_test pattern
alongside the current false-positive suppression cases to keep the behavior
locked in.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e9688f85-86b7-45fb-938e-40ad658b014d

📥 Commits

Reviewing files that changed from the base of the PR and between b4c84a0 and 263f656.

📒 Files selected for processing (4)
  • .claude/hooks/context-drift-check.sh
  • CHANGELOG.md
  • hooks/context-drift-check.sh
  • tests/test-hooks.sh

Comment on lines +44 to +46
GENERATED_BRIDGES=("GEMINI.md" "AGENTS.md" "CLAUDE.md" "llms.txt" "llms-full.txt"
".windsurfrules" ".cursorrules" ".clinerules"
"copilot-instructions.md" "agents.mdc" "agents.md")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

agents.mdc bridge entry is unreachable — extraction regex has no .mdc extension.

GENERATED_BRIDGES includes "agents.mdc" (Line 46), but the backtick-reference extraction regex on Line 99 only matches ts|js|py|go|rs|md|json|toml|yaml|yml|sh extensions. A reference like `agents.mdc` will never even be captured, so this allowlist entry can never fire — it's dead. If Cursor's modern agents.mdc (mentioned in CHANGELOG Line 25) needs suppression too, the extraction regex needs the extension added.

🐛 Proposed fix
   done < <(grep -oE '`[a-zA-Z][a-zA-Z0-9._/-]+\.(ts|js|py|go|rs|md|json|toml|yaml|yml|sh)`' "$CTX" 2>/dev/null \
+  done < <(grep -oE '`[a-zA-Z][a-zA-Z0-9._/-]+\.(ts|js|py|go|rs|md|mdc|json|toml|yaml|yml|sh)`' "$CTX" 2>/dev/null \

Also applies to: 99-100

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/hooks/context-drift-check.sh around lines 44 - 46, The
`context-drift-check.sh` bridge allowlist includes `agents.mdc`, but the
backtick-reference extraction regex used by the script does not match the `.mdc`
extension, so that entry can never be reached. Update the reference extraction
pattern in the script’s regex logic to include `mdc`, and ensure the
`GENERATED_BRIDGES` entry for `agents.mdc` remains aligned with the symbols used
for bridge detection.

Comment on lines 78 to 96
BASENAME=$(basename "$REF_PATH")
if ! git ls-files "*/$BASENAME" "$BASENAME" 2>/dev/null | grep -q . \

# Skip generated-bridge filenames — plugin output, not a local file.
SKIP_REF=false
for BRIDGE in "${GENERATED_BRIDGES[@]}"; do
if [ "$BASENAME" = "$BRIDGE" ]; then SKIP_REF=true; break; fi
done

# Skip references marked descriptive by a same-line cue word (rename
# history or a path that lives in another repo).
if ! $SKIP_REF && grep -F -- "$REF_PATH" "$CTX" 2>/dev/null | grep -qiE "$DESCRIPTIVE_CUE"; then
SKIP_REF=true
fi

# Fallback: check if basename exists anywhere in repo (tracked or untracked)
if ! $SKIP_REF \
&& ! git ls-files "*/$BASENAME" "$BASENAME" 2>/dev/null | grep -q . \
&& ! find . -name "$BASENAME" -not -path './.git/*' -print -quit 2>/dev/null | grep -q .; then
BROKEN_PATHS+=("$CTX references \`$REF_PATH\` (not found)")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target script and related symbols.
git ls-files hooks/context-drift-check.sh
wc -l hooks/context-drift-check.sh
sed -n '1,220p' hooks/context-drift-check.sh

printf '\n--- search ---\n'
rg -n 'GENERATED_BRIDGES|CONTEXT_FILES|DESCRIPTIVE_CUE|SKIP_REF|BROKEN_PATHS|REF_PATH|BASENAME' hooks/context-drift-check.sh

Repository: littlebearapps/contextdocs

Length of output: 6267


🏁 Script executed:

python3 - <<'PY'
from pathlib import PurePosixPath
import re

GENERATED_BRIDGES = {"GEMINI.md", "AGENTS.md", "CLAUDE.md", "llms.txt", "llms-full.txt",
                     ".windsurfrules", ".cursorrules", ".clinerules",
                     "copilot-instructions.md", "agents.mdc", "agents.md"}
DESCRIPTIVE_CUE = re.compile(r"renamed|formerly|previously|used to be|marketing[- ]site|external repo|upstream repo|other repo", re.I)

def should_skip(ref_path, ctx_text):
    # mirrors the script's logic closely
    if not ref_path:
        return False
    if True:  # [ -n "$REF_PATH" ] && [ ! -e "$REF_PATH" ] is modeled as missing inputs only
        basename = PurePosixPath(ref_path).name
        skip = False
        for bridge in GENERATED_BRIDGES:
            if basename == bridge:
                skip = True
                break
        if not skip:
            # grep -F -- "$REF_PATH" "$CTX" | grep -qiE "$DESCRIPTIVE_CUE"
            matching_lines = [line for line in ctx_text.splitlines() if ref_path in line]
            if any(DESCRIPTIVE_CUE.search(line) for line in matching_lines):
                skip = True
        return skip

cases = [
    (
        "docs/other-project/CLAUDE.md",
        "See `docs/other-project/CLAUDE.md` for details.",
        "A missing nested CLAUDE.md reference"
    ),
    (
        "docs/other-project/CLAUDE.md",
        "renamed from `docs/other-project/CLAUDE.md`\nLater we still reference `docs/other-project/CLAUDE.md` elsewhere.",
        "Same ref string appears on a cue line and a non-cue line"
    ),
    (
        "scripts/docs-sync.config.ts",
        "The marketing-site `scripts/docs-sync.config.ts` mapping.",
        "Descriptive external path"
    ),
]

for ref, ctx, label in cases:
    print(f"\nCASE: {label}")
    print("ref:", ref)
    print("skip:", should_skip(ref, ctx))
PY

Repository: littlebearapps/contextdocs

Length of output: 439


Restrict bridge skipping to exact filename refs.

  • Matching GENERATED_BRIDGES on basename lets docs/other-project/CLAUDE.md bypass the broken-path check even though it is not a bare generated-bridge reference.
  • The cue-word fallback has the same per-path limitation: if the same path string appears more than once in a file, one descriptive mention can suppress another broken occurrence.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hooks/context-drift-check.sh` around lines 78 - 96, The broken-path detection
in context-drift-check.sh is too broad because GENERATED_BRIDGES and the
descriptive-cue fallback are applied by basename or by any same-path match,
which can wrongly skip real bad references. Tighten the skip logic around
REF_PATH/BASENAME so generated-bridge suppression only applies to exact bare
bridge filenames, and make the descriptive cue check evaluate the specific
reference occurrence rather than any matching path elsewhere in the CTX content.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: context-drift-check false positives for generated-bridge, renamed, and external-repo path references

1 participant