# Pinscope Backend FastAPI application providing async pipeline orchestration, project storage, and SSE progress streaming. Wraps the `pinscopex/` core library — calls existing functions with local paths, adds no domain logic of its own. ## Running ```bash # From project root python3 -m uvicorn backend.main:app --reload # localhost:8000 ``` Config reads from `backend/.env` (see `config.py`). Key settings: `DEEPSEEK_API_KEY`, `DEEPSEEK_MODEL`, per-stage DeepSeek overrides (`model_pintable_deepseek`, `model_validation_deepseek`, …), `PROVIDER_DEFAULT` (default `deepseek`), optional `ANTHROPIC_API_KEY` / `GEMINI_API_KEY`, `CORS_ORIGINS`, DigiKey keys. For local mode, leave `GCS_BUCKET` empty — uses `LocalStorageBackend` (`data/` directory) and no auth (user_id defaults to `"local"`, admin access granted). ## Architecture ``` backend/ ├── main.py # App entry, lifespan hook, CORS, auth middleware, router includes ├── config.py # Pydantic Settings from .env ├── _version.py # Reads app version from frontend/content/changelog.md (single source of truth) ├── Dockerfile # Python 3.12-slim, copies taxonomy/ + changelog.md for runtime ├── skills_manifest.json # Claude Console Skill IDs (extract-pintable, extract-pattern, extract-specs) ├── pinscopex/ # Core library (models, parsers, graph, validator, taxonomy, derating) │ ├── utils.py # Shared utilities: safe_mpn(), natural_sort_key() │ └── resolve_passives.py # Passive MPN pattern matching + value decoders (R/C/L) ├── middleware/ │ └── auth.py # JWT verification via JWKS (enabled when CLERK_JWKS_URL is set; off in OSS mode) ├── routers/ │ ├── deps.py # Shared router dependencies: get_storage(), get_user_id(), resolve_or_404() │ ├── projects.py # CRUD + file upload + library check + collaborators + DigiKey endpoints │ ├── pipeline.py # Start, cancel, estimate, resume, restart, regen, SSE, status │ ├── reports.py # Report, comments, graph, datasheet, API logs, BOM, derating │ ├── admin.py # Admin-only: components, users, usage, projects, runs, settings │ ├── feedback.py # User feedback tickets │ └── contact.py # Contact form (email relay; inert unless email is configured) └── services/ ├── storage.py # StorageBackend protocol + LocalStorageBackend ├── storage_gcs.py # GCSStorageBackend (optional, Google Cloud Storage) ├── projects.py # Project CRUD + library ops via StorageBackend ├── pipeline.py # Multi-stage orchestrator + EventBroker + PipelineWorkspace ├── extraction.py # Async Claude API calls (pintable, patterns, specs, auto-resolve, value fallback) + skills + page trimming ├── validation.py # Async agentic validation wrapper with per-IC error isolation ├── normalize_findings.py # Post-review per-IC normalize pass (downgrade-only) ├── dedupe_findings.py # Cross-IC finding dedup ├── billing_hook.py # Open-core billing seam (NullBilling here — pipelines run free) ├── digikey.py # DigiKey API v4 — OAuth2, datasheet fetch, parameter fetch (exact MPN only) ├── purple_parts.py # Optional external LCSC→MPN resolver (env-gated; fails soft when unset) ├── api_logs.py # API call logging, cost calculation per pipeline run ├── cost_estimator.py # Pre-flight pipeline estimate (read-only) ├── datasheet_store.py # Content-addressed PDF storage: blobs/{md5}.pdf + refs/{safe_mpn}.json ├── admin_settings.py # Admin-only settings (e.g., min_model_version threshold) ├── job_runner.py # Optional Cloud Run job trigger (in-process asyncio locally) └── email.py # Email notifications (optional, env-gated) ``` ## Storage Abstraction All file I/O goes through `StorageBackend` (protocol in `services/storage.py`): - **LocalStorageBackend**: Maps keys to `data/` directory. Default. - **GCSStorageBackend**: Uses `google-cloud-storage` SDK. Used when `GCS_BUCKET` is set. Storage keys follow GCS-style paths: `users/{user_id}/projects/{id}/uploads/bom.csv` The `pinscopex/` core library is **unaware of storage** — it operates on local paths. During pipeline execution, `PipelineWorkspace` downloads files to a temp dir, runs `pinscopex/` functions locally, then uploads results back. ## Project Storage ``` # data/ directory (or GCS bucket) users/{user_id}/projects/{id}/ ├── project.json # ProjectMeta (name, status, timestamps, user_id, total_cost_usd) ├── uploads/ │ ├── bom.csv │ ├── netlist.asc # OR netlist.edn for EDIF uploads │ └── datasheets/*.pdf ├── extracted/ # Per-project IC extractions ├── patterns/ # Per-project passive patterns ├── models/ # Per-project resolved specs ├── design_graph.json ├── bom_summary.json # BOM summary table (collated from design graph) ├── derating.json # Capacitor voltage derating table ├── report.json # Findings + comments └── api_logs.jsonl # Claude API call log (token counts, cost, timing) library/ # Shared across projects ├── extracted/ # Shared IC extractions ├── patterns/ # Shared passive patterns ├── models/ # Shared component specs (discrete, connectors, etc.) ├── passives/ # DigiKey-resolved passive specs (exact-MPN only) └── datasheets/ ├── blobs/{md5}.pdf # Content-addressed PDF blobs (deduped) └── refs/{safe_mpn}.json # MPN → blob pointer ({hash, blob_key}) taxonomy/ # Component taxonomy (repo taxonomy/ dir in local mode) ``` Library lookups happen first — if an MPN was already extracted, it's reused without re-calling the API. ## Pipeline Stages The pipeline runs async via `asyncio.create_task()`. Progress emitted as SSE events via `EventBroker` (async queue per subscriber). `PipelineWorkspace` handles download/upload. Pipelines can be cancelled mid-run via `POST /api/pipeline/{id}/cancel`. 1. **Parse BOM** — Read uploaded CSV/XLSX (uses stored column mappings from upload; XLSX converted to CSV via openpyxl) 2. **Extract IC Pintables** — Async Claude API calls for pintable per IC MPN (datasheets keyword-trimmed via `pypdf`). Cache-miss MPNs are extracted **concurrently**, up to `IC_CONCURRENCY` (default 6) in flight at once. 2.5. **Extract Simple Components** — Specs extraction for discrete/simple components with datasheets 3. **Extract Passives** — Pattern-based extraction per MPN group, then a specs fallback per MPN. 3.5. **DigiKey Auto-Resolve (exact MPN)** — Fallback for unresolved passives; parameters mapped to taxonomy specs via Haiku. Requires exact MPN match so the shared `library/passives/` stays clean. 3.6. **Value Fallback (R/C/L/FB only)** — When DigiKey misses, parse the BOM `Value` string via Haiku into typed passive specs. Per-project only; never written to the shared library. 4. **Build Graph** — Call `pinscopex.graph.build_graph()` with local temp paths 5. **BOM Summary** — Collate components from design graph (no AI) 6. **Derating Table** — Capacitor voltage derating computation (no AI) 7. **Direct Datasheet Review** — Per-IC (isolated): Claude reads the datasheet PDF + circuit neighborhood from the graph, compares to reference application circuit, and submits findings via graph query tools. ICs are reviewed **concurrently**, up to `IC_CONCURRENCY` in flight at once. **Concurrency knob** — `IC_CONCURRENCY` (`config.py: ic_concurrency`, default 6) governs parallelism for stage 2 (IC extraction), stage 3.5 (passive specs fallback), and stage 7 (review). Set `IC_CONCURRENCY=1` for fully sequential behavior. ## Key Patterns - **StorageBackend protocol** — all file I/O is abstracted; swap local/GCS via `GCS_BUCKET` env var - **PipelineWorkspace** — downloads to temp dir, runs pinscopex locally, uploads results - **BillingHook seam (open-core)** — core code reaches billing exclusively through `services/billing_hook.py:get_billing()`. In this repo that's `NullBilling`: every pipeline runs free and no billing routes are mounted. Never import billing modules directly from core code — go through the hook. - **Auth middleware** — JWT verification via a JWKS endpoint; disabled when `CLERK_JWKS_URL` is empty (local mode: `user_id="local"`, `is_admin()` returns True) - **AsyncAnthropic** for all Claude API calls — extraction and validation - **Claude Console Skills** — extraction uses managed skills (skill_id + version from `skills_manifest.json`); no fallback, raises error if skill not configured - **Prompt caching** — extraction and validation calls use `cache_control={"type": "ephemeral"}` on system prompts and input context - **Forced tool calls** for extraction — structured output via `tool_choice` - **SSE via sse-starlette** — `EventBroker` manages per-project async queues with history replay - **API call logging** — `ApiLogger` in `services/api_logs.py` collects per-call metadata; `CallMeta` returned from extraction functions - **Traceability IDs** — findings get IDs (format: `{designator}-{001}`) for audit trails - **Taxonomy specs schemas** — auto-generated via Claude per type/subtype; extraction discards parameters not in schema (`extra_specs`) - **Shared router deps** — `routers/deps.py` centralizes `get_storage()`, `get_user_id()`, `resolve_or_404()` across all routers - **DigiKey OAuth2** — Token caching in `services/digikey.py`; `_find_product` requires exact MPN (no silent first-match fallback) - **Version stamping** — `backend/_version.py` reads the latest `##` heading from `frontend/content/changelog.md` and exports `PINSCOPE_VERSION`; stamped onto `ProjectMeta.pinscope_version` at `/start` - **Datasheet page trimming** — `_select_pages()` in `extraction.py` keyword-trims large PDFs to reduce token costs - **Content-addressed datasheets** — `datasheet_store.py` writes PDFs to `library/datasheets/blobs/{md5}.pdf` and maps MPNs via refs - **Passive value decoders** — `pinscopex/resolve_passives.py` decodes EIA-198, R-notation, letter-decimal, EIA3/EIA4 for R/C/L values - **Collaborator access** — `resolve_or_404()` grants access to both owner and collaborators - **Per-IC review isolation** — In `services/validation.py`, each IC review is wrapped so a single bad payload is captured as a skipped component rather than aborting the run ## Guidelines - Keep all Claude API interaction in `services/extraction.py` and `services/validation.py`; logging in `services/api_logs.py` - Keep all storage operations in `services/projects.py` (uses `StorageBackend`) - Routers are thin — validate input, call service, return response - Thread `user_id` from `request.state` through to all service calls - Don't import from `backend/` in `pinscopex/` — dependency flows one way - CORS is configured for `localhost:3000` by default; override with `CORS_ORIGINS` env var