---
title: Development conventions
description: Conventions for apps consuming Eristack and for contributors to the business-libs monorepo.
---

# Development conventions

Conventions for apps consuming Eristack and for contributors to the business-libs monorepo.

## Product priorities

Four **design targets** govern every package (details: [`agent-workflow.md`](./agent-workflow.md) § Design targets; Cursor: `.cursor/rules/eristack-package-targets.mdc`):

1. **Cheap to implement (tokens)** — integration in ≤3 files; canonical docs; export registries/helpers consumers would copy.
2. **Predictable result** — same behavior in core, forms, and API; string-first domain values; documented defaults.
3. **High reliability** — production paths tested; Drizzle/DB default; no demo-only stores in skills.
4. **Clear boundaries** — sharp packages; recommend before inventing; app owns UX and domain tables; **consumers must not reinvent** library exports.

Supporting norms:

- **Package graph:** load `@eristack/ai-knowledge#package-relationships` before multi-package wiring; run `pnpm debottleneck:check:ci` in CI (`eristack check --profile pr`).
- **Correct money and identity** beat clever abstractions.
- **Headless shells** — Express/Nest/React adapters stay thin.
- **Agent-readable docs and skills** ship with every package (`docs/`, `skills/`).

## Monorepo layout (business-libs)

| Path | Role |
| --- | --- |
| `packages/primitive/money` | `@eristack/money` (primitive) |
| `packages/capability/doc-number` | `@eristack/doc-number` (capability) |
| `packages/service/jwt-auth` | `@eristack/jwt-auth` (service) |
| `packages/infrastructure/backseat` | `@eristack/backseat` (infrastructure; browser mock REST + IndexedDB) |
| `packages/ui/multitab` | `@eristack/multitab` (UI; tab workspace — coming soon) |
| `packages/features/` | **Under construction** — future `@eristack/feature-*`; see `roadmap/features.md` |
| `packages/ai/ai-knowledge` | `@eristack/ai-knowledge` (AI; this pack) |
| `packages/ai/ai-workflow` | `@eristack/ai-workflow` (AI) |
| `roadmap/` | Living priority stack for future packages |
| `examples/*` | Private runnable demos (not published) |
| `apps/web` | Public site; renders `packages/<category>/*/docs` by category |
| `_ai-docs/` | WIP (`wip/`), brainstorm, audit — see `_ai-docs/README.md` |
| `_artifacts/` | Domain maps / skill specs |
| `.changeset/` | Pending release notes |

Category order (docs + filesystem): primitive → registries → capability → service → infrastructure → ui → features → AI.

## Lockfile (pnpm)

GitHub CI runs **`pnpm install --frozen-lockfile`** before any tests. Any workspace **`package.json`** change (including `examples/*`) must ship an updated **`pnpm-lock.yaml`** in the same PR.

| Step | Command |
| --- | --- |
| Refresh lockfile after dep edits | `pnpm lockfile:sync` or `pnpm eristack sync deps` |
| Verify locally / catalog CI | `pnpm lockfile:check` or `pnpm eristack sync deps --check` |
| Before push (matches PR CI) | `pnpm prepush` → same as `pnpm ci:affected` (`eristack ci --base origin/main --affected`: drift gates + turbo build/typecheck/test on changed packages) |

`eristack plan --json` adds **`sync deps`** when changed paths include `package.json`.

## Branching and releases

- **GitHub Flow** — feature branches from `main`, PRs into `main` only
- Do not revive a long-lived `dev` integration branch
- User-facing package changes need `pnpm changeset`
- Docs-only / CI-only changes do not need a changeset

### Private consumer monorepos (no npm publish)

**business-libs** and every published `@eristack/*` library use **Changesets** (above). A **consumer app monorepo** that deploys as one product (web + API + libs, no npm) should **not** copy that model.

| Context | Versioning |
| --- | --- |
| This repo + npm `@eristack/*` | Changesets → Version Packages PR → npm |
| Private pnpm product monorepo | `@eristack/local-monorepo-versioning` — `version.lock.json`, root + per-package semver, `eristack-version` CLI |

Load `@eristack/local-monorepo-versioning#local-monorepo-versioning-core` or recipe *local-monorepo-versioning* (`recommend()` triggers: `version.lock.json`, private monorepo release). Package guide: `packages/infrastructure/local-monorepo-versioning/docs/getting-started.md`.

### Changesets on `0.x` (contributors)

