
---

## 1.9 `AGENTS.python.md` — Python / FastAPI conventions

```markdown
# AGENTS.python.md — Python Service Conventions

## Layered architecture (FastAPI)


## 1. Guardrail files (drop these in first)

These artifacts enforce the rules without you or Claude having to remember them. Install them as **Phase 0**, before touching any real code.

### 1.1 `.claude/CLAUDE.md` — loaded into every Claude session

```markdown
# <Your Project Name>

> **NON-NEGOTIABLE STRUCTURE RULES** (enforced by `.claude/settings.json` hook, git pre-commit, and CI):
> 1. **File-size budget:** soft target **300** lines, **hard cap 500** lines for any non-test, non-generated source file. Anything larger → split it. Exceptions are listed in `.claude/rules/loc-exceptions.txt` and require a written rationale.
> 2. **Clean architecture per service.** Routers/handlers stay thin (≤30 lines per handler) and delegate to services; services use repositories; repositories own DB I/O. See `AGENTS.python.md` / `AGENTS.go.md` / `AGENTS.typescript.md`.
> 3. **Do not touch the database schema.** No new migrations, no `ALTER TABLE`, no model field renames without an explicit migration plan reviewed by the DB owner.
> 4. **Public endpoints are a contract.** Any change to a path/method/status/schema in a backend must be accompanied by a matching update in **every** consumer. OpenAPI snapshot tests in `tests/contracts/` are the gate.
> 5. **Tests are not optional.** New code without tests fails CI. Refactors must preserve coverage and add a characterization test before splitting an oversized file.
> 6. **Do not bypass the guardrails.** Do not edit `.claude/settings.json`, `scripts/check-loc.sh`, or the loc-exceptions list to silence violations. If a rule is wrong, raise it in a PR description.
>
> These rules apply to every Claude Code session opened inside this repository, regardless of who launched it. They are loaded automatically via this CLAUDE.md.
```

Keep project-specific notes (dev environment, URLs, tech stack) under this header.

### 1.2 `.claude/settings.json` — PreToolUse LOC hook

First line of defense. Blocks Write/Edit operations that would create or push a file past 500 lines. This stops Claude from ever producing oversized files.

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "f=$(jq -r '.tool_input.file_path // empty'); [ -z \"$f\" ] && exit 0; lines=$(printf '%s' \"$(jq -r '.tool_input.content // empty')\" | awk 'END{print NR}'); if [ \"${lines:-0}\" -gt 500 ]; then echo '{\"decision\":\"block\",\"reason\":\"guardrail: file exceeds the 500-line hard cap. Split it into smaller modules per the layering rules in AGENTS.<lang>.md.\"}'; exit 0; fi",
            "shell": "bash",
            "timeout": 5
          }
        ]
      },
      {
        "matcher": "Edit",
        "hooks": [
          {
            "type": "command",
            "command": "f=$(jq -r '.tool_input.file_path // empty'); [ -z \"$f\" ] || [ ! -f \"$f\" ] && exit 0; case \"$f\" in *.md|*.json|*.yaml|*.yml|*test*|*tests/*|*node_modules/*|*.next/*|*migrations/*) exit 0 ;; esac; new_str=$(jq -r '.tool_input.new_string // empty'); old_str=$(jq -r '.tool_input.old_string // empty'); old_lines=$(printf '%s' \"$old_str\" | awk 'END{print NR}'); new_lines=$(printf '%s' \"$new_str\" | awk 'END{print NR}'); cur=$(wc -l < \"$f\" | tr -d ' '); proj=$((cur - old_lines + new_lines)); if [ \"$proj\" -gt 500 ]; then echo \"{\\\"decision\\\":\\\"block\\\",\\\"reason\\\":\\\"guardrail: this edit would push $f to ~$proj lines (hard cap is 500). Split the file before continuing.\\\"}\"; fi; exit 0",
            "shell": "bash",
            "timeout": 5
          }
        ]
      }
    ]
  }
}
```

### 1.3 `.claude/rules/architecture.md` — auto-loaded architecture rule

```markdown
# Architecture Rules (auto-loaded)

Non-negotiable. Applied to every Claude Code session in this repo.

## File-size budget
- **Soft target:** 300 lines. **Hard cap:** 500 lines.
- Enforced by PreToolUse hook, pre-commit hook, and CI.
- Exceptions live in `.claude/rules/loc-exceptions.txt` and require `[guardrail-change]` in the commit message. This list should SHRINK over time.

## Clean architecture
- Python: see `AGENTS.python.md`. Layering: api → services → repositories → db.models.
- Go: see `AGENTS.go.md`. Standard Go Project Layout + hexagonal.
- TypeScript: see `AGENTS.typescript.md`. Server-by-default, push client boundary deep, colocate `_components/` and `_hooks/` per route.

## Database is frozen
- No new migrations. No `ALTER TABLE`. No column renames.
- Pre-commit hook blocks any change under `migrations/` unless commit message contains `[migration-approved]`.

## Public endpoints are a contract
- Any change to path/method/status/schema must update every consumer in the same change set.
- OpenAPI baseline at `tests/contracts/openapi.baseline.json`. Contract tests fail on drift.

## Tests
- New code without tests fails CI.
- Refactors preserve coverage. Before splitting an oversized file, add a characterization test pinning current behavior.
- Layout: `tests/unit/`, `tests/integration/`, `tests/contracts/`, `tests/e2e/`.

## Guardrails are protected
- Edits to `.claude/settings.json`, `scripts/check-loc.sh`, `scripts/githooks/pre-commit`, `.claude/rules/loc-exceptions.txt`, or any `AGENTS.*.md` require `[guardrail-change]` in the commit message.
- If Claude thinks a rule is wrong, surface it to the user. Do not silently weaken.

## Tooling baseline
- Python: `ruff`, `mypy --strict` on new modules, `pytest --cov`.
- Go: `golangci-lint` strict, `go vet`, table-driven tests.
- TS: `tsc --noEmit` strict, ESLint type-aware, Vitest, Playwright.
- All: dependency caching in CI, license/SBOM scan via `syft`+`grype`.
```

