Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,8 @@ jobs:
pnpm --filter @copper/db generate
git diff --exit-code -- packages/db/migrations
test -z "$(git status --porcelain -- packages/db/migrations)"
# Started the way an operator starts it, so an entry point that dies on import fails here.
- name: The seed script starts and runs
run: pnpm seed
env:
SEED_DAYS: 1
28 changes: 28 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Contributing

The process for people. Rules for agents are in [AGENTS.md](AGENTS.md), and each rule's enforcement is in [docs/guardrails.md](docs/guardrails.md).

## Set up

```bash
nvm use # Node from .nvmrc
pnpm install # also installs the git hooks
pnpm dev:db # local Postgres, nothing to install
```

Copy `.env.example` to `apps/web/.env.local` and fill it in.

## Hooks

| Hook | Runs | Takes |
| --- | --- | --- |
| `.husky/pre-commit` | `lint-staged`: Biome formats the staged files and re-stages them | About a second |
| `.husky/pre-push` | `pnpm check`, then `pnpm build`: the same commands CI runs | A minute or so |

Do not bypass them with `--no-verify`. CI runs the same checks and is the gate.

## Pull requests

One pull request per phase or issue. Paste the output of `pnpm verify` into the description. A human merges.

`pnpm lint` only reports. `pnpm lint:fix` rewrites files, and is for you to run by hand, never a hook or CI.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ pnpm install
pnpm verify # lint, checks, typecheck, tests and the production build
```

[AGENTS.md](AGENTS.md) has the rules for changes, [docs/feature-map.md](docs/feature-map.md) lists every route with a command that reaches it, and [docs/guardrails.md](docs/guardrails.md) maps each rule to the check that enforces it.
[CONTRIBUTING.md](CONTRIBUTING.md) has the process for people, [AGENTS.md](AGENTS.md) has the rules for changes, [docs/feature-map.md](docs/feature-map.md) lists every route with a command that reaches it, and [docs/guardrails.md](docs/guardrails.md) maps each rule to the check that enforces it.

## License

Expand Down
24 changes: 23 additions & 1 deletion docs/guardrails.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,32 @@ Each rule and the thing that enforces it. A rule with nothing in the right-hand
| Installs do not change the lockfile in CI | `pnpm install --frozen-lockfile` in the shared setup action |
| CI steps are pinned | Third-party actions referenced by commit hash; Dependabot moves them |
| Secrets and database code stay out of the browser bundle | `import 'server-only'` first in `apps/web/src/lib/{env,db,auth}.ts` and `features/*/queries.ts` |
| Missing configuration fails by name at startup | `readEnv` in `apps/web/src/lib/env.ts` (`apps/web/test/env.test.ts`); `assertEnv` in `apps/ingest/src/index.ts` |
| Missing configuration fails by name at startup | `readEnv` in `apps/web/src/lib/env.ts` (`apps/web/test/env.test.ts`); `assertEnv` in `apps/ingest/src/index.ts`; the `DATABASE_URL` check first in the `migrate` and `seed` scripts |
| The tracker stays under 1.5 KB gzipped | `packages/tracker/test/tracker.test.ts` |
| No IP address or user agent is stored | `apps/ingest/test/shard.test.ts` ("stores neither the IP address nor the user agent") |
| A failed flush never loses or double counts an hour | `apps/ingest/test/app.test.ts`, `packages/db/test/store.test.ts` |
| Unknown site keys cannot keep the database awake | `ProjectDirectory` negative cache and lookup budget; `apps/ingest/test/app.test.ts` |
| Repeat dashboard visits do not query the database | `unstable_cache` with `project:` and `owner:` tags; check with `COPPER_DEBUG=1` and `/api/health` |
| Chart colors pass the palette checks on both surfaces | `--chart` in `apps/web/src/app/globals.css`, validated with the dataviz palette script |
| Script entry points start outside the bundler and test runner | `migrations` CI job runs `migrate` and `seed` with `tsx` against a real Postgres |
| One Node major everywhere | `.nvmrc`, `engines` (`22.x`) and `@types/node` (`^22`) agree; CI reads `.nvmrc`; Dependabot holds `@types/node` majors |
| The dev overlay is not in screenshots | `devIndicators: false` in `apps/web/next.config.ts` |

## Waiting on a person

| Rule | What is missing |
| --- | --- |
| A red or missing check blocks the merge | Branch protection on `main` is a repository setting, not a file. Require `lint`, `docs`, `typecheck`, `test`, `build` and `migrations`, and require branches to be up to date. Then open one pull request that fails on purpose and confirm the merge is refused. |

## Not adopted

Practices from the [engineering practices guide](https://www.expeditionlabs.co/resources/engineering-practices/llms.txt) that were considered and left out, and what would change that.

| Practice | Why not here | Adopt when |
| --- | --- | --- |
| Changed-files gate and job-level skip conditions | No job skips anything, so there is no skip logic to get wrong. CI has no path filters. | CI gets slow enough that docs-only changes should skip it |
| Scheduled infrastructure-dependent suite | Every CI job is hermetic: the only service is a Postgres container the job owns. | A suite needs a live Neon or Cloudflare account |
| Port and resource registry | No port is assigned by hand. `dev:db` takes `PORT` and Next picks a free port itself. | Two working copies routinely run at once |
| Canonical class ordering | Biome's `useSortedClasses` is still a nursery rule. | It is promoted to a stable group |
| Blame-ignore file | Biome has formatted the code since the first commit, so there is no mass reformat to hide. | A formatter or config change rewrites many files |
| Language-neutral migration runner per runtime | TypeScript is the only runtime that writes the schema. The migrations are already plain SQL files. | A second language writes to the database |
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"license": "MIT",
"packageManager": "pnpm@12.4.1",
"engines": {
"node": ">=22"
"node": "22.x"
},
"scripts": {
"dev": "turbo run dev",
Expand Down
Loading