ENGINEERING SOP

Structural-First, Port-Driven Development Process

Purpose This SOP defines the invariant development process for projects with strong structural dependencies (monorepos, generators, ports/adapters, feature flags, observability). The goal is repeatability, low ambiguity, and safe iteration from day one.


Phase 1 — Bootstrap the Development Environment

Objective: A clean machine can reach “tests pass + app boots” with minimal manual steps.

Steps

  1. Pin runtimes and core tools (language, package manager).
  2. Provision OS-level dependencies deterministically.
  3. Auto-load environment configuration on directory entry.
  4. Define canonical commands:

Acceptance Criteria


Phase 2 — Establish the Repository Spine

Objective: Define the shape of the system before writing behavior.

Steps

  1. Initialize workspace/monorepo.
  2. Define top-level topology:

  3. Add formatting, linting, licensing, and repo policies.
  4. Create a minimal README with the canonical commands.

Acceptance Criteria


Phase 3 — Lock Architecture & Dependency Rules

Objective: Make architectural violations hard or impossible.

Steps

  1. Define architectural layers (e.g., domain, application, adapters, UI).
  2. Enforce dependency constraints between layers.
  3. Introduce ports as the only way core logic interacts with external systems.
  4. Centralize contracts (API schemas, shared types, invariants).

Acceptance Criteria


Phase 4 — Stand Up Local-First Runtime Dependencies

Objective: Local development mirrors production shape.

Steps

  1. Define required services (DB, cache, queues, flags, storage, observability).
  2. Script lifecycle commands: up, down, reset, seed.
  3. Standardize configuration via env + typed validation.
  4. Ensure full environment reset is cheap and reliable.

Feature Flags via OpenFeature + Flipt

Use the OpenFeature SDK with Flipt (binary, not Docker) for vendor-neutral feature flag management.

Install & Configure

1
2
3
4
5
# Install Flipt binary via mise -- version 2.4.0
mise use flipt 

# Install OpenFeature SDK -- 
pnpm add @openfeature/server-sdk @openfeature/flipt-provider

features.yaml Example

Create infra/flipt/features/features.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace: default
flags:
  - key: new-dashboard
    name: New Dashboard Experience
    type: BOOLEAN_FLAG_TYPE
    enabled: true

segments:
  - key: beta-users
    name: Beta Users
    match_type: ANY_MATCH_TYPE
    constraints:
      - type: STRING_CONSTRAINT_TYPE
        property: plan
        operator: eq
        value: beta

Provider Registration

1
2
3
4
5
6
7
8
9
import { OpenFeature } from '@openfeature/server-sdk';
import { FliptProvider } from '@openfeature/flipt-provider';

await OpenFeature.setProviderAndWait(new FliptProvider({
  url: process.env.FLIPT_URL ?? 'http://localhost:8080',
  namespace: 'default',
}));

export const featureClient = OpenFeature.getClient();

evalContext Mapping

Port Abstraction (Testability)

1
2
3
4
// Domain code uses port, not OpenFeature directly
export interface FeatureFlagsPort {
  isEnabled(flag: string, context: FlagContext): Promise<boolean>;
}

Acceptance Criteria


Phase 5 — Observability & Ops Baseline (Before Features)

Objective: Debuggability exists before complexity.

Canonical Reference: ADR-029: Observability Stack Architecture

Stack Components

Component Role
OpenTelemetry Instrumentation SDK (traces, metrics, logs)
OTel Collector v0.142.0 Telemetry pipeline (receive, process, export)
OpenObserve v0.30.2 Unified backend + dashboards (replaces Prometheus/Grafana)
Vanta Continuous compliance automation (SOC2, ISO 27001)
Logfire v4.16.0 Python-native structured logging with trace correlation

Steps

  1. Define logging conventions (structured, correlated via trace_id/span_id).
  2. Add baseline metrics and tracing via OpenTelemetry SDK.
  3. Implement health checks and dependency checks.
  4. Decide audit boundaries (what must be recorded immutably).