### 1.4 `.claude/rules/loc-exceptions.txt`

```
# loc-exceptions.txt — files allowed to exceed the 500-line hard cap.
#
# Format: one repo-relative path per line. Comments start with '#'.
# Each exception MUST be preceded by a comment explaining why splitting is not viable.
# Goal: this list SHRINKS over time.

# --- Example entries ---
# Static data catalogs — splitting fragments lookup tables without improving readability.
# src/catalogs/country-data.ts
# src/catalogs/industry-taxonomy.ts

# Generated files — regenerated from schemas.
# api/generated/types.ts
```


### 1.5 `scripts/check-loc.sh`

```bash
#!/usr/bin/env bash
# check-loc.sh — File-size budget enforcer. Soft: 300. Hard: 500.
#
# Usage:
#   scripts/check-loc.sh                    # scan whole repo
#   scripts/check-loc.sh --changed          # only files changed vs origin/main
#   scripts/check-loc.sh path/to/file.py    # check specific files
#   scripts/check-loc.sh --json             # machine-readable output
# Exit codes: 0 clean, 1 hard violation, 2 bad invocation.

set -euo pipefail
SOFT=300
HARD=500
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
EXCEPTIONS_FILE="$REPO_ROOT/.claude/rules/loc-exceptions.txt"

CHANGED_ONLY=0; JSON=0; TARGETS=()
for arg in "$@"; do
  case "$arg" in
    --changed) CHANGED_ONLY=1 ;;
    --json) JSON=1 ;;
    -h|--help) sed -n '2,10p' "$0"; exit 0 ;;
    -*) echo "unknown flag: $arg" >&2; exit 2 ;;
    *) TARGETS+=("$arg") ;;
  esac
done

is_excluded() {
  local f="$1"
  case "$f" in
    */node_modules/*|*/.next/*|*/.git/*|*/dist/*|*/build/*|*/__pycache__/*|*/vendor/*) return 0 ;;
    */migrations/*|*/alembic/versions/*) return 0 ;;
    *_test.go|*.test.ts|*.test.tsx|*.spec.ts|*.spec.tsx) return 0 ;;
    */tests/*|*/test/*) return 0 ;;
    *.md|*.json|*.yaml|*.yml|*.lock|*.sum|*.mod|*.toml|*.cfg|*.ini) return 0 ;;
    *.svg|*.png|*.jpg|*.jpeg|*.gif|*.ico|*.pdf|*.woff|*.woff2|*.ttf) return 0 ;;
    *.generated.*|*.gen.*|*_pb.go|*_pb2.py|*.pb.go) return 0 ;;
  esac
  return 1
}
is_in_exceptions() {
  [[ -f "$EXCEPTIONS_FILE" ]] || return 1
  local rel="${1#$REPO_ROOT/}"
  grep -Fxq "$rel" "$EXCEPTIONS_FILE"
}
collect_targets() {
  if (( ${#TARGETS[@]} > 0 )); then printf '%s\n' "${TARGETS[@]}"
  elif (( CHANGED_ONLY )); then
    git -C "$REPO_ROOT" diff --name-only --diff-filter=AM origin/main...HEAD 2>/dev/null \
      || git -C "$REPO_ROOT" diff --name-only --diff-filter=AM HEAD
  else git -C "$REPO_ROOT" ls-files; fi
}

violations_hard=(); violations_soft=()
while IFS= read -r f; do
  [[ -z "$f" ]] && continue
  abs="$f"; [[ "$abs" != /* ]] && abs="$REPO_ROOT/$f"
  [[ -f "$abs" ]] || continue
  is_excluded "$abs" && continue
  is_in_exceptions "$abs" && continue
  loc=$(wc -l < "$abs" | tr -d ' ')
  if (( loc > HARD )); then violations_hard+=("$loc	$f")
  elif (( loc > SOFT )); then violations_soft+=("$loc	$f"); fi
done < <(collect_targets)

if (( JSON )); then
  printf '{"hard":['
  first=1; for v in "${violations_hard[@]}"; do
    loc="${v%%	*}"; path="${v#*	}"
    (( first )) || printf ','; first=0
    printf '{"loc":%s,"path":"%s"}' "$loc" "$path"
  done
  printf '],"soft":['
  first=1; for v in "${violations_soft[@]}"; do
    loc="${v%%	*}"; path="${v#*	}"
    (( first )) || printf ','; first=0
    printf '{"loc":%s,"path":"%s"}' "$loc" "$path"
  done
  printf ']}\n'
else
  if (( ${#violations_soft[@]} > 0 )); then
    echo "::warning:: $((${#violations_soft[@]})) file(s) exceed soft target ($SOFT lines):"
    printf '  %s\n' "${violations_soft[@]}" | sort -rn
  fi
  if (( ${#violations_hard[@]} > 0 )); then
    echo "::error:: $((${#violations_hard[@]})) file(s) exceed HARD CAP ($HARD lines) — split required:"
    printf '  %s\n' "${violations_hard[@]}" | sort -rn
  fi
fi
(( ${#violations_hard[@]} == 0 ))
```

