Skip to content

Contract-first workflow

CLAUDE.md's invariant 1: packages/contract/openapi.yaml is the source of truth. Controllers conform to it (and are tested for that), the SDK is generated from it, and neither is ever hand-written to differ from it. This page is the concrete workflow that invariant implies.

The contract itself

packages/contract/openapi.yaml is a single ≈5,300-line file describing 85 paths under /v1. It's lint-checked (redocly lint openapi.yaml, packages/contract's own lint script) and is what the API Reference is generated from — the same file, no separate description to keep in sync.

Adding or changing an endpoint

┌─────────────────────┐
│ 1. Edit              │
│ openapi.yaml          │
└──────────┬───────────┘


┌─────────────────────┐     regenerates packages/sdk/src/generated/
│ 2. pnpm --filter      │     {types,paths}.ts from the contract via
│ @loombre/contract       │     openapi-typescript + a small hand-rolled
│ run codegen            │     operations table
└──────────┬───────────┘


┌─────────────────────┐     gate step: git diff --exit-code -- packages/sdk
│ 3. Commit the          │     — the generated SDK must be committed and
│ generated SDK diff     │     must exactly match what codegen produces.
└──────────┬───────────┘     Never hand-edit files under packages/sdk/src/
           │                  generated/ — the "GENERATED — do not edit"
           │                  banner at the top of each is not a suggestion.

┌─────────────────────┐     gate step: oasdiff breaking PREVIOUS.yaml
│ 4. oasdiff checks      │     CURRENT.yaml, against `main`. Field removal,
│ for breaking changes   │     rename, or type change fails here (docs/PLAN.md
└──────────┬───────────┘     §4.1: "additive-only within a major version").


┌─────────────────────┐
│ 5. Implement/adjust    │
│ the controller         │
└──────────┬───────────┘


┌─────────────────────┐     apps/server/test/conformance.spec.ts +
│ 6. Conformance tests   │     seeded-conformance.spec.ts must pass — every
│ must pass               │     non-public op returns a proper 401 when
└──────────┬───────────┘     unauthenticated, public ops validate against
           │                  their documented schema, and every mounted
           │                  route maps to something actually documented
           │                  (no undocumented surface area).

┌─────────────────────┐
│ 7. pnpm gate            │
└─────────────────────┘

What conformance testing actually checks

apps/server/test/conformance.spec.ts boots a real Nest application against a seeded database and walks the contract itself as its test data — there's no separately maintained list of "endpoints to check." For every documented, non-public operation it asserts an unauthenticated request gets a proper RFC 9457 401; for the public operations (POST /auth/login, POST /auth/refresh, GET /system/capabilities) it validates the response against the contract's own schema (via Ajv); for authenticated requests it asserts they're not walled off; and it asserts /healthz stays public. Crucially, it also asserts that every route Express actually has mounted maps to a documented contract path — an endpoint that exists in code but not in openapi.yaml fails this check, not just the reverse.

Why this order

sdk-drift and oasdiff run early in pnpm gate (right after codegen) because they're structural checks over the contract and its generated artifact — cheap, and a failure there means the rest of the gate is checking against a contract state that isn't what will actually ship. See scripts/gate.mjs's own header comment for the full step order and the reasoning behind each step's position.

Released under the AGPL-3.0-only license. No telemetry, ever.