# The monorepo master list

Load when: auditing a workspace's health, reviewing a monorepo PR, scaffolding review gates, or deciding what to enforce next in an existing workspace.

The definitive requirements for a `@nurix/*` monorepo, in three tiers: **required** (every workspace, day one), **trigger-bound** (required the moment its trigger exists), and **deferred** (explicitly not required before scale). Assembly mechanics are [`monorepo-setup.md`](monorepo-setup.md); the publishing pipeline is [`publishing.md`](publishing.md); naming doctrine is [`pnpm.md`](../../../rules/pnpm.md) §1. Every line is auditable — it holds or it doesn't — and names its enforcement mechanism in the closing table.

## Required — every workspace, day one

1. **Declared membership** — the `pnpm-workspace.yaml` globs cover every `package.json` in the tree: no undeclared package islands, no second lockfile beside `pnpm-lock.yaml`.
2. **Catalog as law** — every dep shared by ≥2 members lives in `catalog:`, members reference `"catalog:"`, and `catalogMode: strict` is on so a literal pin of a cataloged dep is an install error. A declared-but-unconsumed catalog is drift with extra steps.
3. **Private pinned root** — root `package.json` carries `private: true`, an exact `packageManager` pin, and `engines.node: ">=22"`; the root never compiles and never publishes.
4. **Full-tree fan-out** — root `build` / `typecheck` / `test` run `pnpm -r`; a member the root scripts can't reach is unmaintained by definition.
5. **One TS policy owner** — `tsconfig.base.json` carries the full strict set (`strict`, `noUncheckedIndexedAccess`, `noImplicitOverride`, `noFallthroughCasesInSwitch`, `noUnusedLocals`/`noUnusedParameters`, `declaration`+`declarationMap`); every member extends it thinly (coordinates + environment only; a framework-owned base is the one exception); no root `tsconfig.json`. A base nobody extends governs nothing.
6. **Doctrine names with dot⇒private parity** — internal members are `@nurix/<repo>.<thing>` **and** `private: true`: the dot signals, the flag blocks (npm allows dots in names — `socket.io` — so only the flag prevents publishing). A clean `@nurix/<thing>` hyphen name exists only as a deliberate publish surface. Bespoke per-repo scopes are drift.
7. **Cross-repo dependencies ride npm, never disk** — zero `link:`/`file:` to paths outside the repo; consume published restricted versions (`"^0.14.1"`). A fresh clone either installs or fails loudly, and a sibling-repo rename can't strand manifests. A dep that must be co-developed belongs inside this workspace.
8. **Siblings and singletons wired right** — sibling deps via `workspace:*`; host singletons (react, the framework client) as `peerDependencies` in libs; apps hold them directly.
9. **Source-first internals, one-way flow** — internal packages export `./src/index.ts` through typed subpaths, no build step; apps depend on packages, never the reverse; anything not exported stays unreachable.
10. **Strict hoisting** — no `shamefully-hoist`, no `node-linker=hoisted`; an undeclared import must fail immediately.
11. **CI that gates** — a workflow installs with `--frozen-lockfile` and gates merges on the root fan-out. No CI is a compliance failure; a CI that mutates manifests before building is worse than none.
12. **A real suite behind `pnpm test`** — the canonical-command rule applies to every workspace; toolchain configs (eslint + prettier, root-only) are wired to `lint`/`format` scripts that actually run. A fan-out script with zero targets is decoration, not compliance.

## Required the moment the trigger exists

- **Native or postinstall deps in the lockfile** → `onlyBuiltDependencies` in `pnpm-workspace.yaml` (never the root `package.json` `pnpm` field), kept current as native deps come and go — pnpm 10 silently skips unlisted builds and the failure surfaces at runtime.
- **A vulnerable transitive** → one `overrides` block in the workspace file; never duplicated into the root manifest.
- **A member crosses the publish boundary** (`private: false`) → the [`publishing.md`](publishing.md) fields; the build hook **inside** `scripts` (a top-level `prepublishOnly` key silently never runs); `sideEffects: false` (or the CSS-glob variant) so consumers tree-shake; and the parity harness — `pack` → tarball-install into fixtures → build → `publint` + `attw` — re-run on every `exports`-surface change. Dev-mode success proves nothing about what consumers get.

## Deferred until scale — explicitly not required before

- **Affected-filtering and task-graph runners** (`--filter "...[origin/main]"`, turbo, nx) — adopt when full `pnpm -r` runs are slow enough to feel; never day one.
- **`CODEOWNERS` by directory** — when multiple teams own members; decoration before that.

## Enforcement — every rule names its mechanism

A rule that depends on discipline drifts; a rule the tooling enforces doesn't. When adding a rule to this list, name the mechanism and what happens on violation — prefer the variant that fails loudly at install/CI over the one a reviewer must notice.

| Rule | Mechanism | On violation |
| --- | --- | --- |
| Cross-repo deps via npm | registry resolution + ambient restricted auth | fresh clone/CI fails loudly — never a machine-specific path |
| Dot-name internal members | `private: true` (`npm publish` refuses); the dot alone blocks nothing | publish attempt hard-fails |
| Sibling deps | `workspace:*` refuses registry resolution | can't build against a stale published sibling |
| One version per shared dep | `catalog:` + `catalogMode: strict` | literal pin of a cataloged dep = install error |
| No deep imports into siblings | subpath `exports` encapsulation | non-exported path is unresolvable |
| No phantom deps | strict pnpm hoisting (symlinked `node_modules`) | undeclared import = module-not-found immediately |
| Source never ships | `files: ["dist"]` whitelist | unlisted files can't enter the tarball |
| Mistaken publish stays private | `publishConfig.access: "restricted"` | lands org-private, not world-readable |
| One host-singleton copy | `peerDependencies` + package-manager dedupe | duplicate react = loud hook-dispatcher crash |
| One pnpm version | exact `packageManager` pin (corepack) | wrong pnpm refuses to run |
| No unvetted postinstall | pnpm 10 default-block + `onlyBuiltDependencies` | script silently skipped until allow-listed |
| Lockfile honesty | `--frozen-lockfile` in CI | drift fails the build |
| The tarball is what dev promised | parity harness (`pack` → tarball-install → `publint`+`attw`) | catches field bugs no schema validates (e.g. a build hook outside `scripts`) |
| Rename a dep, not the imports | npm alias (`"old": "npm:@nurix/new@^x"`) | — (escape hatch, not a guard) |
