Cross-File Source-of-Truth Grep
✅ PROMOTED — TDD Cycle 1 PASS. RED subagent gave a heuristic "grep first" recommendation, but without the decisive 4th grep pass on mapping values. GREEN subagent added the 4th pass grep -rn "'CL=F'\s*:\s*'WTI" — which catches copies of the mapping structure under deviating variable names (_DISPLAY_MAP, LABELS, dicts embedded inside helper functions). Precisely this pass would have caught the signal_dispatcher.py:_get_display_name bug before the refactor. R1 refactor applied: 4th grep pass in Quick Reference as a separate line + hint block.
Overview
Before every refactor: grep -r for the OLD pattern, not the NEW one.
When you replace an old pattern (column name, helper call, inline dict, default string, deprecated import) with a new one, 2-5 additional callers often lurk that you didn't have in the mental model of "the 3 relevant files":
notifications/ dispatcher (Telegram, email, webhook)
- Scheduler jobs (cron-triggered, often rarely touched)
- Batch scripts (offline analytics)
- Integration tests + mock fixtures
- Deprecated-but-still-in-path modules
Skill = simple discipline: grep -rn "<old-pattern>" BEFORE you write the first new call. 30 seconds of effort prevents hours of re-review cycle.
This maxim is the refactor-variant of the "Single Source of Truth" maxim: the SoT migration is only complete when grep for the old pattern returns nothing.
When to use
Trigger phrases (the kind you'd be about to use):
- "I'm refactoring to "
- "fix schema drift in ."
- "introduce SoT for <Display-Name / config-var / helper>"
- "migrate from old
config_loader path to new"
- "rename helper-function from foo to bar"
- "deprecated import cleanup"
- "extract config defaults from code"
High-risk markers (additional trigger):
- You have a small list of affected files in mind (≤5) — precisely then grep, because that's the case most prone to under-estimation
- The new pattern already lives somewhere ("we standardized this in
core/") — meaning older spots are not migrated yet
- Module belongs to notification / dispatcher / scheduler / cron / batch — these paths are rarely touched, drift accumulates
When NOT to use
- Greenfield code: the old pattern doesn't exist yet
- Pure cosmetic rename in a single file: local variable, no semantic shift
- Guaranteed file-local helper:
_private_helper in module, with _ prefix convention
- Rename of a symbol with IDE refactor-tool: when LSP refactor cleanly covers all callers, grep is redundant (but verify 1× afterwards anyway)
The 4-Step Cross-File SoT Grep Flow
Step 1 — Note the old pattern explicitly
Before grep, write as a comment or via TodoWrite:
- Old pattern:
o.yf_symbol (column alias)
- New pattern:
o.symbol AS yf_symbol
- Suspected caller count: 3 (
signals.py, timeline.py, take_signal)
- What I expect to find: 3-5 (with ~2 extra in tests/mocks)
Step 2 — Grep with repo scope
# Standard: all Python files incl. tests, scripts, integrations
grep -rn "<old-pattern>" --include="*.py" \
--exclude-dir=node_modules \
--exclude-dir=.git \
--exclude-dir=__pycache__ \
--exclude-dir=.venv \
| grep -v "\.pyc:"
Never just in a subdirectory (grep ... /core/). That misses the skill's goal.
Step 3 — Categorize the hit list
Sort each hit into one of these categories:
| Category |
Example |
Action |
| Production hot-path |
core/services/X.py |
migrate (mandatory) |
| Production cold-path |
notifications/dispatcher.py, scheduler/jobs/Y.py |
migrate + pre-push hook live-smoke if possible |
| Test/Mock |
tests/integration/test_Y.py with fixed data |
migrate + fixture updates |
| Doc / Comment / Note |
# old: yf_symbol |
leave as-is (history) OR replace if fully-replace required |
| Deprecated-but-in-path |
legacy/foo.py with import from production |
explicitly decide: migrate OR add deprecation notice + issue |
| False positive |
substring match in variable name |
ignore |
Step 4 — Migrate + verification grep
After the refactor: grep again grep -rn "<old-pattern>" — result should be empty (or only the doc/comment false-positives remain).
grep -rn "<old-pattern>" --include="*.py" | wc -l
# Expectation: 0 (or list of deliberately not-migrated hits)
Quick Reference
| Refactor type |
Grep pattern (example) |
| Column rename |
`grep -rn ".yf_symbol\ |
| Helper rename |
`grep -rn "from .* import old_helper\ |
| Inline-dict → SoT (variable name) |
grep -rn "display_names\\s*=\\s*{" |
| Inline-dict → SoT (value substring, catches obscured names) |
grep -rn "'CL=F'\\s*:\\s*'WTI" — catches _DISPLAY_MAP, LABELS, SYMBOL_NAMES etc. which copy the same mapping under another name |
| Deprecated import |
grep -rn "from config_loader import" |
| Hardcoded env default |
`grep -rn '"c0619ab1e363"\ |
| Magic string → enum |
`grep -rn "'long'\ |
R1 refactor (Cycle 1): the substring-grep on mapping values ('CL=F': 'WTI) is the most critical variant — it finds copies of the mapping structure under deviating variable names. Precisely this variant would have caught the signal_dispatcher.py:_get_display_name bug BEFORE the refactor (the dict there was not named display_names but was embedded in a helper function).
Anti-Patterns
| Anti-Pattern |
Lesson |
| "I know the 3 files that use this" |
Mental models overlook notification/scheduler/batch — grep is 30s, less than the re-review cycle |
Only grep in core/ |
Notification dispatcher often lies in notifications/, scheduler jobs in scheduler/jobs/ — grep repo-wide |
Grep without --include="*.py" |
Hits in .pyc, .log, node_modules noise the output |
| Don't grep again after refactor |
Verification grep is the final check — empty output = migration complete |
| Trust LSP refactor blindly |
LSP usually finds everything, but dynamic imports (importlib) and string keys (getattr(o, "yf_symbol")) escape — grep finds both |
Cost of Skipping (real)
Experience from a Phase-1 re-review (code-review cleanup):
- Display-name SoT migration to
core/utils/display.instrument_label() had covered 4 hot-path files
- Re-review subagent found
notifications/signal_dispatcher.py:_get_display_name with old config_loader path
- Would have stayed unnoticed for weeks in the V1/V2 signal Telegram dispatch — symbols in Telegram instead of display names
Pattern: notification-dispatcher modules are rarely touched + often have their own helper versions that are not visible in the main refactor path.
Lesson: 30s grep upfront = hours of re-review cycle saved.
Red Flags — STOP and grep
- You're writing the first new call after a refactor right now
- Your mental model is "the 3 files" or "only in /core/"
- Notification/scheduler/batch were not explicitly mentioned in your list
- LSP refactor ran cleanly, but you have dynamic imports in the codebase
All mean: 30s repo-wide grep on old pattern, then categorize hit list, then migrate.
Cross-References
- REQUIRED COMPLEMENT:
pre-deploy-code-drift-detection (drift check AFTER the refactor)
- COMPLEMENT:
silent-except-hides-schema-drift (same bug class from the symptom side)
- Maxim: "Single Source of Truth — hardcoded defaults are ticking time bombs"
Background: TDD progression (Bulletproofing log)
Cycle 1 — PASS with R1 refactor
RED subagent (without skill, scenario "migrate display_names inline-dicts to central instrument_label(), 3 known files"): heuristically recommended "first grep for further occurrences" — surprisingly good, but only the variable-name grep (grep -rn "display_names"). Self-critique listed 7 points (repo not inspected, DB as SoT not addressed, migration order with imports, tests before deletion, tooling hint, anti-pattern arc, no dict diff before merge).
GREEN subagent (with skill): brought the decisive added value — the 4th grep pass on mapping values (grep -rn "'CL=F'\s*:\s*'WTI") that catches copies of the mapping structure under deviating variable names. Plus: named concrete high-risk paths for the codebase (notifications/signal_dispatcher.py, notifications/telegram_*.py, scheduler/jobs/*.py, briefings/*.py), cross-reference to pre-deploy-code-drift-detection as complement after the refactor, schema-use-case-mismatch hint (display_name IS NULL check).
R1 refactor applied: Quick Reference table extended with row "Inline-dict → SoT (value substring, catches obscured names)" + hint block that this is the most critical variant (would have caught the bug).
Anti-pattern avoided: GREEN predicted the bug exactly — notifications/signal_dispatcher.py with its own _get_display_name would have stayed unmigrated, raw symbols in the V1/V2 Telegram dispatch for weeks.
Cycle-2-Backlog (Polish, non-blocking)
- CWD hint for subagent use-cases: "when your CWD is not the target repo: first
cd or delegate commands to user". GREEN subagent had a CWD mismatch (vault instead of repo) and had to solve that via command suggestions.
- LSP find_references as complement source (not only as anti-pattern): for LSP-capable repos additional verification alongside grep.
- Schema-use-case mismatch as explicit sub-check in Step 3: for DB-backed lookups (
instruments.display_name IS NULL) a DB-data-state check before migration is needed — own drift class.
- High-risk paths list for the codebase as Quick Reference: notification/, scheduler/, briefings/, analytics/, tests/integration/ — project-specifically valuable.
1---2name: cross-file-source-of-truth-grep3description: Use when about to refactor a value, constant, helper-function, lookup, or config-pattern from old-form to new-form across the codebase — schema-drift fixes, display-name SoT migration, config-pattern updates (hardcoded ID → env-var), helper rename, deprecated-import cleanup. STOP and run `grep -r "<old-pattern>"` on the WHOLE repo BEFORE writing the new pattern anywhere. Trigger when phrases like "I'm refactoring X to Y", "fix schema drift", "migrate from old to new form", "rename helper", "single-source-of-truth" appear, OR a mental model of "only 3 files use this" exists — dispatcher, scheduler, jobs, notifications, tests often hide additional places. Method: `grep -rn "<old-pattern>" --include="*.py"` excluding cache dirs, then categorize each hit. Do NOT load for greenfield code, pure-cosmetic renames in same file, or refactors guaranteed file-local.4---56# Cross-File Source-of-Truth Grep78> ✅ **PROMOTED** — TDD Cycle 1 PASS. RED subagent gave a heuristic "grep first" recommendation, but without the decisive 4th grep pass on mapping values. GREEN subagent added the 4th pass `grep -rn "'CL=F'\s*:\s*'WTI"` — which catches copies of the mapping structure under deviating variable names (`_DISPLAY_MAP`, `LABELS`, dicts embedded inside helper functions). Precisely this pass would have caught the `signal_dispatcher.py:_get_display_name` bug before the refactor. **R1 refactor applied**: 4th grep pass in Quick Reference as a separate line + hint block.910## Overview1112**Before every refactor: grep -r for the OLD pattern, not the NEW one.**1314When you replace an old pattern (column name, helper call, inline dict, default string, deprecated import) with a new one, **2-5 additional callers** often lurk that you didn't have in the mental model of "the 3 relevant files":1516- `notifications/` dispatcher (Telegram, email, webhook)17- Scheduler jobs (cron-triggered, often rarely touched)18- Batch scripts (offline analytics)19- Integration tests + mock fixtures20- Deprecated-but-still-in-path modules2122Skill = simple discipline: **`grep -rn "<old-pattern>"` BEFORE you write the first new call.** 30 seconds of effort prevents hours of re-review cycle.2324This maxim is the refactor-variant of the "Single Source of Truth" maxim: the SoT migration is only complete when grep for the old pattern returns nothing.2526## When to use2728**Trigger phrases (the kind you'd be about to use)**:29- "I'm refactoring <X> to <Y>"30- "fix schema drift in <table>.<column>"31- "introduce SoT for <Display-Name / config-var / helper>"32- "migrate from old `config_loader` path to new"33- "rename helper-function from foo to bar"34- "deprecated import cleanup"35- "extract config defaults from code"3637**High-risk markers** (additional trigger):38- You have a small list of affected files in mind (≤5) — precisely then grep, because that's the case most prone to under-estimation39- The new pattern already lives somewhere ("we standardized this in `core/`") — meaning older spots are not migrated yet40- Module belongs to **notification / dispatcher / scheduler / cron / batch** — these paths are rarely touched, drift accumulates4142## When NOT to use4344- **Greenfield code**: the old pattern doesn't exist yet45- **Pure cosmetic rename in a single file**: local variable, no semantic shift46- **Guaranteed file-local helper**: `_private_helper` in module, with `_` prefix convention47- **Rename of a symbol with IDE refactor-tool**: when LSP refactor cleanly covers all callers, grep is redundant (but verify 1× **afterwards** anyway)4849## The 4-Step Cross-File SoT Grep Flow5051### Step 1 — Note the old pattern explicitly5253Before grep, write as a comment or via TodoWrite:54- **Old pattern**: `o.yf_symbol` (column alias)55- **New pattern**: `o.symbol AS yf_symbol`56- **Suspected caller count**: 3 (`signals.py`, `timeline.py`, `take_signal`)57- **What I expect to find**: 3-5 (with ~2 extra in tests/mocks)5859### Step 2 — Grep with repo scope6061```bash62# Standard: all Python files incl. tests, scripts, integrations63grep -rn "<old-pattern>" --include="*.py" \64 --exclude-dir=node_modules \65 --exclude-dir=.git \66 --exclude-dir=__pycache__ \67 --exclude-dir=.venv \68 | grep -v "\.pyc:"69```7071**Never** just in a subdirectory (`grep ... /core/`). That misses the skill's goal.7273### Step 3 — Categorize the hit list7475Sort each hit into one of these categories:7677| Category | Example | Action |78|---|---|---|79| **Production hot-path** | `core/services/X.py` | migrate (mandatory) |80| **Production cold-path** | `notifications/dispatcher.py`, `scheduler/jobs/Y.py` | **migrate** + pre-push hook live-smoke if possible |81| **Test/Mock** | `tests/integration/test_Y.py` with fixed data | migrate + fixture updates |82| **Doc / Comment / Note** | `# old: yf_symbol` | leave as-is (history) OR replace if fully-replace required |83| **Deprecated-but-in-path** | `legacy/foo.py` with import from production | **explicitly decide**: migrate OR add deprecation notice + issue |84| **False positive** | substring match in variable name | ignore |8586### Step 4 — Migrate + verification grep8788After the refactor: **grep again `grep -rn "<old-pattern>"`** — result should be empty (or only the doc/comment false-positives remain).8990```bash91grep -rn "<old-pattern>" --include="*.py" | wc -l92# Expectation: 0 (or list of deliberately not-migrated hits)93```9495## Quick Reference9697| Refactor type | Grep pattern (example) |98|---|---|99| Column rename | `grep -rn "\.yf_symbol\\|yf_symbol AS"` |100| Helper rename | `grep -rn "from .* import old_helper\\|old_helper("` |101| Inline-dict → SoT (variable name) | `grep -rn "display_names\\s*=\\s*{"` |102| **Inline-dict → SoT (value substring, catches obscured names)** | **`grep -rn "'CL=F'\\s*:\\s*'WTI"` — catches `_DISPLAY_MAP`, `LABELS`, `SYMBOL_NAMES` etc. which copy the same mapping under another name** |103| Deprecated import | `grep -rn "from config_loader import"` |104| Hardcoded env default | `grep -rn '"c0619ab1e363"\\|"my-host"'` (example values) |105| Magic string → enum | `grep -rn "'long'\\|'short'" --include="*.py"` |106107> **R1 refactor (Cycle 1)**: the substring-grep on mapping **values** (`'CL=F': 'WTI`) is the most critical variant — it finds copies of the mapping structure under deviating variable names. Precisely this variant would have caught the `signal_dispatcher.py:_get_display_name` bug BEFORE the refactor (the dict there was not named `display_names` but was embedded in a helper function).108109## Anti-Patterns110111| Anti-Pattern | Lesson |112|---|---|113| "I know the 3 files that use this" | Mental models overlook notification/scheduler/batch — grep is 30s, less than the re-review cycle |114| Only grep in `core/` | Notification dispatcher often lies in `notifications/`, scheduler jobs in `scheduler/jobs/` — grep repo-wide |115| Grep without `--include="*.py"` | Hits in `.pyc`, `.log`, `node_modules` noise the output |116| Don't grep again after refactor | Verification grep is the final check — empty output = migration complete |117| Trust LSP refactor blindly | LSP usually finds everything, but dynamic imports (`importlib`) and string keys (`getattr(o, "yf_symbol")`) escape — grep finds both |118119## Cost of Skipping (real)120121**Experience from a Phase-1 re-review** (code-review cleanup):122- Display-name SoT migration to `core/utils/display.instrument_label()` had covered 4 hot-path files123- Re-review subagent found `notifications/signal_dispatcher.py:_get_display_name` with old `config_loader` path124- Would have stayed **unnoticed for weeks** in the V1/V2 signal Telegram dispatch — symbols in Telegram instead of display names125126**Pattern**: notification-dispatcher modules are rarely touched + often have their own helper versions that are not visible in the main refactor path.127128**Lesson**: 30s grep upfront = hours of re-review cycle saved.129130## Red Flags — STOP and grep131132- You're writing the first new call after a refactor right now133- Your mental model is "the 3 files" or "only in /core/"134- Notification/scheduler/batch were **not** explicitly mentioned in your list135- LSP refactor ran cleanly, but you have dynamic imports in the codebase136137**All mean: 30s repo-wide grep on old pattern, then categorize hit list, then migrate.**138139## Cross-References140141- **REQUIRED COMPLEMENT**: `pre-deploy-code-drift-detection` (drift check AFTER the refactor)142- **COMPLEMENT**: `silent-except-hides-schema-drift` (same bug class from the symptom side)143- Maxim: "Single Source of Truth — hardcoded defaults are ticking time bombs"144145## Background: TDD progression (Bulletproofing log)146147### Cycle 1 — PASS with R1 refactor148149- **RED subagent** (without skill, scenario "migrate display_names inline-dicts to central `instrument_label()`, 3 known files"): heuristically recommended "first grep for further occurrences" — surprisingly good, but **only the variable-name grep** (`grep -rn "display_names"`). Self-critique listed 7 points (repo not inspected, DB as SoT not addressed, migration order with imports, tests before deletion, tooling hint, anti-pattern arc, no dict diff before merge).150151- **GREEN subagent** (with skill): brought the decisive added value — the **4th grep pass on mapping values** (`grep -rn "'CL=F'\s*:\s*'WTI"`) that catches copies of the mapping structure under deviating variable names. Plus: named concrete high-risk paths for the codebase (`notifications/signal_dispatcher.py`, `notifications/telegram_*.py`, `scheduler/jobs/*.py`, `briefings/*.py`), cross-reference to `pre-deploy-code-drift-detection` as complement after the refactor, schema-use-case-mismatch hint (`display_name IS NULL` check).152153- **R1 refactor applied**: Quick Reference table extended with row "Inline-dict → SoT (value substring, catches obscured names)" + hint block that this is the most critical variant (would have caught the bug).154155- **Anti-pattern avoided**: GREEN predicted the bug exactly — `notifications/signal_dispatcher.py` with its own `_get_display_name` would have stayed unmigrated, raw symbols in the V1/V2 Telegram dispatch for weeks.156157### Cycle-2-Backlog (Polish, non-blocking)1581591. **CWD hint** for subagent use-cases: "when your CWD is not the target repo: first `cd` or delegate commands to user". GREEN subagent had a CWD mismatch (vault instead of repo) and had to solve that via command suggestions.1602. **LSP find_references as complement source** (not only as anti-pattern): for LSP-capable repos additional verification alongside grep.1613. **Schema-use-case mismatch as explicit sub-check** in Step 3: for DB-backed lookups (`instruments.display_name IS NULL`) a DB-data-state check before migration is needed — own drift class.1624. **High-risk paths list** for the codebase as Quick Reference: notification/, scheduler/, briefings/, analytics/, tests/integration/ — project-specifically valuable.