Writing Discoverable Code
Markdown--- name: writing-discoverable-code description: Refactors and writes code so coding agents can find it via grep/ripgrep-based search — specific multi-word names, precise types instead of any, comments on definitions, and one spelling per concept. Use when naming functions/variables/files, reviewing code for AI-agent readability, refactoring generic or ambiguous identifiers, splitting large monolithic files, or setting up naming conventions in AGENTS.md/CLAUDE.md. --- Coding agents navigate repositories almost entirely via text search (grep/ripgrep), not compilers or dependency graphs. A name is a search term. Generic names (`create`, `data`, `Config`, `utils.ts`) return hundreds of irrelevant hits and burn the agent's context ruling them out. Specific names (`createStripeClient`, `NotificationDeliveryConfig`) resolve in one hop.
Before/after for a single function:
TypeScript// Before — greps against hundreds of unrelated hits export function create(apiKey: string) { ... } function process(data) { ... } // After — greps to just its own definition + call sites export function createStripeClient(apiKey: string): StripeClient { ... } function enrichUserProfile(user: User): EnrichedUserProfile { ... }
Test any existing name before renaming:
Bashrg -w "functionName" --type <lang> -c | wc -l # 1-3 files = good (unique). 10+ files = too generic, needs a domain word.
Progress checklist for refactoring a file, module, or whole codebase:
- Step 1: Inventory exported symbols (functions, classes, types, constants) and file/directory names
- Step 2: Grep-test each name's uniqueness (
rg -w "<name>" -c | wc -l) - Step 3: Rename generic names to 2-3 word, domain-qualified names
- Step 4: Replace
any/ untyped params with precise types; introduce branded/newtypes for easily-confused primitives (IDs, units) - Step 5: Split god-files/god-functions into modules named after the concept they contain
- Step 6: Move explanatory comments onto definitions (not call sites)
- Step 7: Consolidate synonyms — pick one spelling per concept across the codebase
- Step 8: Mark legacy/unused code paths
@deprecatedor remove them - Step 9: Record conventions in AGENTS.md / CLAUDE.md
- Step 10: Re-run grep tests to confirm each renamed symbol now resolves to ≤3 files
Step details
Step 1-2: Find the generic names.
Bash# List exported symbols, then check each for ambiguity rg -w "create|get|process|handle|data|result|config|utils" --type ts -c
Anything matching 10+ files across unrelated concerns is a candidate.
Step 3: Naming rule of thumb.
| Words | Uniqueness | Verdict |
|---|---|---|
1 (get) | ~61% unique | Too generic |
2 (getUser) | ~88% unique | Borderline |
3 (getUserProfile) | ~96% unique | Good |
4+ (getUserProfileById) | ~98% unique | Great, don't overdo it |
Target 2-3 words per exported name, with at least one domain word (e.g. Stripe, Notification, User) — not generic verbs/nouns alone. In qualified-call languages (Go, Java with packages), the package/namespace counts as one word.
Step 4: Types as search anchors.
- Replace
any/untyped params with real interfaces/types — the type name itself becomes a grep target. - Use branded/newtypes for primitives that shouldn't mix:
UserId,OrgId,ProjectIdinstead of threestringparams. This turns argument-order bugs into compiler errors that name the exact type to search for. - Precise return types (
EnrichedUsernotany/object) let agents skip reading the implementation entirely.
Step 5: File/module splitting. Large files with many unrelated concerns (e.g. one file with 80 nested route handlers) should be split into files named after the concept, not the mechanism:
Before: routes.py (all handlers, one giant function)
After: smtp_settings.py, message_rendering.py, folder_fallbacks.py, delivery_history.py
Directory/file names are search terms exactly like symbol names — an agent will rg --files | rg -i 'session-broker' before reading anything.
Step 6: Comments belong on definitions. Agents reach code by searching a name, and that search resolves to the definition site. Put the one sentence explaining why directly above the definition — not at the call site, not in a separate doc that won't surface in the same search.
Step 7: One spelling per concept.
Don't alternate organizations/customers, or create/build/make for the same operation across files. Consistent vocabulary means one search finds everything.
Step 8: Deprecate, don't silently keep.
TypeScript/** @deprecated Use createStripeClient instead. Removed in v3. */ export function create(apiKey: string) { ... }
Unmarked legacy code will be discovered and reused by agents as if it were current.
Step 9: Write it down. Add a section to AGENTS.md/CLAUDE.md covering: naming conventions, where things live, deprecated paths, and any behavior that's deliberately absent (grep can't find the absence of code — e.g. "we do NOT sanitize inbound HTML here, see X for why").
Example 1: Ambiguous client function Input:
TypeScriptexport function create(apiKey: string) { ... }
Output:
TypeScript/** Creates a client for the Stripe Payments API. */ export function createStripeClient(apiKey: string): StripeClient { ... }
rg 'create' → 1,585 hits across 459 files. rg 'createStripeClient' → 43 hits across 19 files, all relevant.
Example 2: Untyped ID params Input:
TypeScriptfunction transferOwnership(userId: string, orgId: string, projectId: string) { ... } transferOwnership(orgId, projectId, userId); // silent bug
Output:
TypeScriptfunction transferOwnership(userId: UserId, orgId: OrgId, projectId: ProjectId) { ... } // swapped args now fail to compile: // Argument of type 'OrgId' is not assignable to parameter of type 'UserId'.
Example 3: God-file split
Input: email_routes.py — 4,943 lines, 81 route handlers nested in one function.
Output: email/smtp_settings.py, email/message_rendering.py, email/folder_fallbacks.py, email/delivery_history.py — each named after the concept it implements. Observed effect: agents that previously burned 290k+ tokens per question dropped to ~190k, with catastrophic no-answer sessions nearly eliminated.
- Grep-test names before and after renaming (
rg -w "<name>" -c | wc -l); aim for ≤3 files. - Prefer 2-3 word names with a domain term over single generic verbs.
- Use branded/newtypes for IDs and easily-confused primitives so the compiler catches misuse.
- Never leave params/returns as
any— it removes both the type-as-search-target and the compiler's ability to help. - Name test files after the source they cover (
stripe.test.ts↔stripe.ts). - Put the explanatory comment on the definition, not the call site.
- Consolidate on one term per concept across the whole codebase.
- Explicitly document intentional absences of behavior — agents can't grep for what isn't there.
- Record conventions in AGENTS.md/CLAUDE.md; it's the highest-leverage single artifact for agent navigation.
- Renaming without grep-testing. A "better" name that still matches 50 files didn't solve anything — verify uniqueness after renaming.
- Over-specifying names. 5+ word names (e.g.
notificationRetryBackoffCalculationHelper) add noise without improving uniqueness meaningfully; 2-3 words is usually the sweet spot. - Relying on imports/modules for reverse lookup. Imports point forward (call site → definition) but grep has no reverse index; barrels (
export * from './x') and package specifiers actively hide the real definition location. Distinctive names are the only reliable reverse lookup. - Assuming
anyis harmless because "it's just a type." It removes both the compiler's error-catching and the agent's ability to skip reading the implementation. - Splitting files without renaming them meaningfully. A refactor that produces
helpers.pyorutils2.pyrecreates the same generic-name problem in a new location. - Leaving deprecated code unmarked. Agents will find and use it as if it were current, especially if it has a shorter/simpler name than the replacement.
- Forgetting synonyms.
orgIdvsorganizationId,customervsorganization— pick one and use it everywhere.