Output style: Terse. Bullets > prose. Sacrifice grammar while preserving clarity. No trailing summaries.
FastAPI service — parses and standardizes US (USPS Pub 28) and Canadian (libpostal sidecar) addresses. systemd+uvicorn on port 8000. libpostal sidecar on port 4400 (pelias/libpostal-service Docker, infra/libpostal.service).
SocratiCode is the preferred semantic-search tool here once indexed (local
Qdrant store + on-disk graph; manifest .socraticodecontextartifacts.json).
Its MCP tools are deferred — schemas load only after the ToolSearch
prefetch that .claude/hooks/socraticode-reminder.sh prints each session.
Negative rule. Use SocratiCode MCP tools first for semantic questions
("where is X", "how does Y work", "what depends on Z"). Reach for grep/rg
only on exact strings (error messages, log lines, known symbols). Reserve the
Explore subagent for path-pattern walks (*.py under src/address_validator/routers/),
not semantic search.
| Goal | Tool |
|---|---|
| Where is X defined / how does Y work / what touches Z | codebase_search |
| Exact string or regex (errors, log lines, known symbols) | grep / rg |
| Imports/dependents of a file · blast radius of a change | codebase_graph_query / codebase_impact |
Full tool table, prefetch query, per-tool guidance: docs/SOCRATICODE.md.
- Schema, status vocabularies, migrations →
codebase_searchor the source, nevercodebase_context_search. No schema artifact is declared, so a context search answers these from dateddocs/plans— wrongly (GH #218). codebase_context_searchis for documented contracts (log levels, DPV map, Pub 28, style, dep policy) and is the only path todocs/plansrationale.unresolvedPctis a call-edge statistic; import edges are exact, so an emptycodebase_graph_query/codebase_impactanswer means no dependents. Evidence: docs/SOCRATICODE.md.
See docs/ARCHITECTURE.md for the full module map.
models.pyis the single source of truth for API contracts; field name/type changes are breaking- Response models use geography-neutral names:
region,postal_code standardizedfield: two-space separator between logical address lines (USPS single-line convention)- Address input capped at 1000 chars (
Field(max_length=1000)) warnings: list[str]on all response models; empty on clean input. Every warning string is defined incore/warnings.py(single source of truth) and catalogued indocs/WARNINGS.md; a drift test enforces sync — never inline a new warning literalValidationResult.statusvocabulary is defined incore/validation_status.py(single source of truth) and catalogued indocs/VALIDATION-STATUS.md; a drift test (tests/unit/test_validation_status_catalogue.py) enforces sync across theLiteral, thevalidated_addressesCheckConstraint, the DPV→status map, and adminVS_META— never inline a new status literal. Adding a status also requires a new Alembic migration wideningck_validated_addresses_status- Pipeline version (
core/pipeline_version.py, single source of truth) stamps every cachedvalidated_addressesrow; code changes that alter parse/standardize output must bumpPIPELINE_CODE_VERSION— drift testtests/unit/test_pipeline_output_pin.pyenforces (re-pin hash + bump together). CRF model swaps viaCUSTOM_MODEL_PATHinvalidate automatically (fingerprint) db/tables.pymirrors the Alembic-head schema and is never used for DDL; drift testtests/unit/db/test_schema_drift.pycompares it column-by-column (nullability, generated, identity, type) against a migrated DB — schema changes need a migration first, then the mirrored Table defcomponentstakes precedence overaddresswhen both supplied- All request models accepting a country must inherit
CountryRequestMixin
- All
/api/*requireX-API-Key; value fromAPI_KEYenv var - Key at
/etc/address-validator/.env(mode 640); loaded viaEnvironmentFile=in systemd unit - Open routes:
GET /,/docs,/redoc,/openapi.json,GET /api/v2/health /api/v2/health: HTTP 503 when degraded; libpostal state does NOT affect HTTP status. Response shape → docs/DEPLOYMENT.md- Google provider uses ADC — no API key. IAM:
roles/addressvalidation.user,roles/cloudquotas.viewer,roles/monitoring.viewer - Admin (
/admin/*) requires exe.dev proxy auth (X-ExeDev-UserID,X-ExeDev-Email) - CORS: denied by default (GH #35).
ALLOWED_ORIGINSenv var grants browser origins — comma-separated list, or*for any; unset = noAccess-Control-Allow-Originever emitted
No PII at INFO+. Address content never in log messages at INFO or above. See docs/LOGGING.md for event/level table.
Core env vars (see docs/VALIDATION-PROVIDERS.md for full reference):
| Variable | Values | Default |
|---|---|---|
VALIDATION_PROVIDER |
none, usps, google, comma-sep list |
none |
VALIDATION_CACHE_DSN |
PostgreSQL DSN | — (required when non-null) |
VALIDATION_CACHE_TTL_DAYS |
non-negative int | 30 |
CUSTOM_MODEL_PATH |
path to .crfsuite file |
— (bundled usaddress model) |
Quick ops (see docs/DEPLOYMENT.md for full reference):
- Restart:
sudo systemctl restart address-validator(>10 in 600s → staysfailed;reset-failedfirst, #248) - Logs:
journalctl -u address-validator -f - Re-install units:
sudo infra/install-units.sh [unit…](restart the service if its unit changed);infra/install-units.sh --checkflags drift — re-run after moving any path a unit references (#228) - Pre-commit hooks:
uv run pre-commit install - Disk hygiene: weekly timer (
infra/disk-hygiene.sh, Sun 05:00 UTC) prunes VS Code server builds, npm/uv caches, orphaned worktrees; dry-run withinfra/disk-hygiene.sh --dry-run
Single-VM dev+prod model (exe.dev):
- Port 8000 = systemd production service (main worktree) — never start uvicorn manually on this port
- Port 8001 = dev server (active git worktree,
--reload) - No swap; a session can't be OOM-killed, so services die instead — never run a big install unreserved → docs/HOST-MEMORY.md
- exe.dev proxy: dev server accessible at
https://address-validator.exe.xyz:8001/ - All development work happens on git worktrees — never modify the main worktree directly
- Worktrees:
.worktrees/<branch-slug>/only, via theusing-git-worktreesscripts — nevergit worktree remove - New worktree bootstrap:
uv syncandbash .skills/doctor.sh(~2 s) — no.venvis linked in (.skills/worktree_venv=none: under thelinkdefault a worktree'suv syncprunes the port-8000 service's venv), and every vendored skill/hook symlink dangles until the doctor initializes submodules (docs/DEPLOYMENT.md) - Dev server: from the worktree root,
PYTHONPATH=src+--log-configboth mandatory (boot fails without) - A worktree that has run
doctor.shneedsworktree-destroy.sh <branch> --force; full worktree + dev-server reference → docs/DEPLOYMENT.md
| File | Contents | Loaded by |
|---|---|---|
/etc/address-validator/.env |
Production secrets (API_KEY, DSN, provider creds, CUSTOM_MODEL_PATH, NOTIFIER_*) + LOG_LEVEL |
systemd (required) |
/home/exedev/address-validator/.env |
Dev/agent secrets (GH_TOKEN* PATs) |
systemd (optional with - prefix), manual export |
LOG_LEVEL (default INFO) is the only knob for app-logger verbosity — uvicorn's --log-level reaches uvicorn.error/uvicorn.access/uvicorn.asgi and never root. See docs/LOGGING.md.
uv run pytest # all tests + coverage
uv run pytest --no-cov -x # fast, stop on first failure
uv run pytest --no-cov -m integration # integration tests only
uv run pytest --no-cov -m "not integration" # unit tests only (faster; coverage fails below 80% on partial runs)
npm run test:js # admin JS tests (vitest + jsdom)
npm run lint:js # admin JS lint (ESLint flat config)
npm run format:js:check # admin JS format check (Prettier)
npm run format:js # admin JS format write
uv run ruff check . # lint
uv run ruff check . --fix # lint + autofix
uv run ruff format . # format
bash tests/shell/disk-hygiene-test.sh # infra shell-script sandbox tests
Coverage floor: 80% line + branch. Baseline ~93% — don't regress. Pre-commit hooks must pass before any commit (ruff; ESLint + Prettier when admin JS is touched; shellcheck + sandbox rig when infra/test shell scripts are touched).
NEVER source /etc/address-validator/.env before running tests. That file sets VALIDATION_CACHE_DSN to the production database; the audit middleware writes real rows on every TestClient request. tests/conftest.py sets VALIDATION_CACHE_DSN via os.environ.setdefault so no shell prep is needed for uv run pytest. See .env.test for standalone-script use.
uv sync # install/refresh deps
uv add <package> # add dep; commit pyproject.toml + uv.lock together
uv lock --upgrade && uv sync # upgrade all deps; then update lower bounds
See docs/DEPENDENCY-POLICY.md for version pinning rules.
PATs in .env are per repo: GH_TOKEN for this one, GH_TOKEN_<REPO> for others (e.g. GH_TOKEN_SKILLS → gregoryfoster/skills). Anchor the grep; unanchored matches several and gh rejects the result:
export GH_TOKEN=$(grep '^GH_TOKEN=' .env | cut -d= -f2)Critical gotchas (see docs/SENSITIVE-AREAS.md for full per-module risk table):
- Route handlers must be
async def— syncdefruns in threadpool; ContextVar writes are invisible to the audit middleware, silently breaking training candidate collection AddressInputMixin.model_validator— sole 422 guard for address/components input across all endpoints; do not weaken- Middleware order is load-bearing —
request_idmust wrapaudit;reset_audit_context()+reset_candidate_data()must fire at request start - Cache key changes (
_make_pattern_key,_make_canonical_key) silently orphan all existing cache entries PIPELINE_CODE_VERSIONbump discipline — parse/standardize output changes without a bump silently serve stale cached results;_store()'s ON CONFLICT must keep refreshingpipeline_versionexcept Exceptionin fail-open writes — intentional inwrite_audit_row,write_training_candidate,cache_provider.validate(); do not narrowALLOWED_TRANSITIONSintraining_batches.py— single source of truth for batch status; all transitions go through it
Full descriptions: docs/SKILLS.md. Key skills:
| Skill | When to use |
|---|---|
/brainstorming |
Before any new feature — design before code |
/writing-plans |
After brainstorming; before multi-step implementation |
/using-git-worktrees |
Every feature branch — isolated worktree; ports above, lifecycle in docs/DEPLOYMENT.md |
/test-driven-development |
Before writing implementation code |
/systematic-debugging |
Any bug or unexpected test failure |
/verification-before-completion |
Before claiming done or opening a PR |
/reviewing-code-python-fastapi |
Code review — tiered findings, implements approved fixes |
/reviewing-architecture |
Architecture review |
/enforcing-architecture |
Turn an accepted AR finding into an executable fitness function |
/curating-context |
Trim AGENTS.md + docs to the 6,000-token budget; weekly maintenance |
/shipping-work-python-fastapi |
Finalize — commit, push, close issues |
/train-model |
CRF model retraining pipeline |
/schedule |
Recurring or one-time background agents |
/using-mayfly-chat |
Chat with another repo's agent; never commit the channel URL |
socraticode:codebase-exploration / socraticode:codebase-management |
Semantic search, graphs / index, health, watching — docs/SOCRATICODE.md |
With issue: #<n> [type]: <description>
Without: [type]: <description>
Multiple issues: #12, #14 [type]: <description>
Types: feat, fix, refactor, docs, test, chore
- docs/ARCHITECTURE.md — request flow, module ownership
- docs/DEPLOYMENT.md — units, timers, DB scripts, env, worktree + dev-server
- docs/HOST-MEMORY.md — memory reservations, OOM ordering, SocratiCode pins
- docs/TAILNET.md — tailnet node, ACL, MagicDNS takeover, join/rejoin (notifier hop)
- docs/SENSITIVE-AREAS.md — per-module risk table: what breaks silently
- docs/VALIDATION-PROVIDERS.md — provider env vars, DPV→status map, quotas
- docs/VALIDATION-STATUS.md —
ValidationResult.statusvocabulary - docs/WARNINGS.md —
warnings[]catalogue - docs/LOGGING.md — event/level table, PII policy
- docs/STYLE.md — admin dashboard: brand, dark mode, WCAG 2.1 AA
- docs/SKILLS.md — every vendored skill and its trigger
- docs/SOCRATICODE.md —
codebase_*tool table, prefetch hook, graph health - docs/DEPENDENCY-POLICY.md — version pinning rules
- docs/usps-pub28.md — Pub 28 edition behind
usps_data/, API model notes - Vendored USPS OpenAPI specs: standard, Enhanced
docs/plans/, docs/research/ — dated snapshots, never current guidance.