Make executable: `chmod +x scripts/check-loc.sh`.

### 1.6 `scripts/githooks/pre-commit`

```bash
#!/usr/bin/env bash
# pre-commit — enforces structural guardrails.
#
# 1. Blocks commits that introduce a non-test, non-generated source file > 500 LOC.
# 2. Blocks commits touching migrations/ unless commit message contains [migration-approved].
# 3. Blocks edits to guardrail files unless [guardrail-change] is in the commit message.

set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel)"

mapfile -t staged < <(git diff --cached --name-only --diff-filter=ACM)
[[ ${#staged[@]} -eq 0 ]] && exit 0

# 1. LOC budget on staged files.
loc_targets=()
for f in "${staged[@]}"; do
  [[ -f "$REPO_ROOT/$f" ]] && loc_targets+=("$REPO_ROOT/$f")
done
if [[ ${#loc_targets[@]} -gt 0 ]]; then
  if ! "$REPO_ROOT/scripts/check-loc.sh" "${loc_targets[@]}"; then
    echo; echo "Commit blocked: file-size budget violated."
    echo "Split the file (preferred) or add to .claude/rules/loc-exceptions.txt."
    exit 1
  fi
fi

# 2. Migrations frozen unless approved.
if printf '%s\n' "${staged[@]}" | grep -qE '(^|/)(migrations|alembic/versions)/'; then
  if ! grep -q '\[migration-approved\]' "$(git rev-parse --git-dir)/COMMIT_EDITMSG" 2>/dev/null; then
    echo "Commit blocked: this change touches a migrations directory."
    echo "Add '[migration-approved]' to your commit message if approved."
    exit 1
  fi
fi

# 3. Guardrail files protected.
guarded='^(\.claude/settings\.json|\.claude/rules/loc-exceptions\.txt|scripts/check-loc\.sh|scripts/githooks/pre-commit|AGENTS\.(python|go|typescript)\.md)$'
if printf '%s\n' "${staged[@]}" | grep -qE "$guarded"; then
  if ! grep -q '\[guardrail-change\]' "$(git rev-parse --git-dir)/COMMIT_EDITMSG" 2>/dev/null; then
    echo "Commit blocked: this change modifies guardrail files."
    echo "Add '[guardrail-change]' to your commit message and explain why in the body."
    exit 1
  fi
fi
exit 0
```

### 1.7 `scripts/install-hooks.sh`

```bash
#!/usr/bin/env bash
# install-hooks.sh — installs git hooks that enforce repo guardrails locally.
# Idempotent. Safe to re-run. Run once per clone: bash scripts/install-hooks.sh
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
HOOKS_DIR="$REPO_ROOT/.git/hooks"
SRC_DIR="$REPO_ROOT/scripts/githooks"

[[ -d "$REPO_ROOT/.git" ]] || { echo "Not a git repository: $REPO_ROOT" >&2; exit 1; }
mkdir -p "$HOOKS_DIR"
for hook in pre-commit; do
  src="$SRC_DIR/$hook"; dst="$HOOKS_DIR/$hook"
  if [[ -f "$src" ]]; then cp "$src" "$dst"; chmod +x "$dst"; echo "installed: $dst"; fi
done
echo "Done. Hooks active for this clone."
```

### 1.8 CI additions (`.github/workflows/ci.yaml` or `.gitea/workflows/ci.yaml`)

Add a `loc-budget` job that fails on hard violations:

```yaml
jobs:
  loc-budget:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Check file-size budget
        run: bash scripts/check-loc.sh --changed

  python-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: ruff
        run: pip install ruff && ruff check .
      - name: mypy on new modules
        run: pip install mypy && mypy --strict services/ repositories/ domain/

  go-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: golangci-lint
        uses: golangci/golangci-lint-action@v4
        with: { version: latest }

  ts-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npx tsc --noEmit && npx next build

  contract-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pytest tests/contracts/ -v

  license-sbom-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: anchore/sbom-action@v0
      - uses: anchore/scan-action@v3
```

---


### 1.9 `AGENTS.python.md` (Python / FastAPI)

````markdown
# AGENTS.python.md — Python Service Conventions

## Layered architecture

```
<service>/
├── api/                # HTTP layer — routers only. Thin (≤30 LOC per handler).
│   └── <domain>_routes.py
├── services/           # Business logic. Pure-ish; no FastAPI imports.
├── repositories/       # DB access. Owns SQLAlchemy session usage.
├── domain/             # Value objects, enums, domain exceptions.
├── schemas/            # Pydantic models, split per domain. Never one giant schemas.py.
└── db/models/          # SQLAlchemy ORM, one module per aggregate. __tablename__ frozen.
```

