- README.md: new root README with CI badge, per-service table, quick start, CI pipeline table, file budget docs, and links to production endpoints - CONTRIBUTING.md: dev environment setup, pre-commit checklist, commit marker reference, architecture non-negotiables, Claude Code section - CLAUDE.md: insert First-Time Setup & Claude Code Onboarding section with branch guard, hook explanation, guardrail marker table, and staging rules - REFACTOR_PLAYBOOK.md: commit existing refactor playbook doc - .gitignore: add dist/, .turbo/, pnpm-lock.yaml, .pnpm-store/ to prevent SDK build artifacts from appearing as untracked files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
916 lines
40 KiB
Markdown
916 lines
40 KiB
Markdown
|
||
---
|
||
|
||
## 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.
|
||
|