---
name: forge-naming
description: Naming variables, functions, types, files, tables. Ban list for AI defaults (`data`, `Manager`, `Helper`, `utils.ts`), domain-specific over abstract, length proportional to scope, units in names where ambiguous, language-specific conventions. Contains lookup tables for each language. Use whenever you are introducing a new identifier - it sticks for the lifetime of the codebase.
license: MIT
---

# forge-naming

You are naming things. Default agent-written code overuses `data`, `info`, `result`, `manager`, `handler`, `helper`, `utils`, `Service`, `Util`, `Common` - all empty calories. This skill exists to push toward concrete names that survive.

The mental model: **a name is the only thing about a symbol that future-you sees in a long file scan.** The name does the work of communicating intent. Bad names cost reading time forever; good names pay forward.

## Quick reference (the things you must never ship)

1. A variable named `data`, `info`, `obj`, `item`, `value`, `result`, or `payload` outside a 5-line scope.
2. A class ending in `Manager`, `Helper`, `Util`, `Wrapper`, or bare `Service`.
3. A file named `utils.ts`, `helpers.ts`, `common.ts`, `misc.ts`, `lib.ts`.
4. Hungarian notation: `strName`, `iCount`, `bFlag`.
5. Numbered generic names: `data1`, `data2`, `item3`.
6. Boolean prefixed with negation: `notArchived`, `isNotActive`.
7. Abbreviation that saves <3 keystrokes: `usr`, `pwd`, `addr`.
8. File and exported symbol naming disagree (e.g. `Card.tsx` exports `Foo`).
9. `User | null` where you mean "lookup failed" (use Result).
10. Singular/plural confusion: variable `users` holding one user.

## Hard rules

### Banned generic names

**1. `data`, `info`, `payload`, `obj`, `item`, `value`, `result`, `output`, `response`** - all meaningless outside trivial scope. The variable holds *something*; name what it actually is.

```ts
// BAD
const data = await db.users.findById(id);
const result = transform(data);

// GOOD
const userProfile = await db.users.findById(id);
const normalizedProfile = transform(userProfile);
```

**2. `Manager`, `Handler`, `Helper`, `Util`, `Wrapper`, bare `Service`.** These suffixes promise nothing. `OrderManager` is `Order` plus tax. `OrderRepository` or `OrderCharger` says what it does.

```ts
// BAD
class OrderManager {
  doStuff(o: Order) { ... }
}

// GOOD
class OrderRepository {
  findById(id: OrderId): Promise<Order | null> { ... }
}
class OrderCharger {
  charge(order: Order): Promise<Result<Charge, ChargeError>> { ... }
}
```

**3. `temp`, `tmp`, `foo`, `bar`, `thing`.** Anywhere outside a 5-line scratch.

**4. `Common`, `Misc`, `Shared`, `Lib`, `Utils`** as folder/file names. They become trash drawers. Name folders by domain (`auth/`, `billing/`) or by capability (`pdf-rendering/`).

### Length proportional to scope

**5. Length proportional to scope.** Loop variable: `i`. Module-level constant: longer.

```ts
// fine - tiny scope
for (let i = 0; i < users.length; i++) { ... }

// not fine - module-level
export const i = 42;   // ???
export const MAX_RETRIES = 3;
```

**6. Abbreviate only well-known terms.** `db`, `url`, `id`, `ip`, `cpu`. Not `usr`, `pwd`, `addr`.

### Specificity

**7. Domain over abstraction.** `chargeAmountCents` beats `amount`. In a billing system, `email` is too ambiguous; in a contact form, fine.

**8. Verbs for functions.** `getUser`, `createOrder`. Nouns are wrong: `user()` reads like an attribute.

**9. Boolean prefixes:** `is`, `has`, `can`, `should`. `isActive`, `hasShipped`, `canEdit`, `shouldRetry`.

**10. Plural for collections, singular for items.** `for (const user of users)`.

### Conventions per language

#### TypeScript / JavaScript

| Kind | Convention | Example |
| --- | --- | --- |
| Type | PascalCase | `OrderRow`, `UserProfile` |
| Variable, function | camelCase | `chargeAmount`, `findUser` |
| Constant (truly global) | SCREAMING_SNAKE | `MAX_RETRIES` |
| File | kebab-case OR camelCase, pick one project-wide | `order-charger.ts` |
| React component | PascalCase, file matches | `UserCard.tsx` exports `UserCard` |

#### Python

| Kind | Convention | Example |
| --- | --- | --- |
| Variable, function | snake_case | `charge_amount`, `find_user` |
| Class | PascalCase | `OrderRow` |
| Constant | SCREAMING_SNAKE | `MAX_RETRIES` |
| Private | `_leading_underscore` | `_internal_helper` (no `__` unless you understand name mangling) |

#### Go

| Kind | Convention | Example |
| --- | --- | --- |
| Exported (public) | PascalCase | `ChargeOrder` |
| Unexported (private) | camelCase | `chargeOrder` |
| Acronyms | Keep case | `userID` not `userId`, `parseURL` not `parseUrl` |
| Receiver | 1-2 letters, consistent per type | `(u *User)` always |

#### Rust

| Kind | Convention | Example |
| --- | --- | --- |
| Variable, function, module | snake_case | `charge_amount` |
| Type, trait | PascalCase | `OrderRow` |
| Constant | SCREAMING_SNAKE | `MAX_RETRIES` |
| Lifetime | lowercase letter | `'a`, `'src` |

#### SQL / databases

| Kind | Convention | Example |
| --- | --- | --- |
| Table | plural, snake_case | `users`, `order_items` |
| Column | snake_case, no table prefix | `name`, NOT `user_name` |
| Foreign key | `<other_table_singular>_id` | `user_id` |
| Boolean | positive predicate | `is_active`, NOT `not_archived` |
| Index | `idx_<table>_<columns>` | `idx_users_email_active` |

