Verify
Last reviewed against the codebase: 2026-07-25.
How to confirm a change is sound before it ships - the same signals CI enforces, run by hand. This is a life-safety application, so “it compiles” is not the bar: a change is verified when the types build, the suites are green with production secrets absent, the running process reports ready (not just alive), and - if you touched anything the reference is generated from - the docs gates stay green.
Run everything below from Fire-Path-AI/ unless a step says otherwise. The order
here mirrors the PR workflow, so working top-to-bottom reproduces CI locally.
1. Type-check the workspace
pnpm run typecheckThis is a two-stage check, and the order matters - it is why there is a
dedicated script rather than a bare tsc:
typecheck:libs-tsc --build --forcecompiles every workspace library (lib/*) first, emitting their declaration files.typecheck- then runs each artifact’s owntypecheck(tsc --noEmit) acrossartifacts/**andscripts.
Why libs first: the apps import the libs by their built .d.ts outputs. If you
type-check an app before its libs have been built, tsc resolves stale or missing
declarations and reports phantom errors (or, worse, misses real ones). CI runs the
same root script (see the typecheck job in .github/workflows/pr.yml), so a green
pnpm run typecheck locally means a green typecheck job.
The client app additionally guards its design system. pnpm --filter @workspace/firepath-ai run theme:check runs typecheck + a theme audit + a WCAG
contrast audit; the web CI job runs this, so a token/colour change that passes
locally but fails contrast will be caught there.
2. Run the tests
Both the API server and the client use Vitest. They are separate suites with very different shapes.
API server
pnpm --filter @workspace/api-server run test # unit + route integrationpnpm --filter @workspace/api-server run test:coverage # same, with the coverage ratchetpnpm --filter @workspace/api-server run test:watch # re-run on change (local dev)test:coverage enforces ratchet thresholds defined in
artifacts/api-server/vitest.config.ts (currently statements 59 / branches 50 /
functions 62 / lines 61). These floors sit just under the measured baseline and are
a one-way ratchet: raise them as new tests land, never lower them. A change that
deletes or fails to cover code enough to drop below a floor fails the suite even if
every assertion passes - that is intentional, and it is what makes “we have a
coverage gate” a real claim.
Client
pnpm --filter @workspace/firepath-ai run testThe client suite is pure-logic only - it runs Vitest over the shared lib/**
math (spread geometry, offline-tile math, formatting) that must be correct on a
phone during an evacuation. Component/UI behaviour is deliberately not unit-tested
here; it is covered by the workspace typecheck plus the on-device EAS build. CI runs
this as the client-tests job.
Secrets must be UNSET
Both suites must pass with production secrets absent. They inject dummy values or
mock the external call, so a missing real key is never the reason for a red suite -
and a suite that needs a real secret is a bug. The api-test CI job proves this: it
sets only throwaway values (JWT_SECRET: ci-test-only-..., a local DATABASE_URL)
and never receives production credentials.
The api-test job also mirrors the local DB setup: it boots Postgres, applies the
PostGIS init migration, pushes the Drizzle schema (push-force), seeds fixtures,
then runs test:coverage. See Start/stop locally for the same
sequence by hand.
3. Service health - alive vs ready
With the API running (default port 3000, all routes mounted under /api), two
endpoints distinguish “the process is up” from “the process can actually serve”:
curl http://localhost:3000/api/healthzcurl http://localhost:3000/api/readyz-
GET /healthz- liveness. Cheap and dependency-free; always returns{"status":"ok"}if the process is running. The orchestrator (ALB / Cloudflare) uses this to decide whether to kill and replace a task. It must stay dependency-free - otherwise a transient DB blip would make the orchestrator destroy an otherwise-healthy process. -
GET /readyz- readiness. Actually verifies the process can serve. It runsSELECT 1against Postgres and returns HTTP 503 with{"status":"not_ready", "db":"down"}if the DB is unreachable; on success it returns200with{"status":"ready","db":"ok","ingest":[...]}, whereingestreports the age (in minutes) of each source’s last cycle and flags any older than 60 min asstale.
Why both exist (real defect history): /healthz used to be the only signal, and
it returned a static "ok" even while Postgres was down and every real route was
500-ing - a green light that lied. /readyz was added (audit finding M1) so
readiness reflects the truth. Stale ingest is surfaced, not fatal: a stale FIRMS
feed sets stale: true (visible on the dashboard and to a CloudWatch alarm) but does
not flip readiness to 503 - taking the whole API down because one upstream feed
lagged would be the wrong failure mode.
4. Documentation gates
Any change that touches code the reference is generated from must keep the docs
site green. Run the full gate chain exactly as CI does (the docs-site job):
pnpm --filter @workspace/docs-site run ciThis runs, in order:
gen- regenerate all four reference sections from source.check:drift- fails if the committed reference is stale (i.e.genproduced output that differs from what’s checked in).check:reference- every page the nav/links point at actually exists.astro build- the site compiles.check:redaction- the builtdist/contains no secrets or PII (runs the shared denylist; the marketing site runs the same list in its own job).check:links- no broken internal links in the output.
5. Load smoke test (optional)
A ramped k6 smoke test proves routing + the auth middleware + the DB
read path hold under concurrency - so the HOT wake only has to confirm the
behaviour at prod scale, not discover a problem in it. With the local stack up and
the server on :3000:
docker compose up -d postgres redisk6 run Fire-Path-AI/artifacts/api-server/load/api-smoke.k6.jsThe script (artifacts/api-server/load/api-smoke.k6.js) targets
http://localhost:3000/api by default. Point it at another environment by
overriding API_BASE:
API_BASE=https://api.firepath.software/api k6 run \ Fire-Path-AI/artifacts/api-server/load/api-smoke.k6.jsThis is a smoke test, not a capacity benchmark - it validates that the tier stays correct under load, not how many requests it can serve. Real capacity numbers come from a HOT wake.
What “verified” means
A change is ready to ship when, from a clean checkout with production secrets unset:
pnpm run typecheckis green (libs build, apps + scripts type-check);- the API and client Vitest suites pass, and coverage does not regress below the ratchet;
- a running server answers
/healthzand/readyzwith200; - if you touched a generated source,
pnpm --filter @workspace/docs-site run ciis green (no drift, no broken links, no redaction hits).
These are exactly the jobs in .github/workflows/pr.yml, so a local pass predicts a
green PR. When something still only fails in CI, it is almost always a real secret
leaking through your shell (see §2) - not a difference in the code.