# General Standards

Runtime versions, package policy, source layout, the API response envelope, and GitHub Actions pins — the workspace baseline.

## Source Layout

How a package's `src/` is laid out. A single-runtime package drops `server/` and `shared/`.

| Folder | Intent — what it protects | Belongs here when |
| --- | --- | --- |
| `app/` | The frame that survives navigation — one router, one auth gate, one shell. The property is **instance count, not importance**; without it every route reinvents the chrome, and the shell becomes where "general" code hides. | Mounted once, present on every page |
| `surfaces/` | An address maps to one deletable folder holding its page, panels, client and private `lib/`. **One folder per route outranks file count** — a one-file route keeps its folder, because the value is that the folder list *is* the route list. | A routable destination |
| `components/` | UI shared across the app, **and the known place anyone looks before building** — the second job is the load-bearing one, because a component nobody finds gets rebuilt and the app grows three of it that disagree. | A component importing no route folder |
| `lib/` | The client's non-visual work — API client, session state, ids, timestamps, hooks. | Non-React logic only the client uses |
| `shared/` | The contracts and domain logic both runtimes read, so the halves cannot drift; without it the contract is stated twice and diverges on the first change nobody propagates. | Client **and** server both import it |
| `server/` | The other runtime as a peer in the same package — what lets a contract be a file both sides import instead of a shape both sides guess. | Runs on the server runtime |

**Split by role, not by feature** — a flat `src/leads/ src/accounts/…` trades one level of depth for unbounded root breadth, and the top-level scan is what keeps a repo legible. The container **bounds** rather than classifies, so `routes/` is an equally valid name for `surfaces/` — use the word the codebase already speaks.

**Never `core/`** — its membership test collapses to "is it important?", making it the queue for files nobody could place; a bare `lib/` holding everything non-visual fails the same way, which is what `shared/` prevents. A route's own nested `lib/` is private and is **never** hoisted, and **dependencies point down** — a shared folder importing one route folder is a defect.

**A folder earns its place at ~4+ files, or by being a boundary somebody could delete whole.** Below that, inline it. The boundary clause splits the one-file cases: a one-file route folder stays, a one-file component folder goes.

**Never judge dead code by a name grep** — a relative import never contains its own parent's path, so every folder imported only by its parent reads as dead. Test by transitive reachability from the real entry points, resolving specifiers as the bundler does (`.ts`, `.tsx`, `.css`, `/index.ts`; strip `?raw`/`?url`; map a `.js` specifier onto its `.ts` source), with the test tree as a **second, separate** entry set — reachable from tests alone is a suite pinning code nothing ships. Ambient `.d.ts` files enter through tsconfig `include`, so a sweep always reports them dead; grep the build config for a retired mechanism before calling its removal complete. **Unreachable ≠ unwanted**: retiring a routed dummy surface removes a route — a product decision.

**Reshape an existing tree largest-lever-first**, verifying typecheck plus the full suite between steps: purge unreachable files → collapse single-occupancy and pass-through folders → push domain-coupled components down → name the shared layer (the only step that adds a top-level name) → optionally add a path alias (depth becomes invisible at the import site). **Delete and promote before you move** — a folder whose name misdescribes its contents just relocates the lie.

## Runtime Versions

- **Node.js:** the version pinned in `.nvmrc` / `package.json` `engines` — never a hardcoded number anywhere.
- **Package manager:** detect from the lockfile — **never** hardcode one.
- **PostgreSQL:** 17 · **Redis:** 7.x (latest)

## Packages

**The approved table is `.claude/hooks/approved-packages.md`**, enforced by the PreToolUse install gate. It records three states, and the gate treats them differently:

| The package is… | What happens |
| --- | --- |
| in the **Package** column | installs |
| in a **Don't use** column | **blocked**, and the block names that row's replacement — `moment` answers "use `date-fns`" |
| in neither | **installs.** No decision exists, so there is nothing to enforce |

A rejection is a decision and binds; an absence is not one and never blocks work. Unlisted packages are harvested across the fleet by the package census, and become rows — or bans — by explicit user decision. **Never edit the table to unblock yourself**; a rejection changes only on the user's say-so.

Two **dev-only** installs are exempt even from the rejection check being reached: a scoped companion of an approved package (`@testing-library/react` is on the table, so `-D @testing-library/user-event` needs no row of its own) and `@types/*`.

## API Response Envelope

Success and error share **one** envelope — a cross-service contract clients branch on, so a per-service "improvement" (`success: true`, a boolean `status`, a bare payload) breaks every consumer:

```json
{
  "status": 1,
  "message": "Message sent successfully",
  "data": { "delivered": [{ "user@example.com": 2 }] }
}
```

Error: `{ "status": 0, "message": "Error description", "data": null }`

HTTP codes, default list: `200`, `201` (created), `400`, `401` (not authenticated), `403` (not authorized), `404`, `500` — plus `204`/`304` (null-body responses), `409` (conflicting write), `429` (rate limit), `503` (unhealthy dependency) where the semantics call for them. Middleware- and protocol-emitted codes are exempt from the envelope.

## GitHub Actions

Pin the major, and check it against the action's releases — a major behind still runs, so a stale pin never fails, it only warns.

| Action | Version | Use |
| --- | --- | --- |
| `actions/checkout` | v7 | Repo checkout |
| `actions/setup-node` | v7 | Node toolchain + npm cache |

Set `node-version` via `node-version-file: .nvmrc`, **never** the runner's default.