Semantic Context Requirements (ADR-029 Invariants)

All telemetry MUST include these resource attributes:

Attribute Description
sea.domain Bounded context (e.g., governance)
sea.concept Domain concept (e.g., PolicyRule)
sea.regime_id Active compliance regime ID
sea.platform Always sea-forge

Acceptance Criteria


Phase 6 — Define Contracts Before Implementation

Objective: Freeze boundaries early to reduce rework.

Steps

  1. Define API request/response schemas.
  2. Define async/job/event payload schemas.
  3. Define structured outputs for nondeterministic systems (e.g., LLMs).
  4. Define a stable error taxonomy across boundaries.

Acceptance Criteria


Phase 7 — Generate Scaffolding (Structure via Automation)

Objective: Humans design structure once; generators reproduce it forever.

Steps

  1. Use generators for apps, libs, modules, test harnesses.
  2. Create custom generators for recurring patterns:

  3. Ensure every generated project has standard targets:

Acceptance Criteria


Phase 8 — Build the Testing Harness (Before Behavior)

Objective: Every layer has a test strategy before features land.

Layer TypeScript Python Purpose
Unit Vitest 3.x pytest 8.x Fast, isolated tests for domain logic
Integration Vitest + Testcontainers pytest + testcontainers-python Adapter tests with real DBs/services
E2E/Browser Playwright 1.49.x Playwright Cross-browser UI tests
API Mocking MSW 2.x respx Network-level request interception
Fixtures @faker-js/faker factory_boy Deterministic test data

Steps

  1. Define the test pyramid explicitly:

  2. Provide deterministic fixtures and builders.
  3. Ensure tests run locally and in CI.

Vitest + Nx Configuration (Critical for Stability)

Pitfall Avoidance: Vitest + Nx integration requires matching versions and proper plugin configuration.

1. Install (version-locked to Nx)

1
2
3
# Nx 19+ includes @nx/vite with Vitest support
nx add @nx/vite
pnpm add -D vitest @vitest/coverage-v8 vite-tsconfig-paths

2. Configure nx.json for Vitest plugin

1
2
3
4
5
6
7
8
9
10
11
12
{
  "plugins": [
    {
      "plugin": "@nx/vite/plugin",
      "options": {
        "buildTargetName": "build",
        "testTargetName": "test",
        "serveTargetName": "serve"
      }
    }
  ]
}

3. Project vite.config.ts (with Vitest inline)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig({
  plugins: [tsconfigPaths()],
  test: {
    globals: true,
    environment: 'node', // or 'jsdom' for React
    include: ['src/**/*.{test,spec}.{ts,tsx}'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'lcov'],
      exclude: ['node_modules/', 'src/**/*.d.ts'],
    },
    // CRITICAL: Disable watch mode for CI
    watch: false,
  },
});

4. Run Tests via Nx

1
2
3
4
5
6
7
8
# Single project
nx test my-lib

# Affected only (CI)
nx affected -t test

# With coverage
nx test my-lib --coverage

Python Testing with pytest

1
2
3
4
5
# Install
pip install pytest pytest-cov pytest-asyncio testcontainers factory_boy

# Run
pytest tests/ -v --cov=src --cov-report=html

pytest.ini

1
2
3
4
5
6
[pytest]
asyncio_mode = auto
testpaths = tests
python_files = test_*.py
python_functions = test_*
addopts = -v --tb=short

Playwright E2E Setup

1
2
3
4
5
6
# Install
pnpm add -D @playwright/test
npx playwright install chromium

# Run
nx e2e my-app-e2e

playwright.config.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  timeout: 30_000,
  retries: process.env.CI ? 2 : 0,
  use: {
    baseURL: 'http://localhost:4200',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  webServer: {
    command: 'nx serve my-app',
    url: 'http://localhost:4200',
    reuseExistingServer: !process.env.CI,
  },
});