| Changeset type | On `0.0.0` | On `0.1.0+` (still pre-1.0) |
| --- | --- | --- |
| **`patch`** | `0.0.1` | next **`0.(n+1).0`** (stay pre-1.0) |
| **`minor`** | **`0.1.0`** (first publish) | **`0.2.0`** semver on 0.1.x — **breaks `^0.1.0` peers** (`<0.2.0`) and Changesets **major-bumps** all peer dependents (e.g. backseat → 12 spine packages). Use only when coordinating a release train. |

Routine features/fixes on packages already past `0.0.0`: use **`patch`** (e.g. `0.1.4` → `0.1.5`, stays inside `^0.1.0`). Use **`minor`** on `0.0.0` for first release, or when you **mean** a 0.y line bump / exit from 0.x with peer range updates.

**Peer cascade (why patch on `@eristack/backseat`):** twelve packages declare optional peer `"@eristack/backseat": "^0.1.0"`. npm treats `^0.1.0` as `>=0.1.0 <0.2.0`. A **minor** changeset on backseat at `0.1.4` becomes **`0.2.0`**, out of range — Changesets (`onlyUpdatePeerDependentsWhenOutOfRange: true`) schedules **major** bumps on jwt-auth, qups, pbac, etc. This is not `workspace:*` in `dependencies` (those are clean); it is **peer semver range** + Changesets policy.

### Changeset file shape (CI enforced)

- **One `@eristack/*` package per `.changeset/*.md` file** — a multi-package frontmatter block repeats the same body in every package changelog on the Version PR.
- **Body describes only that package** — no `### @eristack/foo` cross-package sections or copy-pasted monolith.
- Run **`pnpm changesets:check`** in CI and locally before merge.

### Internal optional peers (Backseat spine)

- Published **`peerDependencies`**: semver (`^0.1.0`), not `workspace:*`
- Monorepo **`devDependencies`**: `workspace:*`
- `.changeset/config.json`: `onlyUpdatePeerDependentsWhenOutOfRange: true` — dependents bump only when the new peer is outside the declared range
- No **`fixed`** / **`linked`** Changesets groups

Consumer upgrade steps: [`upgrading.md`](./upgrading.md) · skill `@eristack/ai-knowledge#upgrading-eristack`.

### Two-PR release model (contributors)

Ship features in **one feature PR**; let Changesets + CI own version bumps in a **second PR**. Do not run `pnpm changeset version` locally on a feature branch.

| Ship in the **feature PR** | Do **not** touch in the feature PR | CI / Version Packages PR |
| --- | --- | --- |
| Source, tests, `package.json` **exports** (not `version`) | `package.json` `version` fields | `changeset version` bumps versions |
| Package `docs/` + Intent `skills/` **content** | Hand-edited `CHANGELOG.md` | Generated changelog sections |
| `recipes.yaml`, `pnpm knowledge:sync` output (`catalog.ts`, `recipes.ts`, `recommend-eristack` catalog block) | Deleting skill/recipe work when reverting a mistaken local version | `pnpm knowledge:sync` again (via `version-packages`) so catalog versions match bumped `package.json` |
| One or more `.changeset/*.md` files | Running `changeset publish` locally | `pnpm release` on npm after Version PR merges |

**If you accidentally ran `changeset version` locally:** revert only `version` + `CHANGELOG` + lockfile drift. Keep skills, recipes, generated catalog content, and docs — those are the feature.

**Feature PR green bar:** `pnpm ci` (=`pnpm build && pnpm eristack check --profile full --skip-build`). Agents: run `pnpm eristack plan --json` first for a minimal command list.

**After feature PR merges to `main`:** the Release workflow opens **“chore: version packages”**. Review and merge it; npm publish runs on that merge.

### Publish gate — package exports (hard rule)

1. Every subpath imported by spine packages must appear in **`package.json` `exports`** (e.g. `"./adapters"` on `@eristack/backseat`).
2. Every export subpath must have a **`tsup` entry** that builds `dist/…`.
3. After **`pnpm build`**, run **`pnpm exports:check`** — CI enforces; catches Vite `Missing "./adapters" specifier`.
4. **`pnpm exports:check`** (after build) validates dist subpaths — do not duplicate that in vitest with `import("@eristack/pkg/subpath")` from unit tests. Vitest SSR re-bundles `dist/` chunks and causes flaky `ERR_MODULE_NOT_FOUND` on CI (mixed chunk hashes). Unit tests import **`../src/…`**; reserve package-name resolution tests for dedicated export suites (e.g. `@eristack/backseat` adapters) or integration/e2e.

