# New Module Workflow

Authoritative source: [`docs/adr/014-module-ownership-and-internal-boundaries.md`](../adr/014-module-ownership-and-internal-boundaries.md).

## The golden path

```bash
npm run new:module -- <name> [--with-route] [--with-ui] [--owner <name>]
```

This scaffolds a new module under `lib/<name>/` with:

```
lib/<name>/
├── index.ts          ← public surface (only file other modules may import)
├── README.md         ← what / why / owner / public API / dependencies
├── internal/
│   └── .gitkeep      ← private implementation; not importable from outside
└── <name>.test.ts    ← Vitest entry point
```

Optional flags:

- `--with-route` adds `app/api/<name>/route.ts` stub
- `--with-ui` adds `components/<name>/<name>-panel.tsx` stub
- `--owner <name>` writes the owner into the README front-matter

## Public surface rule

The only thing other modules may import from your module is what `index.ts`
re-exports. Everything in `internal/` is private. The boundaries lint
enforces this — it is now at error level (Phase 2). Run
`npm run check:boundaries` for a focused report.

### Public surfaces that exist today

| Module | Barrel | What's public |
|---|---|---|
| `lib/api` | ✅ | response envelope helpers (`ok`, `list`, `err`, …) — required by all `/api/v1/*` routes |
| `lib/cache` | ✅ | `getCacheAdapter()` selector |
| `lib/control-plane` | ✅ | clients + cp_* domain types |
| `lib/customers/bmw` | ✅ | branding tokens + palette |
| `lib/entitlement` | ✅ | `requireEntitlement`, `checkEntitlement`, hooks |
| `lib/oee` | ✅ | calculation + interpretation engine |
| `lib/owner-audit` | ✅ | `requireOwnerRole`, `logAuditEvent` |
| `lib/provisioning` | ✅ | dispatcher + runner |
| `lib/support` | ✅ | session validators |
| `lib/tenant` | ✅ | `requireActiveTenant`, `requireTenantAndModule` |

### When migrating to a barrel

- New code: always import via the barrel (`@/lib/entitlement`, not
  `@/lib/entitlement/engine`).
- Existing deep imports: tolerated. Migrate opportunistically when you're
  already touching the file. Don't churn imports for their own sake.
- Removing a re-export from a barrel is a breaking change to the module's
  public surface. Treat it like an API change: search for callers, plan the
  migration.

### Where domain widgets and helpers live

- A React widget that knows a domain table or schema → `components/<module>/`,
  never `components/ui/`. Example: `components/master-data/master-data-combobox.tsx`.
- An i18n helper that knows a domain shape → next to that module's other
  files (e.g. `lib/assessment-lang.ts`), never under `lib/i18n/`.

Generic UI primitives (`button`, `input`, `dropdown`, color-only tokens) and
generic locale plumbing (`useTranslation`, locale context) stay in shared.

## README convention

Every module has a README with:

- **Owner** (a real human, not a team alias)
- **Purpose** (one sentence)
- **Public API** (what `index.ts` exports)
- **Dependencies** (what circles it touches — see
  [architecture-rails.md](./architecture-rails.md))
- **Tests** (how to run them)

Empty / stale READMEs are flagged in code review.

## Adding a new public API endpoint

Any externally-stable API surface lives under `app/api/v1/`. The contract is
in `openapi/v1/openapi.json`. Workflow:

1. Implement the route under `app/api/v1/<resource>/route.ts`.
2. Use the envelope helpers from `@/lib/api` for every response.
3. Add the path to `openapi/v1/openapi.json` in the **same PR**, with at
   minimum: tag, summary, parameters, request body (if any), responses with
   the shared `ErrorEnvelope`.
4. Run `npm run check:openapi` — it cross-checks that every documented path
   resolves to a real `route.ts` file.
5. CI's `npm run check:portability` bundles the OpenAPI check.

**Internal routes** (`/api/admin/*`, `/api/owner/*`, `/api/demo/*`,
`/api/planning/*`, `/api/wertstrom/*`, …) are NOT bound by the v1 contract
and are not in the spec. If a future external integration needs one, promote
it to `/api/v1/*` first. See [`api-contract.md`](./api-contract.md).

## When NOT to create a module

- For one-off helpers — keep them in the calling file.
- To "make things tidier" without a clear owner.
- To wrap a customer-specific concern — that's an adapter
  (`lib/customers/<tenant>/`), not a module.