Testcontainers for Integration Tests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { PostgreSqlContainer } from '@testcontainers/postgresql';

describe('UserRepository', () => {
  let container: PostgreSqlContainer;
  let connectionString: string;

  beforeAll(async () => {
    container = await new PostgreSqlContainer().start();
    connectionString = container.getConnectionUri();
  });

  afterAll(async () => {
    await container.stop();
  });

  it('should save and retrieve user', async () => {
    const repo = new UserRepository(connectionString);
    // ...
  });
});

MSW for API Mocking

1
2
3
4
5
6
7
8
9
10
11
12
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  http.get('/api/users', () => {
    return HttpResponse.json([{ id: 1, name: 'Test User' }]);
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Acceptance Criteria


Phase 9 — Implement Features as Vertical Slices

Objective: Each increment is production-shaped and safe.

Required Order (Do Not Skip)

  1. Domain model + invariants
  2. Use case / application logic
  3. Ports (interfaces + fakes)
  4. Adapters (real implementations)
  5. Wiring (API/UI)
  6. Observability hooks
  7. Tests at appropriate layers
  8. Feature flags (if rollout risk exists)

Pre-Implementation Checklist

Lesson Learned: Infrastructure issues commonly derail implementation. Verify before coding.

Generator Usage Patterns

Critical: Prefer just recipes. Use direct Nx invocation as fallback only.

Just Recipes (Preferred):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Bounded context (creates domain/ports/adapters layers)
just generator-bc <name>
just generator-bc <name> sea typescript    # With explicit scope/lang

# Adapter pair (fake + real implementation)
just generator-adapter <name> <ctx> http

# API surface (routes, handlers)
just generator-api <name> <ctx>

# List available generators
just generator-list

# Build generators (required after changes to generators/)
just generator-build

Fallback (Direct Nx Invocation):

1
2
3
pnpm nx g @sea/generators:bounded-context <name> --scope=sea
pnpm nx g @sea/generators:adapter <name> --context=<ctx> --backend=http
pnpm nx g @sea/generators:api-surface <name> --context=<ctx>

After running generators:

  1. Add path mappings to tsconfig.base.json:
    1
    2
    3
    
    "@sea/<name>-domain": ["libs/<name>/domain/src/index.ts"],
    "@sea/<name>-ports": ["libs/<name>/ports/src/index.ts"],
    "@sea/<name>-adapters": ["libs/<name>/adapters/src/index.ts"]
    
  2. Install commonly needed dependencies: pnpm add uuid && pnpm add -D @types/uuid

Polyglot Implementation Strategy

For bounded contexts requiring both Python and TypeScript:

Layer TypeScript Location Python Location
Domain Types domain/src/lib/types.ts domain/src/gen/types.py
Ports ports/src/lib/*.port.ts ports/src/gen/ports.py
Adapters adapters/src/lib/*.adapter.ts services/<ctx>/src/adapters/
Tests adapters/src/lib/*.spec.ts tests/<ctx>/test_*.py

Python Service Structure:

1
2
3
4
5
6
7
8
services/<name>/
├── pyproject.toml          # Dependencies (use poetry or pip)
├── Dockerfile              # Container build
├── main.py                 # FastAPI entry point
└── src/
    ├── api/routes.py       # HTTP endpoints
    ├── adapters/<name>.py  # Real adapter implementation
    └── config/settings.py  # Pydantic settings

Fake-First Development

Lesson Learned: Create fakes before real adapters. This enables:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1. Define the port first
export interface LlmProviderPort {
  completeChat(request: ChatRequest): Promise<ChatCompletion>;
}

// 2. Implement the fake immediately
export class FakeLlmAdapter implements LlmProviderPort {
  async completeChat(request: ChatRequest): Promise<ChatCompletion> {
    // Deterministic response for testing
    return {
      id: `fake-${Date.now()}`,
      content: `Echo: ${request.messages[0]?.content ?? ''}`,
      // ...
    };
  }
}

// 3. Write tests against the fake
// 4. Then implement the real adapter

Common Pitfalls

Pitfall Symptom Prevention
Wrong generator prefix “Cannot find generator ‘sea:adapter’” Use @sea/generators:adapter
Missing path mappings “Cannot find module ‘@sea/myctx-domain’” Add to tsconfig.base.json
Generator type errors TypeScript compilation fails Use literal unions, not string
Missing @cqrs annotations flow_lint.py fails Add annotations before pipeline
Python imports fail ModuleNotFoundError Use pip install -e . in service dir

Acceptance Criteria


Phase 10 — Feature Flags & Rollout Discipline

Objective: Enable speed without permanent complexity.

Steps

  1. Classify flags (release, experiment, ops, tiering).
  2. Assign owner and expiry for every flag.
  3. Enforce server-side authority for security.
  4. Schedule regular flag cleanup.

Flag Types

Type Purpose Typical Lifespan
release Gate incomplete features Days to weeks
experiment A/B testing, metrics-driven Weeks to months
ops Circuit breakers, kill switches Permanent
tiering Entitlement, plan-based access Permanent

Acceptance Criteria


Phase 11 — CI/CD & Release Mechanics

Objective: Fast feedback, safe merges, predictable releases.

Steps

  1. Use graph-aware CI (only build/test what changed).
  2. Enforce quality gates (lint, types, tests).
  3. Produce immutable release artifacts.
  4. Define environment promotion flow.

Smart Sync Check (Spec-First CI)

Lesson Learned: Spec-first development means specs change before generated code. CI must accommodate this workflow.

File Category CI Behavior
Only **/src/gen/** Determinism check runs — must match pipeline output
docs/specs/** or generators/** Determinism check skipped — stale code expected
Both categories Determinism check skipped — spec changes take precedence

Workflow after spec PR merges:

1
2
just pipeline <context>   # Regenerate from updated specs
git add -A && git commit -m "chore: regenerate from spec updates"

Acceptance Criteria


Phase 12 — Continuous Hardening & Debt Management

Objective: Prevent slow decay of system quality.

Steps

  1. Add resilience patterns (timeouts, retries, idempotency).
  2. Rotate secrets and audit dependencies.
  3. Track intentional shortcuts with payoff windows.
  4. Set and monitor performance budgets.

Acceptance Criteria


Invariant Checklist (Use This Everywhere)


1) ENGINEERING SOP — 1-Page Printable Checklist Template

Use this as a literal checklist. If a phase isn’t satisfied, don’t move forward.


Phase 1 — Bootstrap Dev Environment

Pass if: clean clone → setup → dev up → test → dev works


Phase 2 — Repository Spine

Pass if: new dev knows where to start in <5 minutes


Phase 3 — Architecture & Dependency Rules

Pass if: architectural violations fail automatically


Phase 4 — Local-First Runtime Stack

Pass if: local mirrors production shape, flags work via OpenFeature


Phase 5 — Observability Baseline (ADR-029)

Pass if: “what broke?” is answerable immediately, all telemetry uses OTel


Phase 6 — Contracts Before Code

Pass if: boundaries are frozen before implementation


Phase 7 — Generator-Driven Structure

Pass if: adding a module is one command, generators compile cleanly


Phase 8 — Testing Harness

Pass if: just test-ts and just test-e2e both pass


Phase 9 — Vertical Slice Delivery

Pre-Implementation Checks:

For each feature:

Post-Generator Checklist:

Pass if: slice is production-shaped, reversible, and fakes exist for all ports


Phase 10 — Feature Flags Discipline

Pass if: flags don’t accumulate, governance is automated


Phase 11 — CI/CD & Releases

Pass if: releases are boring and repeatable


Phase 12 — Continuous Hardening

Pass if: quality improves over time