Dependency direction: `api → services → repositories → db.models`. Lower layers must not import upper.

## Routers
- One `APIRouter` per domain file. Handlers ≤30 LOC.
- Parse request → call service → map domain errors → return response model.
- Inject services via `Depends`. No globals.

```python
@router.post("/items", response_model=ItemRead, status_code=201)
async def create_item(
    payload: ItemCreate,
    service: ItemService = Depends(get_item_service),
    tenant_id: UUID = Depends(get_tenant_id),
) -> ItemRead:
    with translate_domain_errors():
        return await service.create(tenant_id, payload)
```

## Domain errors + translator

```python
# domain/errors.py
class DomainError(Exception): ...
class NotFoundError(DomainError): ...
class ConflictError(DomainError): ...
class ValidationError(DomainError): ...
class PermissionError(DomainError): ...

# api/_http_errors.py
from contextlib import contextmanager
from fastapi import HTTPException

@contextmanager
def translate_domain_errors():
    try: yield
    except NotFoundError as e:   raise HTTPException(404, str(e)) from e
    except ConflictError as e:   raise HTTPException(409, str(e)) from e
    except ValidationError as e: raise HTTPException(400, str(e)) from e
    except PermissionError as e: raise HTTPException(403, str(e)) from e
```

## Services
- Constructor takes repository interface, not concrete.
- No FastAPI / HTTP knowledge.
- Raise domain exceptions, never HTTPException.

## Repositories
- Intent-named methods (`get_pending_for_tenant`), not CRUD-named (`select_where`).
- Session injected. No business logic.
- Return ORM models or domain VOs; never `Row`.

## Schemas (Pydantic v2)
- One module per domain. ≤300 lines.
- `model_config = ConfigDict(from_attributes=True, frozen=True)` for reads.
- Separate `*Create`, `*Update`, `*Read`.

## Tests
- `tests/unit/`, `tests/integration/`, `tests/contracts/`.
- Unit tests mock repository via `AsyncMock`.
- Integration tests use real Postgres from compose via transactional fixture (rollback per test).
- Contract tests diff `/openapi.json` against `tests/contracts/openapi.baseline.json`.
- Naming: `test_<unit>_<scenario>_<expected>.py::TestX::test_method`.
- `pytest-asyncio` mode = `auto`. Coverage target: 80% new code.

## Tooling
- `ruff check` + `ruff format` (line length 100).
- `mypy --strict` on `services/`, `repositories/`, `domain/` first. Expand outward via per-module overrides in mypy.ini:

```ini
[mypy]
strict = True

[mypy-<service>.services.*]
strict = True

[mypy-<service>.legacy.*]
# Legacy modules not yet refactored — expand strictness over time.
ignore_errors = True
```

## What you may NOT do
- Add a new migration.
- Rename `__tablename__`, column, or enum value.
- Change route contract without simultaneous consumer update.
- Catch `Exception` broadly.
- Put business logic in a router or a Pydantic validator.
- Create a file > 500 lines.
````

### 1.10 `AGENTS.go.md` (Go / Gin or chi)

````markdown
# AGENTS.go.md — Go Service Conventions

## Layered architecture (Standard Go Project Layout + hexagonal)

```
<service>/
├── cmd/server/main.go        # Thin: parse flags → app.New → app.Run. < 50 LOC.
├── internal/
│   ├── app/                  # Wiring: config + DI + lifecycle.
│   ├── domain/<aggregate>/   # Pure types, interfaces, errors. No I/O.
│   ├── service/<aggregate>/  # Business logic. Depends on domain interfaces.
│   ├── repository/postgres/<aggregate>/   # Concrete repos.
│   ├── transport/http/
│   │   ├── handler/<aggregate>/
│   │   ├── middleware/
│   │   └── router.go
│   └── platform/             # DB pool, logger, config, tracing.
└── pkg/                      # Importable by other repos. Empty unless needed.
```

Direction: `transport → service → domain ← repository`. `domain` imports no siblings.

## Handlers
- ≤40 LOC. Bind → call service → map error via `httperr.Write(c, err)` → respond.

```go
func (h *ItemHandler) Create(c *gin.Context) {
    var req CreateItemRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        httperr.Write(c, httperr.BadRequest(err)); return
    }
    out, err := h.svc.Create(c.Request.Context(), req.ToInput())
    if err != nil { httperr.Write(c, err); return }
    c.JSON(http.StatusCreated, out)
}
```

## Errors — single `httperr` package
```go
switch {
case errors.Is(err, domain.ErrNotFound): return 404
case errors.Is(err, domain.ErrConflict): return 409
case errors.As(err, &validationErr):     return 422
default:                                  return 500
}
```
Never `panic` in request handling. Recovery middleware logs and returns 500.

## Services
- Struct + constructor + interface methods. No package-level state.
- `context.Context` first arg always.
- Return `(value, error)`. Wrap with `fmt.Errorf("create item: %w", err)`.
- Domain errors as sentinel vars or typed; match with `errors.Is` / `errors.As`.