Do not document/catalog an export unless it passes `exports:check`.

### CI pipeline (what runs where)

| Trigger | Workflow · command | Scope | Speed levers |
| --- | --- | --- | --- |
| PR → `main` | `ci.yml` · `pnpm eristack ci --base origin/main` | **Affected**: drift gates first (lockfile, changesets, publish, skills, knowledge, docs, ticket), then turbo build/typecheck/test on `...[origin/main]`, web typecheck/build only when `apps/web` changed, exports, contrast. Switches to **full** when lockfile / root `package.json` / `scripts/check-*` / `ai-dev` change, or PR label `ci:full`. | turbo cache restored from `actions/cache` (`.turbo/cache`, prefix key → last run on any branch); single root `intent validate` |
| Push → `main` | `ci.yml` · `pnpm eristack ci --full` | **Full** `pr` profile on every merge, ordered: cheap drift gates → one parallel `turbo run build typecheck test` graph → examples, exports, integration coverage | same turbo cache; `@eristack/old-web` excluded (scripts suffixed `:archived`) |
| Push → `main` | `release.yml` · build → `exports:check && publish:check` → `changesets/action` | Version Packages PR or npm publish | pre-publish gate is the last owner of what lands on npm — CI runs in parallel, not before |
| PR touching `skills/` | `check-skills.yml` · `pnpm exec intent validate --github-summary` | Skills only, **workspace-pinned** intent (never `npm i -g`) | — |

Node **22** everywhere (CI, skills, release) — Node 20 is EOL. `timeout-minutes: 15` on CI; `cancel-in-progress` per ref. CI sets `ERISTACK_LOCKFILE_VERIFIED=1` because its Install step already ran `pnpm install --frozen-lockfile`; locally the lockfile check still runs. `typecheck` ignores `*.md`, `docs/`, `skills/`, `knowledge/` in its turbo hash, so docs-only edits hit cache.

Local parity: `pnpm prepush` = PR affected mode; `pnpm ci` = main push. Both are the *same* `eristack` runner CI uses — if it is green locally with a synced lockfile, it is green in CI.

## Adapter design rules

- Core entry is framework-agnostic (no Express/Nest/React/Drizzle imports in core)
- Adapters are separate export paths (`/drizzle`, `/express`, `/nest`, `/client`, `/react`, …)
- App injects `db`, secrets, and domain ids — libraries do not own your users table
- Credentials / refresh tokens / format sequences are **child** resources

## Testing

- Prefer focused unit tests next to behavior (`tests/` or `src/**/*.test.ts`)
- Memory stores are for tests/ephemeral use; Drizzle stores for production paths
- Examples prove end-to-end wiring; they are not published packages

## Documentation + ai-knowledge (hard rule)

- Library guides: `packages/<category>/<name>/docs/*.md` + `_meta.json`
- Do not duplicate library markdown inside `apps/web`
- Site-only marketing/story/support pages live under `apps/web`
- **Every iteration:** update `docs/` + Intent `skills/` together; update `packages/ai/ai-knowledge/knowledge/recipes.yaml` when discoverable by product language; run `pnpm knowledge:sync` / `knowledge:check`
- Stale ai-knowledge after a docs/API change is incomplete work

### Token-efficient documentation (hard rule)

Agents are the primary audience. **In-depth** does not mean **many files**.

| Cross-cutting topic | One canonical doc |
| --- | --- |
| Upgrades, Backseat spine, peers, Changesets | `packages/ai/ai-knowledge/knowledge/upgrading.md` + `@eristack/ai-knowledge#upgrading-eristack` |
| Product routing | `recipes.yaml` + `recommend-eristack` skill |

Per-package docs: full detail for **that package’s production adapters**; for monorepo-wide Backseat wiring, **redirect** to upgrading §3 with a small delta table only. See `.cursor/rules/docs-depth-tokens.mdc`.

## AI working docs (`_ai-docs/`)

Three buckets — see repo `_ai-docs/README.md`:

| Bucket | Path | Rule |
| --- | --- | --- |
| WIP | `_ai-docs/wip/<topic>/` | Ephemeral; delete after promote |
| Brainstorm | `_ai-docs/brainstorm/` | Package names before `roadmap/horizon.md` |
| Audit | `_ai-docs/audit/` | Point-in-time quality snapshot |

While implementing: WIP notes good enough to draft public docs (include skill/recipe impact). When finished: promote to package docs / site / skills / recipes; sync catalog; **delete** the WIP folder.