### Negation

**11. Avoid negative names where positive works.** `isEnabled` beats `isDisabled`. Double negatives (`if (!isDisabled)`) are confusing.

**12. Empty/missing states:** `isEmpty`, `notFound`, `missing` over `not-not-X`.

### Time and quantity

**13. Units in names for ambiguous quantities.** `timeoutMs`, `maxSizeBytes`, `priceCents`. Saves bugs from unit mismatches.

**14. Dates use ISO-style names where ambiguity is possible.** `startDate` is fine; `dateFormat` is vague (which format?).

### Symmetry

**15. Pair opposing operations with opposing words.** `start/stop`, `enable/disable`, `open/close`, `create/delete`. Mixing `start/end` and `open/close` for the same thing in one codebase is friction.

**16. Same level of abstraction in the same family.** `getUser`, `getOrder`, `getInvoice` fine. `getUser`, `fetchOrder`, `loadInvoice` not.

### File names

**17. One default-exported thing per file. Name the file after the thing.** `UserProfile.tsx` exports `UserProfile`.

**18. Tests co-located.** `user-profile.test.ts` next to `user-profile.ts`. Separate `tests/` folders make navigation worse.

**19. No `index.ts` re-exports unless the public API genuinely needs them.** Generic `index.ts` files are how naming intent gets lost.

### Naming after refactor

**20. When responsibility shifts, the name shifts too.** If `UserManager` becomes "user repository + user notifier," it is two classes now.

**21. Renaming has cost - do it deliberately.** A good rename is worth it; constant renames are churn.

## Common AI-output patterns to reject

| Pattern | Why wrong | Fix |
| --- | --- | --- |
| `const data = ...` | Meaningless | Domain name: `userProfile`, `orderRow` |
| `class FooManager` | Empty suffix | `FooRepository`, `FooCharger`, `FooNotifier` |
| `utils.ts` | Trash drawer | Name by domain: `strings.ts`, `dates.ts` (or split into the things they hold) |
| `data1`, `data2`, `data3` | Numbered generic | Name each by what it is |
| `strName: string` | Hungarian | Just `name` |
| `usr`, `pwd`, `addr` | Tiny saving, big ambiguity | `user`, `password`, `address` |
| Folder `Common/` | Trash drawer | Move pieces into domain folders |
| `let timeout = 5000` | Unit unclear | `let timeoutMs = 5000` |
| Function returns `User \| null` for not-found | Confuses not-found + lookup-failed | Return `Result<User, "not_found">` |
| `class AbstractWidget` | Abstract suffix | Just `Widget`, or rename to describe what it is |

## Worked example: a refactor by renaming

```ts
// BEFORE
class UserManager {
  async getUser(id: string): Promise<any | null> {
    const data = await this.db.query("SELECT * FROM users WHERE id = $1", [id]);
    return data.rows[0] || null;
  }
  async doStuff(id: string, password: string): Promise<boolean> {
    const usr = await this.getUser(id);
    if (!usr) return false;
    const result = crypto.createHash("sha256").update(password).digest("hex");
    return usr.password_hash === result;
  }
}

// AFTER
class UserRepository {
  async findById(id: UserId): Promise<User | null> {
    const result = await this.db.query<UserRow>(
      "SELECT * FROM users WHERE id = $1 AND deleted_at IS NULL",
      [id],
    );
    return result.rows[0] ?? null;
  }
}

class PasswordVerifier {
  async verify(userId: UserId, candidatePassword: string): Promise<boolean> {
    const user = await this.users.findById(userId);
    if (!user) return false;
    return argon2.verify(user.password_hash, candidatePassword);
  }
}
```

What changed:
- `UserManager` → split into `UserRepository` (data access) + `PasswordVerifier` (one action).
- `getUser` → `findById` (matches typical repo pattern, name implies the lookup criterion).
- `any | null` → `User | null` (and `User` is a typed interface, not implicit).
- `data` → `result` (acceptable here as immediate-scope DB query result; `result.rows[0]` is named correctly by pg's API).
- `usr` → `user`.
- `result` (the hash) → method extracted entirely; argon2 used (also `forge-secrets` rule 10).
- `password_hash` parameter name kept consistent with DB column.

## Workflow

When introducing a new name:

1. **Say what the thing is in one phrase.** "The list of orders that failed to charge."
2. **Compress to a name.** `failedCharges`, `unchargedOrders`. Pick the one that reads in a sentence.
3. **Test against the codebase.** Already a similar name? Stay consistent.
4. **Read it in context.** `for (const failedCharge of failedCharges)` - reads naturally.
5. **Look for smell.** "Hmm, this is a `Manager`" → "what does it actually manage?" Often the answer reveals the better name.

## Verification

```bash
bash skills/dx/forge-naming/verify/check_naming.sh path/to/file
```

Flags: banned generic names (`data`, `info`, `obj`), unhelpful suffixes (`Manager`, `Helper`, `Util` as bare class names), Hungarian-style prefixes, file names like `utils.ts`, `helpers.ts`, `common.ts`.

## When to skip this skill

- Generated code (no point linting names that get regenerated).
- One-line scripts and REPL exploration.
- Established codebases with a different, internally-consistent convention. Match the codebase first.

## Related skills

- [`forge-typescript`](../../backend/forge-typescript/SKILL.md) - branded types as a kind of name.
- [`forge-readme`](../../docs/forge-readme/SKILL.md) - how to talk about the names you chose.
- [`forge-schema-design`](../../data/forge-schema-design/SKILL.md) - DB-table naming conventions.