## Repositories
- Interface in `domain/<aggregate>/repository.go`. Impl in `repository/postgres/<aggregate>/`.
- One file per query group; no file > 500 LOC.
- `pgx`/`sqlc` over hand-rolled SQL. No ORM globals. Everything takes `ctx`.

## Tests
- Co-located `*_test.go`. Table-driven for service logic.
- Handlers via `httptest.NewRecorder`.
- Repos via `testcontainers-go` (or the compose Postgres). Never mocks at SQL boundary.
- Coverage target: 80% on `service/`.

## Tooling (`golangci-lint` strict config)
- Linters: `errcheck, govet, staticcheck, revive, gosec, gocyclo(max 15), gocognit(max 20), unused, ineffassign, errorlint, nilerr, nolintlint, contextcheck`.
- `gofumpt` formatting. `go vet ./...` clean. `go mod tidy` clean.

## What you may NOT do
- Touch DB schema/migrations.
- Add a new top-level package under `internal/` without review.
- `import "C"`, unsafe, reflection-heavy code.
- Non-trivial setup in `init()`. Wire in `internal/app`.
- File > 500 lines.
- Change route contract without updating consumers.
````

### 1.11 `AGENTS.typescript.md` (TypeScript / Next.js)

````markdown
# AGENTS.typescript.md — TypeScript / Next.js Conventions

## Layered architecture (Next.js 15 App Router)

```
app/
├── <route>/
│   ├── page.tsx              # Server Component by default. ≤200 LOC.
│   ├── layout.tsx
│   ├── _components/          # Private folder; colocated UI. Each file ≤300 LOC.
│   ├── _hooks/               # Client hooks for this route.
│   ├── _server/              # Server actions, data loaders for this route.
│   └── loading.tsx / error.tsx
├── api/<domain>/route.ts     # Thin handler. Delegates to lib/server/<domain>/.
lib/
├── <domain>/                 # Pure helpers, types, zod schemas. Reusable.
└── server/<domain>/          # Server-only logic; uses "server-only".
components/                   # Truly shared, app-wide components.
```

Server vs Client: default is Server Component. Add `"use client"` only when state/effects/browser APIs needed. Push client boundary as deep as possible.

## API routes (route.ts)
- One handler per HTTP method, ≤40 LOC.
- Validate with `zod`. Reject invalid → 400.
- Delegate to `lib/server/<domain>/`.

```ts
export async function POST(req: Request) {
  const parsed = CreateItemSchema.safeParse(await req.json());
  if (!parsed.success)
    return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
  const result = await itemService.create(parsed.data);
  return NextResponse.json(result, { status: 201 });
}
```

## Page components
- Pages > 300 lines → split into colocated `_components/`.
- Server Components fetch data; pass plain objects to Client Components.
- No data fetching in `useEffect` for server-renderable data.
- State: prefer URL state (`searchParams`) + Server Components over global stores.

## Types — barrel re-export pattern for splitting monolithic type files

```ts
// lib/sdk/types/index.ts
export * from './enums'
export * from './vendor'
export * from './dsfa'
// consumers still `import { Foo } from '@/lib/sdk/types'`
```

Rules: no `any`. No `as unknown as`. All DTOs are zod schemas; infer via `z.infer`.

## Tests
- Unit: **Vitest** (`*.test.ts`/`*.test.tsx`), colocated.
- Hooks: `@testing-library/react` `renderHook`.
- E2E: **Playwright** (`tests/e2e/`), one spec per top-level page minimum.
- Coverage: 70% on `lib/`, smoke on `app/`.

## Tooling
- `tsc --noEmit` clean (strict, `noUncheckedIndexedAccess: true`).
- ESLint with `@typescript-eslint`, type-aware rules on.
- `next build` clean. No `@ts-ignore`. `@ts-expect-error` only with a reason comment.

## What you may NOT do
- Business logic in `page.tsx` or `route.ts`.
- Cross-app module imports.
- `dangerouslySetInnerHTML` without explicit sanitization.
- Backend API calls from Client Components when a Server Component/Action would do.
- Change route contract without updating consumers in the same change.
- File > 500 lines.
- Globally disable lint/type rules — fix the root cause.
````

---


## 2. Phase plan — behavior-preserving refactor

Work in phases. Each phase ends green (tests pass, build clean, contract baseline unchanged). Do **not** skip ahead.

### Phase 0 — Foundation (single PR, low risk)

**Goal:** Set up rails. No code refactors yet.

1. Drop in all files from Section 1. Install hooks: `bash scripts/install-hooks.sh`.
2. Populate `.claude/rules/loc-exceptions.txt` with grandfathered entries (one line each, with a comment rationale) so CI doesn't fail day 1.
3. Append the non-negotiable rules block to root `CLAUDE.md`.
4. Add per-language `AGENTS.*.md` at repo root.
5. Add the CI jobs from §1.8.
6. Per-service `README.md` + `CLAUDE.md` stubs: what it does, run/test commands, layered architecture diagram, env vars, API surface link.

**Verification:** CI green; loc-budget job passes with allowlist; next Claude session loads the rules automatically.

### Phase 1 — Backend service (Python/FastAPI)

**Critical targets:** any `routes.py` / `schemas.py` / `repository.py` / `models.py` over 500 LOC.