## Drizzle integration tests (Sprint A)

Production paths default to Drizzle; prove them with sqlite integration tests — not memory stores in skills.

| Piece | Location |
| --- | --- |
| Shared sqlite helper | `@internal/test-harness` — `createTestSqliteDb`, `execSql`, `canUseBetterSqlite` |
| Hash-chain harness | `@eristack/hash-chained-ledger/testing` — `setupHclSqlite`, tamper helpers |
| Per-package test | `packages/<layer>/<name>/tests/drizzle.integration.test.ts` |
| Run all | `pnpm test:integration` (root) or `pnpm eristack check --profile integration` |
| In CI | Suites run **once**, inside each package's turbo `test` (cached). The `integration` check in `pr`/`ci` only verifies (`--verify-covered`) that every `drizzle.integration.test.ts` is matched by the package vitest `include` — keep `tests/**/*.test.ts` in it. |

**Pattern:** wrap suites in `describe.skipIf(!canUseBetterSqlite())` so CI without native bindings skips cleanly. When adding a new Drizzle store, add or extend an integration test in the same PR.

## Shipping a new `@eristack/*` package (HARD RULE — do not skip steps)

When adding a **new publishable package** under `packages/<category>/<name>/`, complete this checklist in the **same change set** as the code. Agents must not merge library code without site + knowledge visibility.

### 1. Package spine

| Step | Location |
| --- | --- |
| Core + adapters + tests | `packages/.../src`, `tests/` — Drizzle integration where applicable; memory only in unit tests |
| Public exports | `package.json` `exports` + `tsup` entries → `pnpm build` + `pnpm exports:check` |
| Package docs (source of truth) | `docs/` — getting-started with ≤3-file wiring, `_meta.json` sections; every page starts with frontmatter `title:` + `description:` (`pnpm docs:check` enforces `title`) |
| Intent skills | `skills/**/SKILL.md` — actionable body; `sources` → **one** canonical guide when possible |
| Ticket stub | `ticket.yaml` if repo convention requires it |
| Root wiring | root `package.json`: add `workspace:@eristack/<name>` to `intent.skills` **and** `devDependencies` → `pnpm lockfile:sync` → `pnpm exec intent install --map` regenerates the `AGENTS.md` skills block |

### 2. Agent discoverability (`@eristack/ai-knowledge`)

| Step | Location |
| --- | --- |
| Recipe | `packages/ai/ai-knowledge/knowledge/recipes.yaml` — product language (`recommend()` must find it) |
| Dependency map | `knowledge/package-relationships.md` — stack row + load order |
| Upgrade matrix | `knowledge/upgrading.md` — new package row, IndexedDB/Backseat notes if any |
| Sync | `pnpm knowledge:sync` then `pnpm knowledge:check` — **required** when any `skills/**/SKILL.md` frontmatter changes (catalog reads skills) |

### 3. Public site (`apps/web`) — **required**

| Step | Location |
| --- | --- |
| Product card | `apps/web/src/lib/site.ts` — slug, npm name, layer, description, doc path |
| Products page | `apps/web/src/app/products/page.tsx` — metadata/copy if the grid intro should mention the capability |
| Home / ecosystem | `apps/web/src/lib/ecosystem-content.ts` — new tech (e.g. AWS S3, presigned uploads) in the right layer group |
| Site config | `apps/web/src/lib/site-config.ts` — tagline/description when it changes what Eristack “is” |
| Marketing strengths | `apps/web/src/lib/marketing-content.ts` — optional bullet if it is a headline capability |
| Docs nav on site | `pnpm docs:sync` after `docs/_meta.json` changes — human commits generated `_meta.json` under `apps/web` |

### 4. Release

| Step | When |
| --- | --- |
| Changeset | User-facing API/behavior → `pnpm changeset` |
| CI | `pnpm eristack check --profile pr` or `pnpm ci:pr` before merge |

### 5. Cursor / repo memory

- `.cursor/rules/new-package-ship.mdc` — quick agent reminder (points here).
- Cross-cutting ERP patterns (e.g. document attachments): **one** delta in the canonical guide (`document-lines-erp.md`), not ten package stubs.

**Definition of done:** package builds, tests pass, docs + skills + recipe + site catalog + ecosystem copy updated, `knowledge:check` and `docs:check` green.

## Scope discipline

- Change only what the task requires
- Do not rewrite root README for agent guidance (use `AGENTS.md` / skills)
- Never commit secrets or tokens into docs or AI notes
