From 263f6569c9d685244b71474cefc8f043bada5b4a Mon Sep 17 00:00:00 2001 From: Nathan Schram <5553883+nathanschram@users.noreply.github.com> Date: Sat, 4 Jul 2026 14:27:05 +1000 Subject: [PATCH] fix: suppress context-drift-check false positives for generated-bridge, 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) --- .claude/hooks/context-drift-check.sh | 30 ++++++++++++++++++++++-- CHANGELOG.md | 1 + hooks/context-drift-check.sh | 30 ++++++++++++++++++++++-- tests/test-hooks.sh | 35 ++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 4 deletions(-) diff --git a/.claude/hooks/context-drift-check.sh b/.claude/hooks/context-drift-check.sh index 1d57e1e..cc46e72 100755 --- a/.claude/hooks/context-drift-check.sh +++ b/.claude/hooks/context-drift-check.sh @@ -38,6 +38,18 @@ fi CONTEXT_FILES=("CLAUDE.md" "AGENTS.md" "GEMINI.md" ".cursorrules" ".github/copilot-instructions.md" "llms.txt" ".windsurfrules" ".clinerules") +# Generated-bridge filenames the plugin emits into *other* projects. A context +# file may list these as outputs without them existing in this repo, so a +# missing local copy is not drift. (Basename match.) +GENERATED_BRIDGES=("GEMINI.md" "AGENTS.md" "CLAUDE.md" "llms.txt" "llms-full.txt" + ".windsurfrules" ".cursorrules" ".clinerules" + "copilot-instructions.md" "agents.mdc" "agents.md") + +# Same-line cue words that mark a reference as descriptive rather than a live +# local path: rename history ("renamed from `index.md`") or a file in another +# repo ("the marketing-site `scripts/docs-sync.config.ts` mapping"). +DESCRIPTIVE_CUE='renamed|formerly|previously|used to be|marketing[- ]site|external repo|upstream repo|other repo' + STALE=() BROKEN_PATHS=() @@ -63,9 +75,23 @@ for CTX in "${CONTEXT_FILES[@]}"; do # Quick broken-path check: extract backtick-quoted file references while IFS= read -r REF_PATH; do if [ -n "$REF_PATH" ] && [ ! -e "$REF_PATH" ]; then - # Fallback: check if basename exists anywhere in repo (tracked or untracked) 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)") fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 55c46b4..bf45b4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* Context Guard drift check no longer reports false positives for backtick references to generated-bridge files (e.g. `GEMINI.md`), historical rename mentions ("renamed from `index.md`"), or paths in other repos ("marketing-site `scripts/docs-sync.config.ts`") ([#31](https://github.com/littlebearapps/contextdocs/issues/31)) * Context Guard hooks no longer flag infrastructure rule files as false positives ([#13](https://github.com/littlebearapps/contextdocs/issues/13)) * ai-context skill triggers more reliably when you describe stale context or MEMORY.md promotion scenarios * SKILL.md and CLAUDE.md descriptions now match the canonical product definition across all tools diff --git a/hooks/context-drift-check.sh b/hooks/context-drift-check.sh index 1d57e1e..cc46e72 100755 --- a/hooks/context-drift-check.sh +++ b/hooks/context-drift-check.sh @@ -38,6 +38,18 @@ fi CONTEXT_FILES=("CLAUDE.md" "AGENTS.md" "GEMINI.md" ".cursorrules" ".github/copilot-instructions.md" "llms.txt" ".windsurfrules" ".clinerules") +# Generated-bridge filenames the plugin emits into *other* projects. A context +# file may list these as outputs without them existing in this repo, so a +# missing local copy is not drift. (Basename match.) +GENERATED_BRIDGES=("GEMINI.md" "AGENTS.md" "CLAUDE.md" "llms.txt" "llms-full.txt" + ".windsurfrules" ".cursorrules" ".clinerules" + "copilot-instructions.md" "agents.mdc" "agents.md") + +# Same-line cue words that mark a reference as descriptive rather than a live +# local path: rename history ("renamed from `index.md`") or a file in another +# repo ("the marketing-site `scripts/docs-sync.config.ts` mapping"). +DESCRIPTIVE_CUE='renamed|formerly|previously|used to be|marketing[- ]site|external repo|upstream repo|other repo' + STALE=() BROKEN_PATHS=() @@ -63,9 +75,23 @@ for CTX in "${CONTEXT_FILES[@]}"; do # Quick broken-path check: extract backtick-quoted file references while IFS= read -r REF_PATH; do if [ -n "$REF_PATH" ] && [ ! -e "$REF_PATH" ]; then - # Fallback: check if basename exists anywhere in repo (tracked or untracked) 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)") fi diff --git a/tests/test-hooks.sh b/tests/test-hooks.sh index 96b4ce4..d3885f7 100755 --- a/tests/test-hooks.sh +++ b/tests/test-hooks.sh @@ -429,6 +429,41 @@ run_test "Stale llms.txt → drift detected" "$HOOK" \ '{"tool_name":"Bash","tool_input":{"command":"git commit -m test"}}' 0 "AI CONTEXT DRIFT DETECTED" teardown_git_repo +echo "" +echo "--- Broken-path false-positive suppression (issue #31) ---" + +# Generated-bridge filename referenced but absent locally → not drift +setup_git_repo +printf '%s\n' 'Thin bridges include `GEMINI.md` for Gemini CLI.' > "$TEMP_REPO/CLAUDE.md" +git add CLAUDE.md && git commit -q -m "add context" +run_test "Generated-bridge ref (GEMINI.md) → not flagged" "$HOOK" \ + '{"tool_name":"Bash","tool_input":{"command":"git commit -m test"}}' 0 "{}" +teardown_git_repo + +# Rename-history mention on the same line → not drift +setup_git_repo +printf '%s\n' 'The FAQ was renamed from `index.md` to faq.md.' > "$TEMP_REPO/CLAUDE.md" +git add CLAUDE.md && git commit -q -m "add context" +run_test "Rename mention (index.md) → not flagged" "$HOOK" \ + '{"tool_name":"Bash","tool_input":{"command":"git commit -m test"}}' 0 "{}" +teardown_git_repo + +# External-repo path flagged by cue word → not drift +setup_git_repo +printf '%s\n' 'Synced by the marketing-site `scripts/build.ts` mapping.' > "$TEMP_REPO/CLAUDE.md" +git add CLAUDE.md && git commit -q -m "add context" +run_test "External-repo path (marketing-site) → not flagged" "$HOOK" \ + '{"tool_name":"Bash","tool_input":{"command":"git commit -m test"}}' 0 "{}" +teardown_git_repo + +# Regression guard: a genuinely missing repo-local path is still flagged +setup_git_repo +printf '%s\n' 'See `src/missing.ts` for the handler.' > "$TEMP_REPO/CLAUDE.md" +git add CLAUDE.md && git commit -q -m "add context" +run_test "Genuinely broken ref (src/missing.ts) → still flagged" "$HOOK" \ + '{"tool_name":"Bash","tool_input":{"command":"git commit -m test"}}' 0 "not found" +teardown_git_repo + echo "" ########################################################################