**Steps:**

1. **Snapshot the API contract:** `curl /openapi.json > tests/contracts/openapi.baseline.json`. Add a contract test that diffs current vs baseline and fails on any path/method/param drift.
2. **Characterization tests first.** For each oversized route file, add `TestClient` tests exercising every endpoint (happy path + one error path). Use `httpx.AsyncClient` + factory fixtures.
3. **Split models.py per aggregate.** Keep a shim: `from <service>.db.models import *` re-exports so existing imports keep working. One module per aggregate; `__tablename__` unchanged (no migration).
4. **Split schemas.py** similarly with a re-export shim.
5. **Extract service layer.** Each route handler delegates to a `*Service` class injected via `Depends`. Handlers shrink to ≤30 LOC.
6. **Repository extraction** from the giant repository file; one class per aggregate.
7. **`mypy --strict` scoped to new packages first.** Expand outward via `mypy.ini` per-module overrides.
8. **Tests:** unit tests per service (mocked repo), repo tests against a transactional fixture (real Postgres), integration tests at API layer.

**Gotchas we hit:**
- Tests that patch module-level symbols (e.g. `SessionLocal`, `scan_X`) break when you move logic behind `Depends`. Fix: re-export the symbol from the route module, or have the service lookup use the module-level symbol directly so the patch still takes effect.
- `from __future__ import annotations` can break Pydantic TypeAdapter forward refs. Remove it where it conflicts.
- Sibling test file status codes drift when you introduce the domain-error translator (e.g. 422 → 400). Update assertions in the same commit.

**Verification:** all pytest files green. Characterization tests green. Contract test green (no drift). `mypy` clean on new packages. Coverage ≥ baseline + 10%.

### Phase 2 — Go backend

**Critical targets:** any handler / store / rules file over 500 LOC.

