# Setting up a `@nurix/*` pnpm monorepo

Load when: scaffolding a new pnpm workspace, converting a repo to one, adding a workspace member, or wiring the root toolchain (catalog, scripts, build gating).

How a workspace is **assembled** — the root files, layout globs, catalog, and verification. What packages are *named* and how their boundaries work is [`pnpm.md`](../../../rules/pnpm.md) §1–2; how a member *publishes* is [`publishing.md`](publishing.md); the standing invariants to keep true after assembly are [`monorepo-master-list.md`](monorepo-master-list.md). Reference implementations: `~/dev/workflows` (the canonical `packages/*` + catalog shape), `~/dev/starter` (adds `fixtures/*` and the publish-parity harness), `~/dev/robin` and `~/dev/sync-info` (the `packages/*` + `apps/*` split).

## 1. Three root files own the workspace

Everything workspace-wide lives in exactly three root files; packages stay thin. Nothing else at root compiles, publishes, or carries policy.

**`pnpm-workspace.yaml`** — membership + shared versions:

```yaml
packages:
  - "packages/*"
  - "apps/*"              # only if the repo has deployables — see §2

catalog:                   # one version per shared dep — see §3
  typescript: ^5.7.3
  react: ^19.2.0
  react-dom: ^19.2.0
  vite: ^8.0.0
  vitest: latest
  zod: ^4.2.0

onlyBuiltDependencies:     # postinstall allow-list — see §4
  - esbuild
```

**Root `package.json`** — a private orchestrator, never a package:

```jsonc
{
  "name": "<repo-name>",            // or "@nurix/<repo>.root" (starter) — never publishable
  "private": true,                  // MUST — `pnpm -r publish` would otherwise ship the root
  "packageManager": "pnpm@10.33.1", // exact pin, no range — corepack enforces it; without it contributors churn the lockfile across pnpm majors
  "engines": { "node": ">=22" },
  "scripts": {
    "build": "pnpm -r build",
    "typecheck": "pnpm -r typecheck",
    "test": "pnpm -r test",
    "dev": "pnpm -r --parallel --if-present dev",
    "lint": "eslint .",
    "format": "prettier --write ."
  }
}
```

- **Fan out with `pnpm -r`; alias with `--filter`.** `--if-present` when not every member has the script; per-app conveniences read `"dev:admin": "pnpm --filter @nurix/nustack-app dev"` (nustack) or `"worker:dev": "pnpm --filter @sync/worker dev"` (sync-info).
- **One toolchain config at root.** `eslint`/`prettier` run from the root over the whole tree — never per-package configs.
- **No turbo/nx.** Plain `pnpm -r` is the baseline; reach for a task-graph runner only when the build graph is slow enough to feel — the same threshold as TS project references (`pnpm.md` §2).

**`tsconfig.base.json`** — the single TS policy owner (`pnpm.md` §2). The workflows repo's base is the model: `strict`, `noUncheckedIndexedAccess`, `noImplicitOverride`, `noFallthroughCasesInSwitch`, `noUnusedLocals` / `noUnusedParameters`, `declaration` + `declarationMap`, `skipLibCheck`. There is **no root `tsconfig.json`** — the base is extended by members, never compiled from.

## 2. Layout — pick the globs by what the repo holds

| Shape | When | Model |
| --- | --- | --- |
| `packages/*` only | libraries + services, no deployable front-ends | workflows, campaign-studio |
| `packages/*` + `apps/*` | shared libs consumed by deployables (web, worker, extension, desktop) | robin, sync-info |
| Explicit member list | few members, nothing picked up by accident | ui-grader (`taxonomy`, `db`, `rater-ui`, `services/*`) |
| + `fixtures/*` | consumer-simulation apps for parity testing (§5) | starter |

- **Dependency direction is one-way** — `apps/*` depend on `packages/*`, never the reverse; siblings import each other's typed subpath exports, never a deep path into `src/` (`pnpm.md` §2).
- Flat globs are the default; a deep path in the list (`sdk/packages/node/iii`) is for polyglot trees only.

## 3. `catalog:` — one version per shared dep

Anything two or more members depend on goes in the catalog: the toolchain (`typescript`, `vite`, `vitest`, type packages) and the host singletons (`react`, `react-dom`, `zod`). Members reference it as `"react": "catalog:"` — never a literal version. **A member that pins its own version beside the catalog defeats it** — two `react` copies means two hook dispatchers and runtime errors, not a type error.

Internal siblings are `"workspace:*"`; both protocols are rewritten to real versions at publish time.

## 4. pnpm 10 build gating

pnpm 10 refuses to run dependency postinstall scripts unless allow-listed. Keep the allow-list in `pnpm-workspace.yaml` — `onlyBuiltDependencies` (or the map-form `allowBuilds`; `pnpm approve-builds` writes it for you) — for native/codegen deps: `esbuild`, `@swc/core`, `better-sqlite3`. The failure mode is **not** an install error: install succeeds and the missing binary surfaces at runtime.

## 5. Adding a member

1. `packages/<thing>/` — name per [`pnpm.md`](../../../rules/pnpm.md) §1 (`@nurix/<repo>.<thing>`, `private: true` for internal).
2. Internal member: source-first `exports` — `"types"`/`"default"` → `./src/index.ts`, one typed subpath per public surface (`@nurix/workflows.agent-core` is the model). No build step, no `dist`.
3. `tsconfig.json`: `extends` the base + `rootDir`/`outDir`/`include` only — coordinates and environment, never policy.
4. Dependencies: `workspace:*` for siblings, `catalog:` for shared externals.
5. Publishable member: dist-based `exports`, `files: ["dist"]`, `publishConfig` — [`publishing.md`](publishing.md) §2 — and cover it with the parity check (§6).

> **Agent guard — never commit a `link:` to a sibling repo on disk** (`"@nurix/contracts": "link:../../../NuStack-contracts"`). The path is machine-specific: every fresh clone and CI run breaks. Consume the published restricted package (`pnpm.md` §3); if the dep must be co-developed, that's the signal it belongs in this workspace.

## 6. Verify — publish parity

`pnpm install && pnpm -r typecheck` is the smoke test after any membership or catalog change. For workspaces with publishable members, the starter's `scripts/publish-parity.mjs` is the model harness: `npm pack` each publishable member, tarball-install into isolated copies of `fixtures/*`, build them, run `publint` + `attw` — proving consumers of the tarball get what workspace-source dev promises (a deep `src/` import or a missing `exports` entry works in dev and only breaks here). Wire it as the root `parity` script; run before first publish and after any change to an `exports` surface.

## 7. Quick checklist for a new workspace

- [ ] `pnpm-workspace.yaml`: globs (§2) + `catalog:` + build allow-list if needed
- [ ] Root `package.json`: `private: true`, exact `packageManager` pin, `engines`, `pnpm -r` scripts
- [ ] `tsconfig.base.json` with the strict policy set; no root `tsconfig.json`
- [ ] First member added per §5; siblings via `workspace:*`, shared deps via `catalog:`
- [ ] `pnpm install && pnpm -r typecheck` green
- [ ] Publishable member → [`publishing.md`](publishing.md) + parity harness (§6)
