Files
motovaultpro/.claude/skills/mvp-build-and-env/SKILL.md
T
Eric GullicksonandClaude Fable 5 c239bb9347
Deploy to Staging / Build Images (push) Successful in 5m32s
Deploy to Staging / Deploy to Staging (push) Successful in 44s
Deploy to Staging / Verify Staging (push) Successful in 5s
Deploy to Staging / Notify Staging Ready (push) Successful in 4s
Deploy to Staging / Notify Staging Failure (push) Has been skipped
chore: replace AI skill library with 16 verified mvp-* skills
Removes the old planner/decision-critic/role-agents/domain-agents system
(step-injector scripts, stale scopes, sprint-era workflow) and replaces it
with 16 ground-truth-verified skills under .claude/skills/: change control,
debugging playbook, failure archaeology, architecture contract, domain
reference, OCR/Gemini pipeline, config and secrets, build and env, run and
operate, diagnostics (with tested scripts), validation and QA, docs and
writing, launch readiness, deploy-safety campaign, proof and analysis
toolkit, and research frontier. RULE 0/1/2, the temporal-contamination rule,
and the decision stress-test protocol are carried forward into the new
skills; the retired content remains in git history.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-09 20:21:42 -05:00

202 lines
17 KiB
Markdown

---
name: mvp-build-and-env
description: Load when starting from a fresh MotoVaultPro checkout or when any build/test/install command fails locally. Triggers - "how do I run this", "npm test fails", "Could not resolve a module for a custom reporter", "tdd-guard-jest", "Jest did not exit one second after the test run", jest hangs, "Configuration file not found at /app/config/production.yml", "make setup fails", secrets are directories, "mobile-setup nothing to be done", pytest/paddleocr install, Dockerfile build questions, "why is my migration not on staging". Covers what works on the dev machine vs container-only, the exact local test loop per workspace, Docker build anatomy, and known environment traps.
---
# Build and Environment Reality
All commands below were verified by running them or reading the exact source on 2026-07-07. Where docs contradict code, code wins; this file states code behavior.
## When to use / When NOT to use
Use this skill when you have a fresh checkout and need to build, lint, type-check, or run tests, or when a local command fails in a confusing way.
Do NOT use this skill for:
- Deploying, rollback, staging/prod operations, backups: `mvp-run-and-operate`
- What counts as test evidence, adding tests, mobile+desktop validation: `mvp-validation-and-qa`
- Issue/branch/PR workflow and review rules: `mvp-change-control`
- Runtime failures of a deployed stack: `mvp-debugging-playbook`
- Config keys, secrets catalog, feature tiers: `mvp-config-and-secrets`
## 1. The development model (read this first)
Development on this project is done by AI sessions working in this repo; the human owner reviews PRs. There is NO fully working local dev loop and none is expected. The loop is:
1. Edit code locally. Run unit tests + lint + type-check + build locally (per workspace, below).
2. Open a PR. CI builds 3 Docker images and deploys them to staging. That is the entire gate — no tests, no lint (canonical statement: `mvp-validation-and-qa` section 1). The only compile check CI performs is `tsc` inside the Dockerfiles.
3. End-to-end verification happens ON STAGING (https://staging.motovaultpro.com) after the PR pipeline deploys. See `mvp-validation-and-qa` for the evidence bar.
Consequence: anything your local commands do not catch, nothing catches before staging. Run the local gates every time; they are the only gates.
## 2. What works on the dev machine (verified 2026-07-07)
Root `package.json` has NO scripts and no workspaces. `npm test` / `npm run lint` at repo root fail with "Missing script". Always cd into `backend/`, `frontend/`, or `ocr/`.
### Bootstrap
```bash
make install # runs npm install in frontend/ AND backend/ (Makefile:220)
# or individually:
cd backend && npm install
cd frontend && npm install
```
### Dev-safe make targets (Makefile:218-254; these never touch Docker)
| Target | Does |
|---|---|
| `make install` | `npm install` in frontend + backend |
| `make type-check` | `npm run type-check` in frontend + backend |
| `make lint` | `npm run lint` in frontend + backend |
| `make build-local` | `npm run build` in frontend + backend (outputs `frontend/dist`, `backend/dist`) |
Every other make target (`setup`, `start`, `rebuild`, `migrate`, `clean`, ...) drives Docker. Per root CLAUDE.md, `make setup`/`make rebuild` are for staging/prod-style builds, NOT development — and on this machine `make setup` does not produce a working stack anyway (section 3).
### backend/ — everything works
```bash
cd backend
npm run lint # eslint src (flat config eslint.config.js)
npm run type-check # tsc --noEmit
npm run build # tsc --project tsconfig.build.json -> dist/
npm test -- --forceExit # all unit tests
npm test -- --forceExit --testPathPattern=src/features/fuel-logs # one feature
npm run test:feature --feature=fuel-logs # same, needs --forceExit caveat too
```
`--forceExit` is MANDATORY locally. Without it jest prints "Jest did not exit one second after the test run" and hangs indefinitely (verified: killed after 8+ min; with `--forceExit` the same suite finishes in ~2 s). Cause: `backend/src/core/config/database.ts` creates a pg Pool eagerly and unit tests leave open handles. Do not "fix" a hang by waiting.
Expect a RED baseline on main: 15 of 25 unit suites fail pre-existing (6 die loading real config, 7 fail ts-jest compilation, 2 contain the 2 genuinely failing tests; `Tests: 2 failed, 147 passed, 149 total`, re-verified 2026-07-09 — baseline detail homed in `mvp-deploy-safety-campaign` Phase 0.3). Judge your change against that baseline, not against zero.
Why unit tests work at all locally: they mock `core/config/config-loader` and `core/config/redis` at the top of the test file (see `src/features/stations/tests/unit/station-matching.test.ts:5-15`). The real config-loader is an eager singleton (`config-loader.ts:281`: `export const appConfig = configLoader.load()`) that throws `Configuration file not found at /app/config/production.yml` if imported unmocked outside a container. If you see that error in a test, the test (or something it imports) is loading real config — mock it like the existing unit tests do.
Integration tests (`src/features/*/tests/integration/`) are container-only AND destructive — see section 3.
Broken script: `npm run migrate:feature` references `src/_system/migrations/run-feature.ts`, which does not exist (only `run-all.ts` does). It fails on invocation.
### frontend/ — build tools work, `npm test` is broken outside the container
```bash
cd frontend
npm run lint # works
npm run type-check # works
npm run build # works: tsc --project tsconfig.build.json && vite build
npm run dev # vite dev server on :3000 (UI only; API calls need a backend)
```
`npm test` fails outside the container with:
```
Error: Could not resolve a module for a custom reporter. Module name: tdd-guard-jest
```
Two independent causes in `frontend/jest.config.ts:28-36`:
1. The `tdd-guard-jest` reporter is declared only in the ROOT `package.json` devDependencies, and no root `node_modules/` exists on a fresh checkout (root has no scripts, nobody runs `npm install` there), so the module cannot resolve from `frontend/`.
2. The reporter config hardcodes `projectRoot: '/home/egullickson/motovaultpro'` — a Linux path from the container/CI host, wrong on this macOS checkout even if the module resolved.
WORKING FALLBACK (verified 2026-07-07, executes and reports results):
```bash
cd frontend
npx jest src/path/to/File.test.tsx --reporters=default # single file
npx jest --reporters=default --testPathPattern=fuel-logs # by pattern
npx jest --reporters=default # full suite
```
Trap: `--reporters` is greedy. `npx jest --reporters=default src/Foo.test.tsx` (positional AFTER the flag) swallows the path as a second reporter name and fails with "Could not resolve a module for a custom reporter. Module name: src/Foo...". Put the positional path FIRST, or use `--testPathPattern`.
Expect the fallback run to be RED at baseline on main: `Tests: 17 failed, 196 passed, 213 total` across 14 failing suites (re-verified 2026-07-09; the baseline numbers are homed in `mvp-deploy-safety-campaign` Phase 0.3). Diff your run against that baseline — do not attribute the pre-existing failures to your change, and do not report "only 1 known failure". There is NO in-container alternative: the shipped frontend image is the nginx production stage (no node/npm) and `frontend/.dockerignore` excludes `*.test.*` from every stage, so `docker compose exec mvp-frontend npm test` (still documented in docs/TESTING.md) has never been able to work. This fallback plus staging verification is the whole frontend test story today.
Also note: `frontend/test/` contains test files outside jest `roots: ['<rootDir>/src']` — they never run anywhere. See `mvp-validation-and-qa`.
### ocr/ — tests exist; running them locally is heavyweight and UNVERIFIED
```bash
cd ocr
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt # includes paddleocr, google-cloud-vision, google-genai — multi-GB, slow
python -m pytest tests/
```
Verified facts only: `ocr/requirements.txt` lists `pytest>=7.4.0` and `pytest-asyncio>=0.21.0`; 16 test files exist under `ocr/tests/`; tests import `from app.main import app`; there is no venv, no `pytest.ini`, and no `conftest.py` in the repo. The pip install and pytest run above were NOT executed during authoring (paddleocr download is too heavy) — UNVERIFIED as a complete recipe. CI never runs OCR tests either (`ocr/Dockerfile` has no test step), so OCR test results always come from a manual run.
## 3. Container-only, and why
### Backend integration tests — DESTRUCTIVE, backup first (non-negotiable)
`backend/src/features/*/tests/integration/*.test.ts` need a real Postgres + Redis and real config (`CONFIG_PATH`, `SECRETS_DIR` — container paths). docs/TESTING.md:24-32 documents `make shell-backend` then `npm test`, but that recipe is STALE-DANGEROUS: the shipped backend image is the production stage (`npm ci --omit=dev` — no jest; only `dist/` plus migration SQL copied), so `npm test` inside it fails with "jest: not found". As-shipped, the integration tests are runnable nowhere; the viable paths are a builder-stage image (`docker build --target builder ...`) or a host run against an ephemeral Postgres/Redis — see `mvp-deploy-safety-campaign` 1B.
They are DESTRUCTIVE by design: `beforeAll` executes the feature's real migration SQL and `afterAll` drops the tables, e.g. `vehicles.integration.test.ts:38` runs `DROP TABLE IF EXISTS vehicles CASCADE` and drops `update_updated_at_column()` CASCADE (which other tables' triggers depend on); `admin.integration.test.ts:72-73` drops `admin_audit_logs` and `admin_users`. They run against whatever database the container config points at — the shared dev DB `motovaultpro`.
Owner non-negotiable: never run integration tests against a database you care about without a fresh backup first (`make db-backup` or `./scripts/export-database.sh`). Restore path: `./scripts/import-database.sh`. See `mvp-run-and-operate`.
Note: `config/app/ci.yml` ("CI-specific configuration for backend tests") exists but nothing in the repo references it — it also points at `mvp-postgres`/db `motovaultpro`, so it does not make the tests safe.
### Full local stack — currently cannot work on this machine
`make setup` (compose up --build + migrations) is labeled staging/prod-build only in root CLAUDE.md, and on this checkout it will not produce a healthy stack because four "secret files" exist only as Docker-created EMPTY DIRECTORIES (Docker auto-creates a directory when a bind-mount source file is missing; canonical description of this trap: `mvp-config-and-secrets` section 2):
```
secrets/app/stripe-secret-key.txt <- directory
secrets/app/stripe-webhook-secret.txt <- directory
secrets/app/auth0-ocr-client-id.txt <- directory
secrets/app/auth0-ocr-client-secret.txt <- directory
```
The backend's Zod secrets schema (`config-loader.ts:114-125`) requires `stripe_secret_key` and `stripe_webhook_secret` as non-optional strings; reading a directory fails, validation throws, and the backend crash-loops at startup. The OCR container bind-mounts the two auth0-ocr paths (`docker-compose.yml:213-214`). To ever make the local stack work: `rm -rf` the four directories, create real `.txt` files (see the `.txt.example` siblings), then `make setup`. Until then, local full-stack is off the table — which is consistent with the dev model in section 1.
Verify current state: `ls -la secrets/app/ | grep '^d'` (any `.txt` entry that is a directory is a broken mount point).
## 4. Docker build anatomy
Three Dockerfiles; CI (`.gitea/workflows/staging.yaml`) builds all three on every push to main and every PR sync, tags `:<7-char-sha>` AND `:latest`, and pushes to the registry.
| Image | Dockerfile | Compile gate | Registry |
|---|---|---|---|
| backend | `backend/Dockerfile` (2-stage) | `RUN npm run build` (tsc) at line 31 | `git.motovaultpro.com/egullickson/backend` |
| frontend | `frontend/Dockerfile` (4-stage) | `RUN npm run build` (tsc + vite) at line 35 | `git.motovaultpro.com/egullickson/frontend` |
| ocr | `ocr/Dockerfile` (1-stage) | none (pip install only, no tests) | `git.motovaultpro.com/egullickson/ocr` |
These tsc runs are the ONLY compile gate CI has. A PR is "green" when the 3 images build and staging boots healthy — nothing else.
Migrations ship inside the backend image: `backend/Dockerfile:58-61` sets `ENV MIGRATIONS_DIR=/app/migrations` and copies `src/features` and `src/core` there; the container CMD (`Dockerfile:84`) runs `node dist/_system/migrations/run-all.js && npm start` on every start. Therefore a NEW MIGRATION REACHES STAGING/PROD ONLY VIA AN IMAGE REBUILD — merging SQL into the repo does nothing until CI builds and deploys a new backend image. A new feature's migrations also require an entry in `MIGRATION_ORDER` in `backend/src/_system/migrations/run-all.ts` or they silently never run.
Frontend build-time vs runtime config: all `VITE_*` values are baked at image build via compose/CI build args (`docker-compose.yml:60-64`, ARGs at `frontend/Dockerfile:21-25`). Exception: the Google Maps key/map-id are injected at container RUNTIME by `frontend/scripts/load-config.sh` from `/run/secrets`, not at build.
PR builds clobber `:latest` — deploy implications and blue-green mechanics are in `mvp-run-and-operate`.
## 5. Environment traps table
| Trap | Reality | Do instead |
|---|---|---|
| Root `npm test`/`npm run lint` | Root `package.json` has NO scripts (root CLAUDE.md is wrong here; code wins) | cd into `backend/` or `frontend/` |
| Root dependencies | `test@^3.3.0` (stray/accidental package, supply-chain smell), plus `jest`, `@playwright/test`, `tdd-guard-jest` with no root node_modules — all dead weight | Do not `npm install` at root expecting anything; do not add root deps |
| `make mobile-setup` | Advertised in help and `.PHONY` but HAS NO RULE — "Nothing to be done" | Mobile testing procedure: `mvp-validation-and-qa` |
| `make clean` | `docker compose down -v --rmi all` — DESTROYS DB VOLUMES wherever run | Non-negotiable: fresh backup first (`make db-backup`) |
| Backend jest hang | pg pool open handles; jest never exits | Always `npm test -- --forceExit` locally |
| Frontend `npm test` | Broken outside container (tdd-guard-jest reporter + hardcoded Linux projectRoot) | `npx jest <path> --reporters=default`; positional path BEFORE the flag |
| Integration tests | `DROP TABLE ... CASCADE` on the shared dev DB | Container-only, backup first, never against a DB you care about |
| `make setup` locally | 4 secrets are empty directories; backend Zod validation crash-loops | Treat local full-stack as unavailable; verify on staging |
| `frontend/.env.local` | Affects `npm run dev` only; deployed images use build args baked by CI | Change Gitea CI variables / compose build args for deployed values |
| New migration "not on staging" | Migrations live inside the backend image at `/app/migrations` | Merge -> CI image build -> deploy; check `_migrations` table |
| `npm run migrate:feature` (backend) | Points at nonexistent `run-feature.ts` | `npm run migrate` (runs all, idempotent per file) |
## Provenance and maintenance
Authored 2026-07-07 against commit e729d42 (main). All commands in sections 2 and the frontend fallback were executed on the dev machine that day, except the OCR venv/pytest recipe (labeled UNVERIFIED). Volatile facts and how to re-check them:
| Fact | Re-verify with |
|---|---|
| Root package.json still has no scripts + stray `test` dep | `cat package.json` |
| Backend scripts unchanged (test/lint/type-check/build, broken migrate:feature) | `cat backend/package.json` and `ls backend/src/_system/migrations/` |
| Frontend jest still broken (reporter + projectRoot) | `grep -n -A6 reporters frontend/jest.config.ts` |
| Frontend fallback still works | `cd frontend && npx jest --reporters=default --listTests` |
| Backend jest still hangs without forceExit | `cd backend && npx jest src/features/stations/tests/unit/station-matching.test.ts` (should finish; if it hangs, trap still live) |
| 4 secrets still empty directories | `ls -la secrets/app/ \| grep '^d'` |
| `make mobile-setup` still ruleless | `grep -n "mobile-setup:" Makefile \|\| echo missing` |
| Integration tests still DROP TABLE | `grep -rn "DROP TABLE" backend/src --include="*.test.ts"` |
| Migrations still image-packaged | `grep -n "MIGRATIONS_DIR\|migrations" backend/Dockerfile` |
| CI still runs zero tests/lint | `grep -rn "npm test\|npm run lint" .gitea/workflows/ \|\| echo none` |
| Registry image names | `grep -n "egullickson/" .gitea/workflows/staging.yaml \| head` |
| OCR pytest deps present | `grep -n "pytest" ocr/requirements.txt` |