**Steps:**
1. OpenAPI/Swagger snapshot (or generate via `swag`) → contract tests.
2. Generate handler-level tests with `httptest` for every endpoint pre-refactor.
3. Define hexagonal layout (see AGENTS.go.md). Move incrementally with type aliases for back-compat where needed.
4. Replace ad-hoc error handling with `errors.Is/As` + a single `httperr` package.
5. Add `golangci-lint` strict config; fix new findings only (don't chase legacy lint).
6. Table-driven service tests. `testcontainers-go` for repo layer.

**Verification:** `go test ./...` passes; `golangci-lint run` clean; contract tests green; no DB schema diff.

### Phase 3 — Frontend (Next.js)

**Biggest beast — expect this to dominate.** Critical targets: `page.tsx` / monolithic types / API routes over 500 LOC.

**Per oversized page:**
1. Extract presentational components into `app/<route>/_components/` (private folder, Next.js convention).
2. Move data fetching into Server Components / Server Actions; Client Components become small.
3. Hooks → `app/<route>/_hooks/`.
4. Pure helpers → `lib/<domain>/`.
5. Add Vitest unit tests for hooks and pure helpers; Playwright smoke tests for each top-level page.

**Monolithic types file:** use barrel re-export pattern.
- Create `types/` directory with domain files.
- Create `types/index.ts` with `export * from './<domain>'` lines.
- **Critical:** TypeScript won't allow both `types.ts` AND `types/index.ts` — delete the file, atomic swap to directory.

**API routes (`route.ts`):** same router→service split as backend. Each `route.ts` becomes a thin handler delegating to `lib/server/<domain>/`.

**Endpoint preservation:** if any internal route URL changes, grep every consumer (SDK packages, developer portal, sibling apps) and update in the same change.

**Gotchas:**
- Pre-existing type bugs often surface when you try to build. Fix them as drive-by if they block your refactor; otherwise document in a separate follow-up.
- `useClient` component imports from `'../provider'` that rely on re-exports: preserve the re-export or update importers in the same commit.
- Next.js build can fail at page-manifest stage with unrelated prerender errors. Run `next build` fresh (not from cache) to see real status.

**Verification:** `next build` clean; `tsc --noEmit` clean; Playwright smoke tests pass; visual diff check on key pages (manual + screenshots in PR).

### Phase 4 — SDKs & smaller services

Apply the same patterns at smaller scale:
- **SDK packages (0 tests):** add Vitest unit tests for public surface before/while splitting.
- **Manager/Client classes:** extract config defaults, side-effect helpers (e.g. Google Consent Mode wiring), framework adapters into sibling files. Keep the main class as orchestration.
- **Framework adapters (React/Vue/Angular):** each component/composable/service/module goes in its own sibling file; the entry `index.ts` is a thin barrel of re-exports.
- **Doc monoliths (`index.md` thousands of lines):** split per topic with mkdocs nav.

### Phase 5 — CI hardening & governance

1. Promote `loc-budget` from warning → blocking once the allowlist has drained to legitimate exceptions only.
2. Add mutation testing in nightly (`mutmut` for Python, `gomutesting` for Go).
3. Add `dependabot`/`renovate` for npm + pip + go mod.
4. Add release tagging workflow.
5. Write ADRs (`docs/adr/`) capturing the architecture decisions from phases 1–3.
6. Distill recurring patterns into `.claude/rules/` updates.

---

## 3. Agent prompt templates

When the work volume is big, parallelize with subagents. These prompts were battle-tested in practice.

### 3.1 Backend route file split (Python)

> You are working in `<repo>` on branch `<branch>`. Every source file must be under 500 LOC (hard cap enforced by a PreToolUse hook); soft target 300.
>
> **Task:** split `<path/to/file>_routes.py` (NNN LOC) following the router → service → repository layering described in `AGENTS.python.md`.
>
> **Steps:**
> 1. Snapshot the relevant slice of `/openapi.json` and add a contract test that pins current behavior.
> 2. Add characterization tests for every endpoint in this file (happy path + one error path) using `httpx.AsyncClient`.
> 3. Extract each route handler's business logic into a `<domain>Service` class in `<service>/services/<domain>_service.py`. Inject via `Depends(get_<domain>_service)`.
> 4. Raise domain errors (`NotFoundError`, `ConflictError`, `ValidationError`), never `HTTPException`. Use the `translate_domain_errors()` context manager in handlers.
> 5. Move DB access to `<service>/repositories/<domain>_repository.py`. Session injected.
> 6. Split Pydantic schemas from the giant `schemas.py` into `<service>/schemas/<domain>.py` if >300 lines.
>
> **Constraints:**
> - Behavior preservation. No route rename/method/status/schema changes.
> - Tests that patch module-level symbols must keep working — re-export the symbol or refactor the lookup so the patch still takes effect.
> - Run `pytest` after each step. Commit each file as its own commit.
> - Push at end: `git push origin <branch>`.
>
> When done, report: (a) new LOC counts, (b) test results, (c) mypy status, (d) commit SHAs. Under 300 words.

### 3.2 Go handler file split

> You are working in `<repo>` on branch `<branch>`. Hard cap 500 LOC.
>
> **Task:** split `<path>/handlers/<domain>_handler.go` (NNN LOC) into a hexagonal layout per `AGENTS.go.md`.
>
> **Steps:**
> 1. Add `httptest` tests for every endpoint pre-refactor.
> 2. Define `internal/domain/<aggregate>/` with types + interfaces + sentinel errors.
> 3. Create `internal/service/<aggregate>/` with business logic implementing domain interfaces.
> 4. Create `internal/repository/postgres/<aggregate>/` splitting queries by group.
> 5. Thin handlers under `internal/transport/http/handler/<aggregate>/`. Each handler ≤40 LOC. Error mapping via `internal/platform/httperr`.
> 6. Use `errors.Is` / `errors.As` for domain error matching.
>
> **Constraints:**
> - No DB schema change.
> - Table-driven service tests. `testcontainers-go` (or compose Postgres) for repo tests.
> - `golangci-lint run` clean.
>
> Report new LOC, test status, lint status, commit SHAs. Under 300 words.

### 3.3 Next.js page split (the one we parallelized heavily)

> You are working in `<repo>` on branch `<branch>`. Every source file must be under 500 LOC (hard cap enforced by a PreToolUse hook); soft target 300. Other agents are working on OTHER pages in parallel — stay in your lane.
>
> **Task:** split the following Next.js 15 App Router client pages into colocated components so each `page.tsx` drops below 500 LOC.
>
> 1. `admin-compliance/app/sdk/<page-a>/page.tsx` (NNNN LOC)
> 2. `admin-compliance/app/sdk/<page-b>/page.tsx` (NNNN LOC)
>
> **Pattern** (reference `admin-compliance/app/sdk/<already-split-example>/` for "done"):
> - Create `_components/` subdirectory (Next.js private folder, won't create routes).
> - Extract each logically-grouped section (forms, tables, modals, tabs, headers, cards) into its own component file. Name files after the component.
> - Create `_hooks/` for custom hooks that were inline.
> - Create `_types.ts` or `_data.ts` for hoisted types or data arrays.
> - Remaining `page.tsx` wires extracted pieces — aim for under 300 LOC, hard cap 500.
> - Preserve `'use client'` when present on original.
> - DO NOT rename any exports that other files import. Grep first before moving.
>
> **Constraints:**
> - Behavior preservation. No logic changes, no improvements.
> - Imports must resolve (relative `./_components/Foo`).
> - Run `cd admin-compliance && npx next build` after each file is done. Don't commit broken builds.
> - DO NOT edit `.claude/settings.json`, `scripts/check-loc.sh`, `loc-exceptions.txt`, or any `AGENTS.*.md`.
> - Commit each page as its own commit: `refactor(admin): split <name> page.tsx into colocated components`. HEREDOC body, include `Co-Authored-By:` trailer.
> - Pull before push: `git pull --rebase origin <branch>`, then `git push origin <branch>`.
>
> **Coordination:** DO NOT touch `<list of pages other agents own>`. You own only `<your pages>`.
>
> When done, report: (a) each file's new LOC count, (b) how many `_components` were created, (c) whether `next build` is clean, (d) commit SHAs. Under 300 words.
>
> If the LOC hook blocks a Write, split further. If you hit rate limits partway, commit what's done and report progress honestly.

### 3.4 Monolithic types file split (TypeScript)

> `<repo>`, branch `<branch>`. Hard cap 500 LOC.
>
> **Task:** split `<lib>/types.ts` (NNNN LOC) into per-domain modules under `<lib>/types/`.
>
> **Steps:**
> 1. Identify domain groupings (enums, API DTOs, one group per business aggregate).
> 2. Create `<lib>/types/` directory with `<domain>.ts` files.
> 3. Create `<lib>/types/index.ts` barrel: `export * from './<domain>'` per file.
> 4. **Atomic swap:** delete the old `types.ts` in the same commit as the new `types/` directory. TypeScript won't resolve both a file and a directory with the same stem.
> 5. Grep every consumer — imports from `'<lib>/types'` should still work via the barrel. No consumer file changes needed unless there's a name collision.
> 6. Resolve collisions by renaming the less-canonical export (e.g. if two modules both export `LegalDocument`, rename the RAG one to `RagLegalDocument`).
>
> **Verification:** `tsc --noEmit` clean, `next build` clean.
>
> Report new LOC per file, collisions resolved, consumer updates, commit SHAs.

### 3.5 Agent orchestration rules (from hard-won experience)

When you spawn multiple agents in parallel:

1. **Own disjoint paths.** Give each agent a bounded list of files under specific directories. Spell out the "do NOT touch" list explicitly.
2. **Always instruct `git pull --rebase origin <branch>` before push.** Agents running in parallel will push and cause non-fast-forward rejects without this.
3. **Instruct `commit each file as its own commit`** — not a single mega-commit. Makes revert surgical.
4. **Ask for concise reports (≤300 words):** new LOC counts, component counts, build status, commit SHAs.
5. **Tell them to commit partial progress on rate-limit.** If they don't, their partial work lives in the working tree and you have to chase it with `git status` after. (We hit this — 4 agents silently left uncommitted work.)
6. **Don't give an agent more than 2 big files at once.** Each page-split in practice took ~10–20 minutes + ~150k tokens. Two is a comfortable batch.
7. **Reference a prior "done" example.** Commit SHAs are gold — the agent can inspect exactly the style you want.
8. **Run one final `next build` / `pytest` / `go test` yourself after all agents finish.** Agent reports of "build clean" can be scoped (e.g. only their files); you want the whole-repo gate.

---

## 4. Workflow loop (per file)

```
1. Read the oversized file end to end. Identify 3–6 extraction sections.
2. Write characterization test (if backend) — pin behavior.
3. Create the sibling files one at a time.
   - If the PreToolUse hook blocks (file still > 500), split further.
4. Edit the root file: replace extracted bodies with imports + delegations.
5. Run the full verification: pytest / next build / go test.
6. Run LOC check: scripts/check-loc.sh <changed files>
7. Commit with a scoped message and a 1–2 line body explaining why.
8. Push.
```

## 5. Commit message conventions

```
refactor(<area>): <one-line what, not how>

<optional 1-3 sentence body: what split changed + verification result>
<LOC table: before → after per file>
<non-behavior changes flagged as drive-by fixes, with reason>

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
```

Markers that unlock pre-commit guards:
- `[migration-approved]` — allows changes under `migrations/` / `alembic/versions/`.
- `[guardrail-change]` — allows changes to `.claude/settings.json`, `.claude/rules/loc-exceptions.txt`, `scripts/check-loc.sh`, `scripts/githooks/pre-commit`, or any `AGENTS.*.md`.

Good examples from our session:
- `refactor(consent-sdk): split ConsentManager + framework adapters under 500 LOC`
- `refactor(compliance-sdk): split client/provider/embed/state under 500 LOC`
- `refactor(admin): split whistleblower page.tsx + restore scope helpers`
- `chore: document data-catalog + legacy-service LOC exceptions` (with `[guardrail-change]` body)

## 6. Verification commands cheatsheet

```bash
# LOC budget
scripts/check-loc.sh --changed               # only changed files
scripts/check-loc.sh                         # whole repo
scripts/check-loc.sh --json                  # for CI parsing

# Python
pytest --cov=<package> --cov-report=term-missing
ruff check .
mypy --strict <package>/services <package>/repositories

# Go
go test ./... -cover
golangci-lint run
go vet ./...

# TypeScript
npx tsc --noEmit
npx next build                               # from the Next.js app dir
npm test -- --run                            # vitest one-shot
npx playwright test tests/e2e                # e2e smoke

# Contracts
pytest tests/contracts/                       # OpenAPI snapshot diff
```

## 7. Out of scope (don't drift)

- DB schema / migrations — unless separate green-lit plan.
- New features. This is a refactor.
- Public endpoint renames without simultaneous consumer fix-up (exception: intra-monorepo URLs when you do the grep sweep).
- Unrelated dead code cleanup — do it in a separate PR.
- Bundling refactors across services in one commit — one service = one commit.

## 8. Memory / session handoff

If using Claude Code with persistent memory, save a `project_refactor_status.md` in your memory store after each phase:
- What's done (files split, LOC before → after).
- What's in progress (current file, blocker if any).
- What's deferred (pre-existing bugs surfaced but left for follow-up).
- Key patterns established (so next session doesn't rediscover them).

This lets you resume after context compacts or after rate-limit windows without losing the thread.

---

That's the whole methodology. Install Section 1, follow Section 2 phase-by-phase, use Section 3 to parallelize the grind. The guardrails do the policing so you don't have to remember anything.

