# pnpm — `@nurix/*` Workspaces: naming, boundaries, and consuming

How a `@nurix/*` repo is **named**, **structured**, and how its packages are **consumed** — every repo takes the workspace shape, single-package frontends included. Publishing and a CLI's packaging live in `publishing.md` and `cli-design.md`.

---

## 1. Package naming

Every workspace package is scoped **`@nurix/`**; the dot vs. hyphen after the scope is semantic, not cosmetic:

| Form | Meaning | Examples |
| --- | --- | --- |
| `@nurix/workflows-sdk` (hyphen) | **Published, externally-destined** contract | `@nurix/workflows-sdk` |
| `@nurix/workflows.<name>` (dot) | **Internal** to the product — `private: true`, refactor freely. The default for a new package | `@nurix/workflows.agent-core`, `.worker`, `.playground` |
| `@nurix/<name>` (plain) | Cross-repo shared lib owned **elsewhere** — never renamed | `@nurix/contracts`, `@nurix/components` |

- `<name>` is always **kebab-case**.
- The dot is **not** a path separator — subpath exports resolve normally (`@nurix/workflows.agent-core/nodes`).
- The dot tracks "internal product package," not the `private` flag: `@nurix/workflows.secrets` is `private: false` (extraction-bound) yet dotted. `-sdk` is the only clean published form.

---

## 2. Monorepo & package boundaries

- **One `tsconfig.base.json` at the repo root owns TS policy** — the strict set, `target`, and everything uniform across packages (here, `module`/`moduleResolution`). Each package's `tsconfig.json` `extends` the base and adds only its coordinates (`rootDir`/`outDir`/`include`) and genuine environment overrides (`lib`/`types`/`jsx`; `module`/`moduleResolution` only when the package's module system truly differs). **Never** re-declare or weaken a policy option locally. One exception: a framework that owns its own base (an Astro app on `astro/tsconfigs/strict`).
- **Internal packages are source-first.** A `private: true` package exposes its API via `exports` pointing at source (`"types"`/`"default"` → `./src/index.ts`) — no build step, and no bare `main`/`module` on new packages. One typed subpath per public surface; consumers import the subpath, **never** a deep relative path into another package's `src/`. `types` comes **first** in every subpath block — resolvers take the first matching condition, so a `types` listed after `default` is never reached.
- **Crossing the publish boundary is `publishing.md`** — `private: false`, the published manifest, the pipeline.
- **Single-source shared dep versions with pnpm `catalog:`** — `typescript`, `zod`, `vitest`, `react`, type packages — so every package resolves the same copy and its types stay mutually assignable.
- **TS project references only when it hurts** — adopt `references` once the typecheck/build graph is slow enough to feel; until then, `pnpm -r typecheck`.

### The catalog has no garbage collector

- **Removing the last consumer of a cataloged package removes its catalog entry in the same change** — `pnpm remove` rewrites the manifest and lockfile but **never** `pnpm-workspace.yaml`, and nothing downstream flags the orphan: no install fails, no lint fires.
- The check is mechanical: an entry no workspace manifest names under any dependency field is orphaned; one absent from `pnpm-lock.yaml` is an orphan with certainty.
- **A cataloged package is `catalog:` in every manifest** — a literal range (`"zod": "^4.2.1"`) resolves identically today and silently stops tracking the catalog the moment it moves.

### The workspace root is a hard boundary

- **A dependency spec never points outside the folder holding `pnpm-workspace.yaml`** — `devDependencies` included. Inside the root, `workspace:*` between `packages/*` is the normal, correct thing; outside it, nothing — no `file:../…`, no `link:`, no absolute `file:`. A sibling checkout is another repo, and the registry is the only supported way to consume one — a cross-root spec bakes machine-local paths into the lockfile and the published tarball, so `install --frozen-lockfile` fails on any other machine.
- **A package unreachable from the registry is an auth or publishing bug — fix it there, never with a local path.** Diagnosis order: the scope's registry (§3) → the token → whether it was actually published.

---

## 3. Consuming `@nurix/*` packages

`@nurix/*` packages publish to npm with **`restricted`** access — installing needs read rights to the `nurix` org.

- **Registry** — point the scope at npmjs, where the packages are published. A restricted read answers `404`, not `401`, so a mispaired registry is indistinguishable from "never published" — on an `@nurix/*` 404, check the registry pairing **first**, before auth and before assuming the package is missing.
- **Auth** — `npm login` once (the npm user must be a `nurix` member), or a read token in `.npmrc` (the only CI option):
  ```ini
  @nurix:registry=https://registry.npmjs.org/
  //registry.npmjs.org/:_authToken=${NPM_READ_TOKEN}
  ```
  The token needs only `Read` on the `nurix` scope. A `403` on install means auth failed — check `npm whoami` (local) or the `NPM_READ_TOKEN` secret (CI). Attempt the install directly — the token authenticates ambiently; don't pre-empt or second-guess it. If it fails, flag it to the user — **never** reach for alternatives, a substitute package, or `npm login` swaps.
- **Import by subpath, never the barrel** — subpaths tree-shake reliably; the available subpaths are the package's `exports` keys. `import { Button } from "@nurix/components/element/button";`
- **Styles** — `@import` a CSS-shipping package's stylesheet **once** in the app's CSS entry, after `@import "tailwindcss"`: `@import "@nurix/components/styles.css";` (the full import order is the `frontend` rule's). Unstyled components are almost always a missing styles import. The stylesheet is **not** precompiled — it omits `@import "tailwindcss"` and carries its own `@source` over the package's compiled JS, so the app's Tailwind build generates its utilities: add no `@source` for it, and never `import` it from JS, which compiles it standalone with no theme loaded and fails the build.
- **CLIs** ship a `bin` — run directly: `npx @nurix/etna@latest` (or `npm install -g`).
