From 8d2b85600f7af202cd4aa94ce99ff8e4bf53b1d5 Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Sun, 13 Sep 2026 20:02:04 +0200 Subject: [PATCH] Rebrand Pinscope to Periscope across product and codebase. Rename the core package to periscopex, update UI/docs/Docker/deploy defaults to periscope.michelebigi.it, and keep legacy version/storage key aliases so existing projects keep working. Co-authored-by: Cursor --- CLAUDE.md | 16 ++-- README.md | 16 ++-- backend/.env.example | 4 +- backend/CLAUDE.md | 18 ++--- backend/Dockerfile | 2 +- backend/_version.py | 6 +- backend/config.py | 4 +- backend/main.py | 8 +- backend/middleware/auth.py | 4 +- backend/{pinscopex => periscopex}/__init__.py | 0 .../antenna_geometry.py | 4 +- .../{pinscopex => periscopex}/antenna_rf.py | 6 +- .../bom_match_check.py | 6 +- .../{pinscopex => periscopex}/bom_summary.py | 4 +- .../{pinscopex => periscopex}/cad_bridge.py | 8 +- .../crystal_cl_check.py | 12 +-- backend/{pinscopex => periscopex}/derating.py | 6 +- .../{pinscopex => periscopex}/dnp_check.py | 8 +- .../{pinscopex => periscopex}/errata_check.py | 8 +- .../{pinscopex => periscopex}/eval_report.py | 32 ++++---- .../{pinscopex => periscopex}/filter_check.py | 14 ++-- .../functional_groups.py | 4 +- backend/{pinscopex => periscopex}/graph.py | 12 +-- .../hf_coverage_check.py | 8 +- .../{pinscopex => periscopex}/impedance.py | 12 +-- .../impedance_traces.py | 4 +- .../internal_features_check.py | 8 +- .../{pinscopex => periscopex}/layout_rules.py | 0 .../led_current_check.py | 4 +- .../{pinscopex => periscopex}/library_gate.py | 0 .../{pinscopex => periscopex}/lifecycle.py | 10 +-- backend/{pinscopex => periscopex}/models.py | 6 +- .../{pinscopex => periscopex}/nc_pin_check.py | 4 +- .../netlist_bundle.py | 4 +- backend/{pinscopex => periscopex}/parsers.py | 4 +- .../{pinscopex => periscopex}/parsers_edif.py | 0 .../parsers_kicad.py | 0 .../parsers_kicad_pcb.py | 4 +- .../passive_rail_check.py | 20 ++--- backend/{pinscopex => periscopex}/pdf_text.py | 0 .../pin_function_tokens.py | 0 .../pin_mux_check.py | 8 +- .../placement_check.py | 18 ++--- .../placement_pack.py | 4 +- .../power_margin_check.py | 14 ++-- .../{pinscopex => periscopex}/quote_verify.py | 6 +- .../resolve_passives.py | 4 +- .../review_fingerprint.py | 4 +- .../review_workflow.py | 2 +- .../sequencing_check.py | 10 +-- backend/{pinscopex => periscopex}/si_check.py | 6 +- backend/{pinscopex => periscopex}/taxonomy.py | 0 .../thermal_check.py | 18 ++--- backend/{pinscopex => periscopex}/utils.py | 2 +- backend/{pinscopex => periscopex}/validate.py | 12 +-- .../validation_tools.py | 8 +- backend/routers/admin.py | 2 +- backend/routers/auth.py | 2 +- backend/routers/contact.py | 8 +- backend/routers/impedance.py | 10 +-- backend/routers/pipeline.py | 8 +- backend/routers/projects.py | 22 +++--- backend/routers/reports.py | 8 +- backend/services/cost_estimator.py | 8 +- backend/services/datasheet_finder.py | 6 +- backend/services/datasheet_store.py | 2 +- backend/services/dedupe_findings.py | 2 +- backend/services/email.py | 68 ++++++++--------- backend/services/extraction.py | 14 ++-- backend/services/llm/local_skill.py | 2 +- backend/services/llm/pdf_ingest.py | 2 +- backend/services/local_jwt.py | 6 +- backend/services/local_users.py | 2 +- backend/services/normalize_findings.py | 2 +- backend/services/passive_from_distributor.py | 4 +- backend/services/passive_from_mpn.py | 4 +- backend/services/passive_from_value.py | 4 +- backend/services/pipeline.py | 40 +++++----- backend/services/placement_pipeline.py | 10 +-- backend/services/projects.py | 26 ++++--- backend/services/validation.py | 52 ++++++------- docker-compose.yml | 10 +-- docs/piano-implementazione.md | 62 +++++++-------- frontend/CLAUDE.md | 8 +- frontend/content/changelog.md | 76 ++++++++++--------- frontend/content/file-guide.md | 36 ++++----- frontend/content/privacy.md | 4 +- frontend/content/terms.md | 12 +-- frontend/public/favicon_io/site.webmanifest | 4 +- frontend/src/app/(app)/admin/page.tsx | 2 +- frontend/src/app/(app)/dashboard/page.tsx | 6 +- frontend/src/app/(app)/feedback/page.tsx | 2 +- frontend/src/app/(app)/project/[id]/page.tsx | 35 ++++++--- .../src/app/(marketing)/changelog/page.tsx | 2 +- frontend/src/app/(marketing)/contact/nav.tsx | 2 +- frontend/src/app/(marketing)/contact/page.tsx | 6 +- .../src/app/(marketing)/file-guide/page.tsx | 2 +- frontend/src/app/(marketing)/page.tsx | 8 +- frontend/src/app/(marketing)/privacy/page.tsx | 2 +- frontend/src/app/(marketing)/sign-in/page.tsx | 2 +- frontend/src/app/(marketing)/terms/page.tsx | 2 +- frontend/src/app/opengraph-image.tsx | 6 +- .../dashboard/create-project-dialog.tsx | 2 +- .../dashboard/onboarding-survey.tsx | 2 +- frontend/src/components/layout/sidebar.tsx | 2 +- .../components/legal/changelog-timeline.tsx | 4 +- frontend/src/components/legal/legal-page.tsx | 2 +- .../src/components/pdf/pdf-viewer-panel.tsx | 2 +- .../components/project/impedance-panel.tsx | 2 +- frontend/src/hooks/use-optional-auth.ts | 17 ++--- frontend/src/hooks/use-report.ts | 2 +- frontend/src/hooks/use-reviewed-count.ts | 10 ++- frontend/src/hooks/use-reviewed-findings.ts | 12 ++- frontend/src/lib/api.ts | 11 ++- frontend/src/lib/auth.ts | 23 +++++- frontend/src/lib/site.ts | 6 +- frontend/src/lib/storage-keys.ts | 42 ++++++++++ frontend/src/lib/types.ts | 2 +- plugins/kicad/__init__.py | 2 +- plugins/kicad/focus.py | 8 +- ...pinscope_plugin.py => periscope_plugin.py} | 28 +++---- scripts/dedup_library_datasheets.py | 2 +- scripts/smoke_simple_project.py | 4 +- ...update-pinscope.sh => update-periscope.sh} | 14 ++-- simple_project/eval_golden.json | 6 +- tests/test_antenna_rf.py | 8 +- tests/test_bom_match_check.py | 14 ++-- tests/test_bom_parser.py | 2 +- tests/test_cad_bridge.py | 16 ++-- tests/test_cost_estimator.py | 2 +- tests/test_crystal_cl_check.py | 10 +-- tests/test_datasheet_excerpt_tool.py | 6 +- tests/test_dedupe_findings.py | 6 +- tests/test_deepseek_only.py | 2 +- tests/test_derating.py | 4 +- tests/test_dnp_check.py | 8 +- tests/test_edif_parser.py | 4 +- tests/test_errata_check.py | 8 +- tests/test_eval_report.py | 20 ++--- tests/test_filter_check.py | 16 ++-- tests/test_finding_schema.py | 6 +- tests/test_functional_groups.py | 10 +-- tests/test_hf_coverage_check.py | 6 +- tests/test_impedance.py | 10 +-- tests/test_impedance_traces.py | 8 +- tests/test_internal_features_check.py | 8 +- tests/test_kicad_parser.py | 26 +++---- tests/test_kicad_pcb.py | 6 +- tests/test_layout_rules.py | 4 +- tests/test_led_current_check.py | 4 +- tests/test_lifecycle_check.py | 16 ++-- tests/test_local_auth.py | 2 +- tests/test_nc_pin_check.py | 6 +- tests/test_netlist_bundle.py | 8 +- tests/test_netlist_parser.py | 2 +- tests/test_normalize_findings.py | 6 +- tests/test_passive_rail_check.py | 26 +++---- tests/test_pdf_ingest.py | 2 +- tests/test_pin_mux_check.py | 12 +-- tests/test_placement_check.py | 22 +++--- tests/test_placement_pack.py | 6 +- tests/test_placement_pipeline.py | 4 +- tests/test_power_margin_check.py | 8 +- tests/test_purple_parts_resolver.py | 4 +- tests/test_quote_verify.py | 6 +- tests/test_review_fingerprint.py | 4 +- tests/test_review_parse.py | 2 +- tests/test_review_workflow.py | 8 +- tests/test_roadmap_integrations.py | 6 +- tests/test_sequencing_check.py | 6 +- tests/test_si_check.py | 12 +-- tests/test_thermal_check.py | 10 +-- tests/test_validation_concurrency.py | 6 +- tests/test_validation_deterministic_seed.py | 2 +- tests/test_validation_no_tool_recovery.py | 7 +- tests/test_validation_trace_log.py | 10 ++- vendor/impedancefinder/SOURCE.md | 4 +- 177 files changed, 869 insertions(+), 766 deletions(-) rename backend/{pinscopex => periscopex}/__init__.py (100%) rename backend/{pinscopex => periscopex}/antenna_geometry.py (98%) rename backend/{pinscopex => periscopex}/antenna_rf.py (99%) rename backend/{pinscopex => periscopex}/bom_match_check.py (95%) rename backend/{pinscopex => periscopex}/bom_summary.py (96%) rename backend/{pinscopex => periscopex}/cad_bridge.py (88%) rename backend/{pinscopex => periscopex}/crystal_cl_check.py (95%) rename backend/{pinscopex => periscopex}/derating.py (97%) rename backend/{pinscopex => periscopex}/dnp_check.py (95%) rename backend/{pinscopex => periscopex}/errata_check.py (92%) rename backend/{pinscopex => periscopex}/eval_report.py (82%) rename backend/{pinscopex => periscopex}/filter_check.py (97%) rename backend/{pinscopex => periscopex}/functional_groups.py (99%) rename backend/{pinscopex => periscopex}/graph.py (97%) rename backend/{pinscopex => periscopex}/hf_coverage_check.py (94%) rename backend/{pinscopex => periscopex}/impedance.py (92%) rename backend/{pinscopex => periscopex}/impedance_traces.py (97%) rename backend/{pinscopex => periscopex}/internal_features_check.py (89%) rename backend/{pinscopex => periscopex}/layout_rules.py (100%) rename backend/{pinscopex => periscopex}/led_current_check.py (98%) rename backend/{pinscopex => periscopex}/library_gate.py (100%) rename backend/{pinscopex => periscopex}/lifecycle.py (96%) rename backend/{pinscopex => periscopex}/models.py (98%) rename backend/{pinscopex => periscopex}/nc_pin_check.py (98%) rename backend/{pinscopex => periscopex}/netlist_bundle.py (98%) rename backend/{pinscopex => periscopex}/parsers.py (98%) rename backend/{pinscopex => periscopex}/parsers_edif.py (100%) rename backend/{pinscopex => periscopex}/parsers_kicad.py (100%) rename backend/{pinscopex => periscopex}/parsers_kicad_pcb.py (99%) rename backend/{pinscopex => periscopex}/passive_rail_check.py (97%) rename backend/{pinscopex => periscopex}/pdf_text.py (100%) rename backend/{pinscopex => periscopex}/pin_function_tokens.py (100%) rename backend/{pinscopex => periscopex}/pin_mux_check.py (97%) rename backend/{pinscopex => periscopex}/placement_check.py (95%) rename backend/{pinscopex => periscopex}/placement_pack.py (97%) rename backend/{pinscopex => periscopex}/power_margin_check.py (94%) rename backend/{pinscopex => periscopex}/quote_verify.py (96%) rename backend/{pinscopex => periscopex}/resolve_passives.py (99%) rename backend/{pinscopex => periscopex}/review_fingerprint.py (95%) rename backend/{pinscopex => periscopex}/review_workflow.py (98%) rename backend/{pinscopex => periscopex}/sequencing_check.py (94%) rename backend/{pinscopex => periscopex}/si_check.py (93%) rename backend/{pinscopex => periscopex}/taxonomy.py (100%) rename backend/{pinscopex => periscopex}/thermal_check.py (95%) rename backend/{pinscopex => periscopex}/utils.py (89%) rename backend/{pinscopex => periscopex}/validate.py (99%) rename backend/{pinscopex => periscopex}/validation_tools.py (99%) create mode 100644 frontend/src/lib/storage-keys.ts rename plugins/kicad/{pinscope_plugin.py => periscope_plugin.py} (82%) rename scripts/{update-pinscope.sh => update-periscope.sh} (92%) diff --git a/CLAUDE.md b/CLAUDE.md index 33f8322..7015a21 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ -# Pinscope — Agentic Schematic Validation +# Periscope — Agentic Schematic Validation -Pinscope validates hardware schematics against component datasheets. It extracts constraints from PDFs, parses netlists and BOMs into a queryable graph, and runs an agentic validation loop to flag design violations. +Periscope validates hardware schematics against component datasheets. It extracts constraints from PDFs, parses netlists and BOMs into a queryable graph, and runs an agentic validation loop to flag design violations. > **Open-core note.** This is the open-source core. A small set of files are > "gateway-owned seams" — pass-through stubs here (`frontend/src/proxy.ts`, @@ -18,7 +18,7 @@ Three layers: | Layer | Location | Purpose | |-------|----------|---------| -| **Core library** | `backend/pinscopex/` | Models, parsers, graph builder, agentic validator, passive resolver, taxonomy, BOM summary, derating | +| **Core library** | `backend/periscopex/` | Models, parsers, graph builder, agentic validator, passive resolver, taxonomy, BOM summary, derating | | **Backend** | `backend/` | FastAPI app — async pipeline orchestration, SSE progress, project/file storage | | **Frontend** | `frontend/` | Next.js 16 app — project dashboard, pipeline progress, report viewer, derating, admin dashboard | @@ -55,17 +55,17 @@ Files: `.asc` (PADS-PCB netlist; `.edn` EDIF 2.0.0 also accepted), `.csv`/`.xlsx - **Cross-IC finding dedup** — After all per-IC reviews complete, a single pass (`services/dedupe_findings.py`) collapses one physical interface defect reported from both endpoints into a single finding. Gated by `cross_ic_dedup_enabled`; fail-soft. - **Capacitor voltage derating** — Deterministic derating table computed from graph (ceramic/tantalum/electrolytic percentages, pass/fail per capacitor) - **Deterministic checks over heuristics** — Exact checks where possible -- **Zero coupling between layers** — Backend calls pinscopex functions with paths; frontend talks to backend via REST + SSE +- **Zero coupling between layers** — Backend calls periscopex functions with paths; frontend talks to backend via REST + SSE - **Library deduplication** — Shared library (`library/extracted/`, `library/patterns/`, `library/models/`, `library/passives/`, `library/datasheets/`) caches extractions across projects - **Content-addressed datasheets** — `library/datasheets/blobs/{md5}.pdf` stores unique PDFs once; `library/datasheets/refs/{safe_mpn}.json` maps MPNs to blobs (dedupe + multi-MPN sharing) - **Taxonomy-driven extraction** — Living component taxonomy (`taxonomy/`) with per-subtype classification and specs schemas - **Per-stage model config** — Each pipeline stage can use a different Claude model (e.g., Sonnet for review, Haiku for auto-resolve) - **API call logging** — Every Claude API call is logged with token counts, cost, and timing per pipeline run -- **Report versioning** — Each project run is stamped with the current app version on the first `/start` transition (`ProjectMeta.pinscope_version`). The version comes from `frontend/content/changelog.md`'s latest `##` heading — single source of truth — read at backend startup via `backend/_version.py`. +- **Report versioning** — Each project run is stamped with the current app version on the first `/start` transition (`ProjectMeta.periscope_version`). The version comes from `frontend/content/changelog.md`'s latest `##` heading — single source of truth — read at backend startup via `backend/_version.py`. ## Datasheet Extraction -Extracted data lives in `library/extracted/` (shared) or per-project under the storage backend. One JSON per MPN, schema in `backend/pinscopex/models.py`. +Extracted data lives in `library/extracted/` (shared) or per-project under the storage backend. One JSON per MPN, schema in `backend/periscopex/models.py`. Per-MPN IC extraction captures: 1. **Pintable** — Pin number + name (required), description + alt functions (optional) @@ -131,8 +131,8 @@ All `ComponentConstraints` extracted JSON files carry a `model_version` semver f - Write tests against `simple_project/` — it's the ground truth - Netlist parser and BOM parser are pure functions with no side effects -- All data structures use Pydantic models in `backend/pinscopex/models.py` -- Frontend types in `frontend/src/lib/types.ts` must stay in sync with `backend/pinscopex/models.py` +- All data structures use Pydantic models in `backend/periscopex/models.py` +- Frontend types in `frontend/src/lib/types.ts` must stay in sync with `backend/periscopex/models.py` - Extraction prompts live in `skills/` (SKILL.md + schema.json + validate.py) and run locally against DeepSeek - **Never swallow exceptions silently** — prefer logging or re-raising over bare `except: continue`. Silent failures hide real bugs. diff --git a/README.md b/README.md index 921babf..06536db 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Pinscope (DeepSeek) +# Periscope (DeepSeek) -Pinscope reviews schematics the way a good senior engineer does: with the datasheets open. +Periscope reviews schematics the way a good senior engineer does: with the datasheets open. This tree is adapted from [manvalan/pinscope](https://github.com/manvalan/pinscope) so the pipeline talks to the **DeepSeek API** (`deepseek-flash`, with legacy aliases still accepted). Do not use Anthropic. @@ -8,7 +8,7 @@ Give it a netlist, a BOM, and your datasheet PDFs. It builds a graph of your des ## What changed for DeepSeek -DeepSeek's Chat Completions API is OpenAI-compatible but **does not accept native PDF documents**. Pinscope therefore: +DeepSeek's Chat Completions API is OpenAI-compatible but **does not accept native PDF documents**. Periscope therefore: 1. **Extracts datasheet text** with `pypdf` (page-marked) and sends it as chat content. 2. **Renders pages to JPEG** with PyMuPDF when the stage uses a vision model, so pin diagrams and tables survive. @@ -66,18 +66,18 @@ docker compose up --build Backend on port 8080, frontend on port 3000. -### Update a live instance (e.g. pinscope.michelebigi.it) +### Update a live instance (e.g. periscope.michelebigi.it) -On the server, from the Pinscope checkout: +On the server, from the Periscope checkout: ```bash -./scripts/update-pinscope.sh +./scripts/update-periscope.sh ``` -The script pulls the current branch, writes `NEXT_PUBLIC_API_URL` / `CORS_ORIGINS` for `https://pinscope.michelebigi.it`, rebuilds both Docker images, and leaves `data/` alone. First run: put `DEEPSEEK_API_KEY` in `.env` at the repo root (compose reads that file). `--no-pull` skips git. `SITE=https://other.host ./scripts/update-pinscope.sh` overrides the public URL. +The script pulls the current branch, writes `NEXT_PUBLIC_API_URL` / `CORS_ORIGINS` for `https://periscope.michelebigi.it`, rebuilds both Docker images, and leaves `data/` alone. First run: put `DEEPSEEK_API_KEY` in `.env` at the repo root (compose reads that file). `--no-pull` skips git. `SITE=https://other.host ./scripts/update-periscope.sh` overrides the public URL. Do not set `ENVIRONMENT=production` unless Clerk auth is configured — that flag refuses to boot with auth disabled. ## License -AGPL-3.0, same as upstream Pinscope. For commercial licensing of the original, write to dev@faradworks.com. +AGPL-3.0, same as upstream Pinscope (Faradworks). For commercial licensing of the original, write to dev@faradworks.com. diff --git a/backend/.env.example b/backend/.env.example index d7f5062..ed81bff 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,4 +1,4 @@ -# Pinscope Backend — Environment Variables +# Periscope Backend — Environment Variables # Copy to backend/.env and fill in values. Only DEEPSEEK_API_KEY is required. # -- AI (DeepSeek, default) -------------------------------------------------- @@ -53,7 +53,7 @@ GCS_BUCKET= CORS_ORIGINS=["http://localhost:3000","http://127.0.0.1:3000","http://localhost:18742","http://127.0.0.1:18742"] # -- Auth (self-host) -------------------------------------------------------- -# Set AUTH_JWT_SECRET to enable Pinscope email/password accounts and +# Set AUTH_JWT_SECRET to enable Periscope email/password accounts and # multi-user project collaborators (invite by email). Clerk keys, if set, # take priority over local auth. # AUTH_JWT_SECRET=change-me-to-a-long-random-string diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md index 7bc2264..a9ce7a4 100644 --- a/backend/CLAUDE.md +++ b/backend/CLAUDE.md @@ -1,6 +1,6 @@ -# Pinscope Backend +# Periscope 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. +FastAPI application providing async pipeline orchestration, project storage, and SSE progress streaming. Wraps the `periscopex/` core library — calls existing functions with local paths, adds no domain logic of its own. ## Running @@ -22,7 +22,7 @@ backend/ ├── _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) +├── periscopex/ # 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/ @@ -63,7 +63,7 @@ All file I/O goes through `StorageBackend` (protocol in `services/storage.py`): 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. +The `periscopex/` core library is **unaware of storage** — it operates on local paths. During pipeline execution, `PipelineWorkspace` downloads files to a temp dir, runs `periscopex/` functions locally, then uploads results back. ## Project Storage @@ -108,7 +108,7 @@ The pipeline runs async via `asyncio.create_task()`. Progress emitted as SSE eve 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 +4. **Build Graph** — Call `periscopex.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. @@ -118,7 +118,7 @@ The pipeline runs async via `asyncio.create_task()`. Progress emitted as SSE eve ## 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 +- **PipelineWorkspace** — downloads to temp dir, runs periscopex 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 @@ -131,10 +131,10 @@ The pipeline runs async via `asyncio.create_task()`. Progress emitted as SSE eve - **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` +- **Version stamping** — `backend/_version.py` reads the latest `##` heading from `frontend/content/changelog.md` and exports `PERISCOPE_VERSION`; stamped onto `ProjectMeta.periscope_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 +- **Passive value decoders** — `periscopex/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 @@ -144,5 +144,5 @@ The pipeline runs async via `asyncio.create_task()`. Progress emitted as SSE eve - 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 +- Don't import from `backend/` in `periscopex/` — dependency flows one way - CORS is configured for `localhost:3000` by default; override with `CORS_ORIGINS` env var diff --git a/backend/Dockerfile b/backend/Dockerfile index 32c0e14..3b379b1 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -22,7 +22,7 @@ COPY taxonomy/ /app/taxonomy/ # Extraction skills (SKILL.md + validate.py) — required for DeepSeek/Gemini COPY skills/ /app/skills/ -# Changelog: single source of truth for the user-facing Pinscope version. +# Changelog: single source of truth for the user-facing Periscope version. COPY frontend/content/changelog.md /app/changelog.md # ImpedenceFinder closed-form engine (no OpenEMS / pcbnew). diff --git a/backend/_version.py b/backend/_version.py index f59b08e..9a24590 100644 --- a/backend/_version.py +++ b/backend/_version.py @@ -1,4 +1,4 @@ -"""Pinscope app version, sourced from frontend/content/changelog.md. +"""Periscope app version, sourced from frontend/content/changelog.md. The changelog is the single source of truth for the user-facing version. The Dockerfile copies it into the image at /app/changelog.md; locally we @@ -23,7 +23,7 @@ _VERSION_RE = re.compile(r"^##\s+(\d+\.\d+\.\d+)\b", re.MULTILINE) @lru_cache(maxsize=1) -def get_pinscope_version() -> str: +def get_periscope_version() -> str: for path in _candidate_paths(): try: text = path.read_text(encoding="utf-8") @@ -35,4 +35,4 @@ def get_pinscope_version() -> str: return "unknown" -PINSCOPE_VERSION = get_pinscope_version() +PERISCOPE_VERSION = get_periscope_version() diff --git a/backend/config.py b/backend/config.py index c35c125..cfd918d 100644 --- a/backend/config.py +++ b/backend/config.py @@ -124,7 +124,7 @@ class Settings(BaseSettings): clerk_publishable_key: str = "" clerk_jwks_url: str = "" - # Local Pinscope auth (self-host). Set AUTH_JWT_SECRET to enable email/password + # Local Periscope auth (self-host). Set AUTH_JWT_SECRET to enable email/password # accounts and multi-user project collaborators without Clerk. auth_jwt_secret: str = "" # Comma-separated emails that become admin on register (in addition to the @@ -182,7 +182,7 @@ class Settings(BaseSettings): ] # Cloud Run Job worker (pipeline runner) - pipeline_worker_job_name: str = "pinscopex-pipeline-worker" + pipeline_worker_job_name: str = "periscopex-pipeline-worker" pipeline_worker_region: str = "us-central1" pipeline_worker_project: str = "" # GCP project id; defaults to GOOGLE_CLOUD_PROJECT or metadata pipeline_worker_timeout_seconds: int = 3600 diff --git a/backend/main.py b/backend/main.py index 2c604cd..f273cbd 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,4 +1,4 @@ -"""PinscopeX backend — FastAPI application.""" +"""PeriscopeX backend — FastAPI application.""" import logging import os @@ -36,7 +36,7 @@ async def lifespan(app: FastAPI): if env == "production" and not settings.use_auth: raise RuntimeError( "Production requires authentication: set AUTH_JWT_SECRET " - "(local Pinscope accounts) or CLERK_JWKS_URL + CLERK_SECRET_KEY." + "(local Periscope accounts) or CLERK_JWKS_URL + CLERK_SECRET_KEY." ) if not settings.use_auth: logger.warning( @@ -44,7 +44,7 @@ async def lifespan(app: FastAPI): "This is only safe for local development." ) elif settings.use_local_auth: - logger.info("Local Pinscope authentication enabled (AUTH_JWT_SECRET)") + logger.info("Local Periscope authentication enabled (AUTH_JWT_SECRET)") elif settings.use_clerk: logger.info("Clerk authentication enabled") if not settings.billing_enabled: @@ -130,7 +130,7 @@ class AuthMiddleware(BaseHTTPMiddleware): app = FastAPI( - title="PinscopeX", + title="PeriscopeX", description="Agentic schematic validation API", lifespan=lifespan, ) diff --git a/backend/middleware/auth.py b/backend/middleware/auth.py index cdd1530..c8c3587 100644 --- a/backend/middleware/auth.py +++ b/backend/middleware/auth.py @@ -1,4 +1,4 @@ -"""JWT verification for FastAPI (Clerk JWKS or local Pinscope HS256).""" +"""JWT verification for FastAPI (Clerk JWKS or local Periscope HS256).""" from __future__ import annotations @@ -89,7 +89,7 @@ async def verify_clerk_token(request: Request) -> str | None: async def verify_local_token(request: Request) -> str | None: - """Verify Pinscope local JWT and return user_id, or None if invalid.""" + """Verify Periscope local JWT and return user_id, or None if invalid.""" if request.url.path in _SKIP_PATHS: return "anonymous" diff --git a/backend/pinscopex/__init__.py b/backend/periscopex/__init__.py similarity index 100% rename from backend/pinscopex/__init__.py rename to backend/periscopex/__init__.py diff --git a/backend/pinscopex/antenna_geometry.py b/backend/periscopex/antenna_geometry.py similarity index 98% rename from backend/pinscopex/antenna_geometry.py rename to backend/periscopex/antenna_geometry.py index d444c57..95d5e49 100644 --- a/backend/pinscopex/antenna_geometry.py +++ b/backend/periscopex/antenna_geometry.py @@ -290,9 +290,9 @@ def _segments_to_kicad_mod( lines = [ f'(footprint "{name}"', " (version 20240108)", - ' (generator "pinscope")', + ' (generator "periscope")', ' (layer "F.Cu")', - f' (descr "Pinscope {template.upper()} PCB antenna template ' + f' (descr "Periscope {template.upper()} PCB antenna template ' f'— not EM-validated")', " (attr smd)", f' (pad "1" smd circle (at 0 0) (size {w_mm * 2:.4f} {w_mm * 2:.4f}) ' diff --git a/backend/pinscopex/antenna_rf.py b/backend/periscopex/antenna_rf.py similarity index 99% rename from backend/pinscopex/antenna_rf.py rename to backend/periscopex/antenna_rf.py index 8868ca1..5e14537 100644 --- a/backend/pinscopex/antenna_rf.py +++ b/backend/periscopex/antenna_rf.py @@ -16,13 +16,13 @@ from typing import Any, Literal from pydantic import BaseModel -from backend.pinscopex.antenna_geometry import ( +from backend.periscopex.antenna_geometry import ( AntennaGeometry, AntennaTemplate, build_geometry, ) -from backend.pinscopex.impedance import GeometryError, solve_width -from backend.pinscopex.models import ( +from backend.periscopex.impedance import GeometryError, solve_width +from backend.periscopex.models import ( ComponentType, DesignGraph, LayoutGraph, diff --git a/backend/pinscopex/bom_match_check.py b/backend/periscopex/bom_match_check.py similarity index 95% rename from backend/pinscopex/bom_match_check.py rename to backend/periscopex/bom_match_check.py index febd943..7925a9a 100644 --- a/backend/pinscopex/bom_match_check.py +++ b/backend/periscopex/bom_match_check.py @@ -7,7 +7,7 @@ we never invent orphans from a format that has no schematic properties. from __future__ import annotations -from backend.pinscopex.models import Finding +from backend.periscopex.models import Finding def _norm_mpn(value: object) -> str: @@ -43,7 +43,7 @@ def check_bom_schematic_match( "validated against a datasheet and may indicate a stale BOM." ), recommendation=f"Remove {ref} from the BOM or add it to the schematic.", - rule_id="PS-BOM-002", + rule_id="PE-BOM-002", pins=[], )) continue @@ -67,7 +67,7 @@ def check_bom_schematic_match( recommendation=( f"Make {ref}'s BOM and schematic MPN identical, then re-run." ), - rule_id="PS-BOM-001", + rule_id="PE-BOM-001", pins=[ref], )) return findings diff --git a/backend/pinscopex/bom_summary.py b/backend/periscopex/bom_summary.py similarity index 96% rename from backend/pinscopex/bom_summary.py rename to backend/periscopex/bom_summary.py index 44efb4d..38c3e6d 100644 --- a/backend/pinscopex/bom_summary.py +++ b/backend/periscopex/bom_summary.py @@ -2,8 +2,8 @@ from __future__ import annotations -from backend.pinscopex.models import ComponentType, DesignGraph -from backend.pinscopex.utils import natural_sort_key +from backend.periscopex.models import ComponentType, DesignGraph +from backend.periscopex.utils import natural_sort_key def build_bom_summary( diff --git a/backend/pinscopex/cad_bridge.py b/backend/periscopex/cad_bridge.py similarity index 88% rename from backend/pinscopex/cad_bridge.py rename to backend/periscopex/cad_bridge.py index 9a213d1..097a855 100644 --- a/backend/pinscopex/cad_bridge.py +++ b/backend/periscopex/cad_bridge.py @@ -1,14 +1,14 @@ -"""pinscope-cad-bridge JSON (E2) for the KiCad action plugin.""" +"""periscope-cad-bridge JSON (E2) for the KiCad action plugin.""" from __future__ import annotations import json from pathlib import Path -from backend.pinscopex.models import CadIndexEntry, DesignGraph, Finding, ValidationReport +from backend.periscopex.models import CadIndexEntry, DesignGraph, Finding, ValidationReport CAD_BRIDGE_VERSION = 1 -_PCB_RULE_PREFIXES = ("PS-PLC", "PS-SI", "PS-LAY", "PS-3W", "PS-CLR") +_PCB_RULE_PREFIXES = ("PE-PLC", "PE-SI", "PE-LAY", "PE-3W", "PE-CLR") def annotate_findings_cad( @@ -54,7 +54,7 @@ def build_cad_bridge( *, url_base: str = "", ) -> dict: - """E2 `pinscope-cad-bridge` payload. Missing uuid/sheet stay empty strings.""" + """E2 `periscope-cad-bridge` payload. Missing uuid/sheet stay empty strings.""" findings: list[dict] = [] for f in report.findings: fid = f.finding_id or "" diff --git a/backend/pinscopex/crystal_cl_check.py b/backend/periscopex/crystal_cl_check.py similarity index 95% rename from backend/pinscopex/crystal_cl_check.py rename to backend/periscopex/crystal_cl_check.py index cb6252e..cf67174 100644 --- a/backend/pinscopex/crystal_cl_check.py +++ b/backend/periscopex/crystal_cl_check.py @@ -6,12 +6,12 @@ never invent a stray default. Without CL in specs → skip. from __future__ import annotations -from backend.pinscopex.functional_groups import ( +from backend.periscopex.functional_groups import ( _cap_farads, _is_ground_net, load_capacitance_farads, ) -from backend.pinscopex.models import ( +from backend.periscopex.models import ( Component, ComponentType, DesignGraph, @@ -46,7 +46,7 @@ def check_crystal_cl(graph: DesignGraph) -> list[Finding]: why="Crystal load capacitance needs a matched C1/C2 pair.", recommendation="Add or value the two load capacitors on XIN/XOUT.", reference="netlist topology", - rule_id="PS-XTAL-001", + rule_id="PE-XTAL-001", pins=[ref], )) continue @@ -83,7 +83,7 @@ def check_crystal_cl(graph: DesignGraph) -> list[Finding]: why="Series combination of load caps exceeds specified CL without needing stray.", recommendation="Reduce load caps or confirm the datasheet CL value.", reference="netlist topology", - rule_id="PS-XTAL-002", + rule_id="PE-XTAL-002", pins=[ref, c1.reference, c2.reference], )) elif series < cl * 0.5: @@ -100,7 +100,7 @@ def check_crystal_cl(graph: DesignGraph) -> list[Finding]: why="Without stray capacitance in specs, effective CL cannot be fully checked.", recommendation="Confirm Cstray or populate load_capacitance / stray in crystal specs.", reference="netlist topology", - rule_id="PS-XTAL-003", + rule_id="PE-XTAL-003", pins=[ref, c1.reference, c2.reference], )) continue @@ -120,7 +120,7 @@ def check_crystal_cl(graph: DesignGraph) -> list[Finding]: why="Effective load capacitance should stay near the crystal's specified CL.", recommendation="Adjust C1/C2 so C_eff ≈ CL.", reference="netlist topology", - rule_id="PS-XTAL-002", + rule_id="PE-XTAL-002", pins=[ref, c1.reference, c2.reference], )) return findings diff --git a/backend/pinscopex/derating.py b/backend/periscopex/derating.py similarity index 97% rename from backend/pinscopex/derating.py rename to backend/periscopex/derating.py index 2af4caa..789d9f9 100644 --- a/backend/pinscopex/derating.py +++ b/backend/periscopex/derating.py @@ -4,9 +4,9 @@ from __future__ import annotations import re -from backend.pinscopex.models import ComponentType, DesignGraph, NetType -from backend.pinscopex.resolve_passives import _format_value -from backend.pinscopex.utils import natural_sort_key +from backend.periscopex.models import ComponentType, DesignGraph, NetType +from backend.periscopex.resolve_passives import _format_value +from backend.periscopex.utils import natural_sort_key # Dielectric strings that indicate ceramic capacitors _CERAMIC_DIELECTRICS = {"X7R", "X5R", "C0G", "NP0", "Y5V", "X7S", "X6S", "X8R", "C0G (NP0)"} diff --git a/backend/pinscopex/dnp_check.py b/backend/periscopex/dnp_check.py similarity index 95% rename from backend/pinscopex/dnp_check.py rename to backend/periscopex/dnp_check.py index 225df1e..3cdd389 100644 --- a/backend/pinscopex/dnp_check.py +++ b/backend/periscopex/dnp_check.py @@ -8,18 +8,18 @@ from __future__ import annotations import re -from backend.pinscopex.models import ( +from backend.periscopex.models import ( ComponentConstraints, ComponentType, DesignGraph, Finding, ) -from backend.pinscopex.passive_rail_check import ( +from backend.periscopex.passive_rail_check import ( _is_ground_net, _is_power_net, _pin_name_tokens, ) -from backend.pinscopex.validate import _match_constraints +from backend.periscopex.validate import _match_constraints _EN_RE = re.compile( r"(?:^|[_/])(EN|ENA|ENABLE|n?SHDN|nEN|EN_N|CHIP_EN)(?:$|[_/\d])", @@ -106,7 +106,7 @@ def check_dnp_enables( reference="BOM DNP/fitted", net=net, pins=[f"{ref}.{pin_num}"], - rule_id="PS-DNP-001", + rule_id="PE-DNP-001", variant=str(variant) if variant else None, )) return findings diff --git a/backend/pinscopex/errata_check.py b/backend/periscopex/errata_check.py similarity index 92% rename from backend/pinscopex/errata_check.py rename to backend/periscopex/errata_check.py index 186d402..7ec2470 100644 --- a/backend/pinscopex/errata_check.py +++ b/backend/periscopex/errata_check.py @@ -5,12 +5,12 @@ from __future__ import annotations import logging import re -from backend.pinscopex.models import ComponentConstraints, DesignGraph, Finding -from backend.pinscopex.passive_rail_check import ( +from backend.periscopex.models import ComponentConstraints, DesignGraph, Finding +from backend.periscopex.passive_rail_check import ( _pin_name_tokens, _resistor_to_power, ) -from backend.pinscopex.validate import _match_constraints +from backend.periscopex.validate import _match_constraints log = logging.getLogger(__name__) @@ -62,7 +62,7 @@ def check_errata( reference=url, net=net, pins=[f"{ref}.{pin_name}"], - rule_id="PS-ERRATA-001", + rule_id="PE-ERRATA-001", )) return findings diff --git a/backend/pinscopex/eval_report.py b/backend/periscopex/eval_report.py similarity index 82% rename from backend/pinscopex/eval_report.py rename to backend/periscopex/eval_report.py index 2ec58d8..50754c6 100644 --- a/backend/pinscopex/eval_report.py +++ b/backend/periscopex/eval_report.py @@ -12,26 +12,26 @@ from pathlib import Path from pydantic import BaseModel -from backend.pinscopex.models import DesignGraph, Finding, ValidationReport -from backend.pinscopex.pin_mux_check import check_pin_mux_feasibility -from backend.pinscopex.led_current_check import check_led_current -from backend.pinscopex.passive_rail_check import ( +from backend.periscopex.models import DesignGraph, Finding, ValidationReport +from backend.periscopex.pin_mux_check import check_pin_mux_feasibility +from backend.periscopex.led_current_check import check_led_current +from backend.periscopex.passive_rail_check import ( check_i2c_pullups, check_reset_pullups, check_supply_decoupling, ) -from backend.pinscopex.bom_match_check import check_bom_schematic_match -from backend.pinscopex.hf_coverage_check import check_hf_decoupling_coverage -from backend.pinscopex.filter_check import check_filters -from backend.pinscopex.thermal_check import check_thermal -from backend.pinscopex.power_margin_check import check_power_margin -from backend.pinscopex.sequencing_check import check_power_sequencing -from backend.pinscopex.dnp_check import check_dnp_enables -from backend.pinscopex.lifecycle import check_lifecycle -from backend.pinscopex.errata_check import check_errata -from backend.pinscopex.internal_features_check import check_internal_features -from backend.pinscopex.placement_check import check_placement -from backend.pinscopex.si_check import check_si +from backend.periscopex.bom_match_check import check_bom_schematic_match +from backend.periscopex.hf_coverage_check import check_hf_decoupling_coverage +from backend.periscopex.filter_check import check_filters +from backend.periscopex.thermal_check import check_thermal +from backend.periscopex.power_margin_check import check_power_margin +from backend.periscopex.sequencing_check import check_power_sequencing +from backend.periscopex.dnp_check import check_dnp_enables +from backend.periscopex.lifecycle import check_lifecycle +from backend.periscopex.errata_check import check_errata +from backend.periscopex.internal_features_check import check_internal_features +from backend.periscopex.placement_check import check_placement +from backend.periscopex.si_check import check_si class EvalScores(BaseModel): diff --git a/backend/pinscopex/filter_check.py b/backend/periscopex/filter_check.py similarity index 97% rename from backend/pinscopex/filter_check.py rename to backend/periscopex/filter_check.py index d167c1a..02b2dc5 100644 --- a/backend/pinscopex/filter_check.py +++ b/backend/periscopex/filter_check.py @@ -10,7 +10,7 @@ from __future__ import annotations import math import re -from backend.pinscopex.models import ( +from backend.periscopex.models import ( Component, ComponentConstraints, ComponentType, @@ -18,14 +18,14 @@ from backend.pinscopex.models import ( Finding, InductorSpecs, ) -from backend.pinscopex.passive_rail_check import ( +from backend.periscopex.passive_rail_check import ( _cap_farads, _is_ground_net, _is_power_net, _pin_name_tokens, _resistor_ohms, ) -from backend.pinscopex.validate import _match_constraints +from backend.periscopex.validate import _match_constraints _ADC_RATE_KEYS = ("adc_sample_rate", "adc_sample_rate_hz", "data_rate", "data_rate_hz") _DCR_MAX_KEYS = ("max_ferrite_dcr_ohms", "ferrite_dcr_max_ohms", "max_bead_dcr_ohms") @@ -179,7 +179,7 @@ def _filter_finding( reference="netlist topology", net=net, pins=[designator], - rule_id="PS-FLT-001", + rule_id="PE-FLT-001", ) if adc_hz is not None and not (0.1 * adc_hz <= fc <= 20 * adc_hz): return Finding( @@ -197,7 +197,7 @@ def _filter_finding( reference="netlist topology", net=net, pins=[designator], - rule_id="PS-FLT-002", + rule_id="PE-FLT-002", ) rec = ( "fc is within a wide band of the IC sample/data rate." @@ -216,7 +216,7 @@ def _filter_finding( reference="netlist topology", net=net, pins=[designator], - rule_id="PS-FLT-001", + rule_id="PE-FLT-001", ) @@ -351,7 +351,7 @@ def check_filters( reference="IC specs", net=analog, pins=[ref], - rule_id="PS-FLT-003", + rule_id="PE-FLT-003", )) for ref, comp in sorted(graph.components.items()): diff --git a/backend/pinscopex/functional_groups.py b/backend/periscopex/functional_groups.py similarity index 99% rename from backend/pinscopex/functional_groups.py rename to backend/periscopex/functional_groups.py index e1f3481..afeda50 100644 --- a/backend/pinscopex/functional_groups.py +++ b/backend/periscopex/functional_groups.py @@ -14,7 +14,7 @@ from typing import Any, Literal from pydantic import BaseModel -from backend.pinscopex.models import ( +from backend.periscopex.models import ( CapacitorSpecs, Component, ComponentConstraints, @@ -23,7 +23,7 @@ from backend.pinscopex.models import ( NetType, SimpleComponentSpecs, ) -from backend.pinscopex.resolve_passives import _parse_spice_value +from backend.periscopex.resolve_passives import _parse_spice_value RoleHint = Literal[ "decoupling", diff --git a/backend/pinscopex/graph.py b/backend/periscopex/graph.py similarity index 97% rename from backend/pinscopex/graph.py rename to backend/periscopex/graph.py index cb20d44..cd95438 100644 --- a/backend/pinscopex/graph.py +++ b/backend/periscopex/graph.py @@ -6,8 +6,8 @@ import json import re from pathlib import Path -from backend.pinscopex.utils import safe_mpn -from backend.pinscopex.models import ( +from backend.periscopex.utils import safe_mpn +from backend.periscopex.models import ( CadIndexEntry, Component, ComponentConstraints, @@ -23,8 +23,8 @@ from backend.pinscopex.models import ( # Datasheets are loaded here for pin-name enrichment during graph build, # but NOT embedded into the graph. The validator loads them separately. -from backend.pinscopex.parsers import parse_bom, parse_netlist_any -from backend.pinscopex.resolve_passives import SkippedItem, resolve_bom, resolved_to_specs +from backend.periscopex.parsers import parse_bom, parse_netlist_any +from backend.periscopex.resolve_passives import SkippedItem, resolve_bom, resolved_to_specs # --------------------------------------------------------------------------- # Component type classification @@ -291,7 +291,7 @@ def build_graph( if pcb_path is not None: pcb = Path(pcb_path) if pcb.is_file(): - from backend.pinscopex.parsers_kicad_pcb import nets_from_pcb, parse_kicad_pcb + from backend.periscopex.parsers_kicad_pcb import nets_from_pcb, parse_kicad_pcb layout = parse_kicad_pcb(pcb) pcb_nets = nets_from_pcb(layout) @@ -300,7 +300,7 @@ def build_graph( for ref, fp in layout.footprints.items(): parts.setdefault(ref, fp.footprint or "") if fmt.startswith("kicad"): - from backend.pinscopex.parsers_kicad import kicad_part_fields + from backend.periscopex.parsers_kicad import kicad_part_fields for ref, extra in kicad_part_fields(netlist_path).items(): schematic_fields[ref] = { "mpn": extra.get("mpn"), diff --git a/backend/pinscopex/hf_coverage_check.py b/backend/periscopex/hf_coverage_check.py similarity index 94% rename from backend/pinscopex/hf_coverage_check.py rename to backend/periscopex/hf_coverage_check.py index 380b5cb..dafd5af 100644 --- a/backend/pinscopex/hf_coverage_check.py +++ b/backend/periscopex/hf_coverage_check.py @@ -6,8 +6,8 @@ INFO only: HF coverage depends on a ~100 nF close to the pin. from __future__ import annotations -from backend.pinscopex.models import ComponentType, DesignGraph, Finding, NetType -from backend.pinscopex.passive_rail_check import ( +from backend.periscopex.models import ComponentType, DesignGraph, Finding, NetType +from backend.periscopex.passive_rail_check import ( _cap_farads, _is_ground_net, _is_ic_supply_pin, @@ -15,7 +15,7 @@ from backend.pinscopex.passive_rail_check import ( _is_regulator_output_pin, _pin_label, ) -from backend.pinscopex.validate import _match_constraints +from backend.periscopex.validate import _match_constraints _BULK_MIN_F = 1e-6 _HF_MAX_F = 1e-6 @@ -104,6 +104,6 @@ def check_hf_decoupling_coverage( reference="netlist topology (stima)", net=net_name, pins=[f"{ref}.{pin_num}"], - rule_id="PS-ESR-001", + rule_id="PE-ESR-001", )) return findings diff --git a/backend/pinscopex/impedance.py b/backend/periscopex/impedance.py similarity index 92% rename from backend/pinscopex/impedance.py rename to backend/periscopex/impedance.py index 7e91784..f9721aa 100644 --- a/backend/pinscopex/impedance.py +++ b/backend/periscopex/impedance.py @@ -1,4 +1,4 @@ -"""Pinscope facade over ImpedanceFinder's closed-form Z0 solver. +"""Periscope facade over ImpedanceFinder's closed-form Z0 solver. All Z0 numbers come from ImpedenceFinder (`vendor/impedancefinder`, Hammerstad-Jensen / Cohn as in KiCad pcb_calculator). This module only @@ -155,15 +155,15 @@ def stackup_targets( def export_kicad_dru(targets: dict[str, ImpedanceResult]) -> str: - """KiCad custom-rule advice. The user applies it; Pinscope does not DRC the PCB.""" + """KiCad custom-rule advice. The user applies it; Periscope does not DRC the PCB.""" lines = [ "(version 1)", - "# Pinscope impedance advice (ImpedanceFinder solver) — apply in pcbnew.", + "# Periscope impedance advice (ImpedanceFinder solver) — apply in pcbnew.", ] mapping = ( - ("microstrip_50", "PINSCOPE_50OHM", "50Ohm"), - ("diff_90", "PINSCOPE_90OHM_USB", "90Ohm"), - ("diff_100", "PINSCOPE_100OHM_DIFF", "100Ohm"), + ("microstrip_50", "PERISCOPE_50OHM", "50Ohm"), + ("diff_90", "PERISCOPE_90OHM_USB", "90Ohm"), + ("diff_100", "PERISCOPE_100OHM_DIFF", "100Ohm"), ) for key, rule, netclass in mapping: r = targets[key] diff --git a/backend/pinscopex/impedance_traces.py b/backend/periscopex/impedance_traces.py similarity index 97% rename from backend/pinscopex/impedance_traces.py rename to backend/periscopex/impedance_traces.py index 1a77350..f436315 100644 --- a/backend/pinscopex/impedance_traces.py +++ b/backend/periscopex/impedance_traces.py @@ -23,8 +23,8 @@ from impedancefinder.model import ( ZonePolygon, ) -from backend.pinscopex.impedance import GeometryError -from backend.pinscopex.models import DesignGraph, LayoutGraph, NetType +from backend.periscopex.impedance import GeometryError +from backend.periscopex.models import DesignGraph, LayoutGraph, NetType def _stackup(layout: LayoutGraph) -> Stackup: diff --git a/backend/pinscopex/internal_features_check.py b/backend/periscopex/internal_features_check.py similarity index 89% rename from backend/pinscopex/internal_features_check.py rename to backend/periscopex/internal_features_check.py index d26318e..408a4e5 100644 --- a/backend/pinscopex/internal_features_check.py +++ b/backend/periscopex/internal_features_check.py @@ -2,12 +2,12 @@ from __future__ import annotations -from backend.pinscopex.models import ComponentConstraints, DesignGraph, Finding -from backend.pinscopex.passive_rail_check import ( +from backend.periscopex.models import ComponentConstraints, DesignGraph, Finding +from backend.periscopex.passive_rail_check import ( _pin_name_tokens, _resistor_to_power, ) -from backend.pinscopex.validate import _match_constraints +from backend.periscopex.validate import _match_constraints def check_internal_features( @@ -51,6 +51,6 @@ def check_internal_features( reference="internal_features", net=net, pins=[f"{ref}.{pin_name}"], - rule_id="PS-INT-001", + rule_id="PE-INT-001", )) return findings diff --git a/backend/pinscopex/layout_rules.py b/backend/periscopex/layout_rules.py similarity index 100% rename from backend/pinscopex/layout_rules.py rename to backend/periscopex/layout_rules.py diff --git a/backend/pinscopex/led_current_check.py b/backend/periscopex/led_current_check.py similarity index 98% rename from backend/pinscopex/led_current_check.py rename to backend/periscopex/led_current_check.py index ab0091d..8383789 100644 --- a/backend/pinscopex/led_current_check.py +++ b/backend/periscopex/led_current_check.py @@ -17,8 +17,8 @@ from __future__ import annotations import re -from backend.pinscopex.models import ComponentType, DesignGraph, Finding, NetType -from backend.pinscopex.resolve_passives import _parse_spice_value +from backend.periscopex.models import ComponentType, DesignGraph, Finding, NetType +from backend.periscopex.resolve_passives import _parse_spice_value _COLOR_TOKENS = { "R": "red", "RED": "red", diff --git a/backend/pinscopex/library_gate.py b/backend/periscopex/library_gate.py similarity index 100% rename from backend/pinscopex/library_gate.py rename to backend/periscopex/library_gate.py diff --git a/backend/pinscopex/lifecycle.py b/backend/periscopex/lifecycle.py similarity index 96% rename from backend/pinscopex/lifecycle.py rename to backend/periscopex/lifecycle.py index 783f5f6..eedd1a6 100644 --- a/backend/pinscopex/lifecycle.py +++ b/backend/periscopex/lifecycle.py @@ -8,8 +8,8 @@ from pathlib import Path from pydantic import BaseModel -from backend.pinscopex.models import ComponentType, DesignGraph, Finding -from backend.pinscopex.utils import safe_mpn +from backend.periscopex.models import ComponentType, DesignGraph, Finding +from backend.periscopex.utils import safe_mpn _EOL = re.compile( r"\b(obsolete|eol|end\s*of\s*life|discontinued|last\s*time\s*buy|ltb)\b", @@ -188,7 +188,7 @@ def check_lifecycle( recommendation=rec_txt, reference=rec.source or "distributor", pins=[ref], - rule_id="PS-LF-001", + rule_id="PE-LF-001", )) elif rec.lifecycle == "nrnd": findings.append(Finding( @@ -202,7 +202,7 @@ def check_lifecycle( recommendation="Prefer an Active orderable if the design is new.", reference=rec.source or "distributor", pins=[ref], - rule_id="PS-LF-002", + rule_id="PE-LF-002", )) if rec.rohs_compliant is False: findings.append(Finding( @@ -216,6 +216,6 @@ def check_lifecycle( recommendation="Choose a RoHS-compliant orderable of the same MPN family.", reference=rec.source or "distributor", pins=[ref], - rule_id="PS-LF-003", + rule_id="PE-LF-003", )) return findings diff --git a/backend/pinscopex/models.py b/backend/periscopex/models.py similarity index 98% rename from backend/pinscopex/models.py rename to backend/periscopex/models.py index 6ce9466..065248a 100644 --- a/backend/pinscopex/models.py +++ b/backend/periscopex/models.py @@ -1,4 +1,4 @@ -"""Pydantic models for PinscopeX: datasheet constraints and design graph.""" +"""Pydantic models for PeriscopeX: datasheet constraints and design graph.""" from __future__ import annotations @@ -40,7 +40,7 @@ def _check_subtype(v: object) -> str | None: """Shared pre-validator for component_subtype fields.""" if v is None or v == "": return None - from backend.pinscopex.taxonomy import validate_subtype + from backend.periscopex.taxonomy import validate_subtype return validate_subtype(str(v)) @@ -352,7 +352,7 @@ class Finding(BaseModel): source: str | None = None # None/"review" = LLM; "pin_mux_check"/"led_current_check"/"supply_decoupling_check"/… = deterministic net: str | None = None # net name for CAD telemetry / SI filters pins: list[str] = [] # e.g. ["U3.54"] for pan-and-zoom - rule_id: str | None = None # deterministic id, e.g. PS-MUX-001 + rule_id: str | None = None # deterministic id, e.g. PE-MUX-001 cad_sheet: str | None = None # schematic sheet filename for plugin sync cad_uuid: str | None = None # KiCad symbol/pin uuid variant: str | None = None # DNP / ECO / assembly variant diff --git a/backend/pinscopex/nc_pin_check.py b/backend/periscopex/nc_pin_check.py similarity index 98% rename from backend/pinscopex/nc_pin_check.py rename to backend/periscopex/nc_pin_check.py index f73d02f..b73df31 100644 --- a/backend/pinscopex/nc_pin_check.py +++ b/backend/periscopex/nc_pin_check.py @@ -4,7 +4,7 @@ from __future__ import annotations import re -from backend.pinscopex.models import ( +from backend.periscopex.models import ( ComponentConstraints, ComponentType, DesignGraph, @@ -70,7 +70,7 @@ def _finding(ref, mpn, pin_num, pin_name, net, others) -> Finding: why="No-connect pins should remain unconnected or on an explicit NC net.", recommendation="Leave the NC pin floating or disconnect the net.", reference="pintable", - rule_id="PS-NC-001", + rule_id="PE-NC-001", net=net, pins=[f"{ref}.{pin_num}"], ) diff --git a/backend/pinscopex/netlist_bundle.py b/backend/periscopex/netlist_bundle.py similarity index 98% rename from backend/pinscopex/netlist_bundle.py rename to backend/periscopex/netlist_bundle.py index 8066dd2..126d7b1 100644 --- a/backend/pinscopex/netlist_bundle.py +++ b/backend/periscopex/netlist_bundle.py @@ -11,7 +11,7 @@ import zipfile from dataclasses import dataclass from pathlib import Path -from backend.pinscopex.parsers import detect_netlist_format +from backend.periscopex.parsers import detect_netlist_format MAX_BUNDLE_BYTES = 30 * 1024 * 1024 _MAX_ZIP_MEMBERS = 400 @@ -144,7 +144,7 @@ def find_bom(work: Path) -> Path | None: def _sheetfiles_of(path: Path) -> list[str]: - from backend.pinscopex.parsers_kicad import _parse_sexp, _sheetfiles, _tag + from backend.periscopex.parsers_kicad import _parse_sexp, _sheetfiles, _tag text = path.read_text(encoding="utf-8", errors="replace") tree = _parse_sexp(text) diff --git a/backend/pinscopex/parsers.py b/backend/periscopex/parsers.py similarity index 98% rename from backend/pinscopex/parsers.py rename to backend/periscopex/parsers.py index 951a365..27af3dd 100644 --- a/backend/pinscopex/parsers.py +++ b/backend/periscopex/parsers.py @@ -200,10 +200,10 @@ def parse_netlist_any( sample = p.read_bytes()[:2048] fmt = detect_netlist_format(sample) if fmt == "edif": - from backend.pinscopex.parsers_edif import parse_edif_netlist + from backend.periscopex.parsers_edif import parse_edif_netlist parts, nets = parse_edif_netlist(p, include_subdesigns=include_subdesigns) elif fmt.startswith("kicad"): - from backend.pinscopex.parsers_kicad import parse_kicad + from backend.periscopex.parsers_kicad import parse_kicad parts, nets, _ = parse_kicad(p) else: parts, nets = parse_netlist(p, known_refs=known_refs) diff --git a/backend/pinscopex/parsers_edif.py b/backend/periscopex/parsers_edif.py similarity index 100% rename from backend/pinscopex/parsers_edif.py rename to backend/periscopex/parsers_edif.py diff --git a/backend/pinscopex/parsers_kicad.py b/backend/periscopex/parsers_kicad.py similarity index 100% rename from backend/pinscopex/parsers_kicad.py rename to backend/periscopex/parsers_kicad.py diff --git a/backend/pinscopex/parsers_kicad_pcb.py b/backend/periscopex/parsers_kicad_pcb.py similarity index 99% rename from backend/pinscopex/parsers_kicad_pcb.py rename to backend/periscopex/parsers_kicad_pcb.py index 4252976..2308153 100644 --- a/backend/pinscopex/parsers_kicad_pcb.py +++ b/backend/periscopex/parsers_kicad_pcb.py @@ -7,7 +7,7 @@ from __future__ import annotations from pathlib import Path -from backend.pinscopex.models import ( +from backend.periscopex.models import ( LayoutDielectric, LayoutFootprint, LayoutGraph, @@ -17,7 +17,7 @@ from backend.pinscopex.models import ( LayoutVia, LayoutZone, ) -from backend.pinscopex.parsers_kicad import ( +from backend.periscopex.parsers_kicad import ( _at, _fnum, _kid, diff --git a/backend/pinscopex/passive_rail_check.py b/backend/periscopex/passive_rail_check.py similarity index 97% rename from backend/pinscopex/passive_rail_check.py rename to backend/periscopex/passive_rail_check.py index ac8fb60..600d17e 100644 --- a/backend/pinscopex/passive_rail_check.py +++ b/backend/periscopex/passive_rail_check.py @@ -9,7 +9,7 @@ from __future__ import annotations import re -from backend.pinscopex.models import ( +from backend.periscopex.models import ( CapacitorSpecs, Component, ComponentConstraints, @@ -19,9 +19,9 @@ from backend.pinscopex.models import ( NetType, ResistorSpecs, ) -from backend.pinscopex.validate import _match_constraints -from backend.pinscopex.led_current_check import _parse_resistance -from backend.pinscopex.resolve_passives import _parse_spice_value +from backend.periscopex.validate import _match_constraints +from backend.periscopex.led_current_check import _parse_resistance +from backend.periscopex.resolve_passives import _parse_spice_value _SUPPLY_PIN_RE = re.compile( r"(?:^|[_/])(VDD|VCC|VDDA|VDDD|VDDIO|DVDD|AVDD|IOVDD|VDD33|VDD18|" @@ -114,7 +114,7 @@ def check_supply_decoupling( reference="netlist topology", net=net_name, pins=[f"{ref}.{pin_num}"], - rule_id="PS-DEC-001", + rule_id="PE-DEC-001", )) continue min_f = _VOUT_MIN_FARADS if role == "output" else _VDD_MIN_FARADS @@ -144,7 +144,7 @@ def check_supply_decoupling( reference="netlist topology", net=net_name, pins=[f"{ref}.{pin_num}"], - rule_id="PS-DEC-002", + rule_id="PE-DEC-002", )) return findings @@ -202,7 +202,7 @@ def check_i2c_pullups( reference="NXP UM10204 (wide bound)", net=net_name, pins=[f"{ref}.{pin_num}"], - rule_id="PS-I2C-002", + rule_id="PE-I2C-002", )) continue pin_label = _pin_label(cons, pin_num, net_name) @@ -228,7 +228,7 @@ def check_i2c_pullups( reference="netlist topology", net=net_name, pins=[f"{ref}.{pin_num}"], - rule_id="PS-I2C-001", + rule_id="PE-I2C-001", )) return findings @@ -285,7 +285,7 @@ def check_reset_pullups( reference="netlist topology", net=net_name, pins=[f"{ref}.{pin_num}"], - rule_id="PS-RST-002", + rule_id="PE-RST-002", )) if _resistor_to_power(graph, net_name): continue @@ -311,7 +311,7 @@ def check_reset_pullups( reference="netlist topology", net=net_name, pins=[f"{ref}.{pin_num}"], - rule_id="PS-RST-001", + rule_id="PE-RST-001", )) return findings diff --git a/backend/pinscopex/pdf_text.py b/backend/periscopex/pdf_text.py similarity index 100% rename from backend/pinscopex/pdf_text.py rename to backend/periscopex/pdf_text.py diff --git a/backend/pinscopex/pin_function_tokens.py b/backend/periscopex/pin_function_tokens.py similarity index 100% rename from backend/pinscopex/pin_function_tokens.py rename to backend/periscopex/pin_function_tokens.py diff --git a/backend/pinscopex/pin_mux_check.py b/backend/periscopex/pin_mux_check.py similarity index 97% rename from backend/pinscopex/pin_mux_check.py rename to backend/periscopex/pin_mux_check.py index bb66f42..c763100 100644 --- a/backend/pinscopex/pin_mux_check.py +++ b/backend/periscopex/pin_mux_check.py @@ -16,19 +16,19 @@ perspective is ambiguous). from __future__ import annotations -from backend.pinscopex.models import ( +from backend.periscopex.models import ( ComponentConstraints, ComponentType, DesignGraph, Finding, ) -from backend.pinscopex.pin_function_tokens import ( +from backend.periscopex.pin_function_tokens import ( complement, normalize_functions, parse_net_token, signals_for_peripheral, ) -from backend.pinscopex.validate import _match_constraints +from backend.periscopex.validate import _match_constraints def check_pin_mux_feasibility( @@ -171,5 +171,5 @@ def _feasibility_finding( reference=f"{mpn or ref} alternate-function table", net=net_name, pins=[f"{ref}.{pin_num}"], - rule_id="PS-MUX-001", + rule_id="PE-MUX-001", ) diff --git a/backend/pinscopex/placement_check.py b/backend/periscopex/placement_check.py similarity index 95% rename from backend/pinscopex/placement_check.py rename to backend/periscopex/placement_check.py index 7f8ac70..36c6fab 100644 --- a/backend/pinscopex/placement_check.py +++ b/backend/periscopex/placement_check.py @@ -2,12 +2,12 @@ Runs only when a LayoutGraph is present and a decoupling_proximity rule has a numeric max_distance_mm. Null millimetres skip — no 3 mm default. -Thermal vias (`PS-PLC-002`) skip without courtyard vertices and without -min_via_count — no invented pad radius. same_layer (`PS-PLC-003`) uses +Thermal vias (`PE-PLC-002`) skip without courtyard vertices and without +min_via_count — no invented pad radius. same_layer (`PE-PLC-003`) uses the boolean parameter plus footprint layers from the PCB. Crystals use the same decoupling_proximity rule. Track length is shortest path on segments vs max_distance_mm — no invented “much larger than euclidean”. -Keepout (`PS-PLC-004`) is a foreign net endpoint inside the courtyard. +Keepout (`PE-PLC-004`) is a foreign net endpoint inside the courtyard. """ from __future__ import annotations @@ -15,7 +15,7 @@ from __future__ import annotations import heapq import math -from backend.pinscopex.models import ( +from backend.periscopex.models import ( ComponentConstraints, ComponentType, DesignGraph, @@ -23,7 +23,7 @@ from backend.pinscopex.models import ( LayoutGraph, LayoutPad, ) -from backend.pinscopex.validate import _match_constraints +from backend.periscopex.validate import _match_constraints def _pad_for(layout: LayoutGraph, ref: str, number: str) -> LayoutPad | None: @@ -169,7 +169,7 @@ def _decoupling_finding(ref, comp, cons, rule, graph: DesignGraph, layout: Layou status="ERROR", recommendation="Place the decoupling capacitor closer to the supply pin.", source="placement_check", - rule_id="PS-PLC-001", + rule_id="PE-PLC-001", net=net, pins=[pin_no], source_page=rule.get("source_page"), @@ -231,7 +231,7 @@ def _same_layer_finding(ref, comp, cons, rule, graph: DesignGraph, layout: Layou status="WARNING", recommendation="Place the decoupling capacitor on the same layer or add a via in the courtyard.", source="placement_check", - rule_id="PS-PLC-003", + rule_id="PE-PLC-003", net=net, pins=[pin_no], source_page=rule.get("source_page"), @@ -274,7 +274,7 @@ def _thermal_via_finding(ref, comp, cons, rule, layout: LayoutGraph) -> list[Fin status="ERROR", recommendation="Add vias in the thermal pad courtyard.", source="placement_check", - rule_id="PS-PLC-002", + rule_id="PE-PLC-002", pins=[pin] if pin else [], source_page=rule.get("source_page"), )] @@ -308,7 +308,7 @@ def _keepout_finding(ref, comp, cons, rule, graph: DesignGraph, layout: LayoutGr status="WARNING", recommendation="Keep other nets out of the courtyard.", source="placement_check", - rule_id="PS-PLC-004", + rule_id="PE-PLC-004", net=net, pins=[pin_no], source_page=rule.get("source_page"), diff --git a/backend/pinscopex/placement_pack.py b/backend/periscopex/placement_pack.py similarity index 97% rename from backend/pinscopex/placement_pack.py rename to backend/periscopex/placement_pack.py index efa65f3..fc78c3c 100644 --- a/backend/pinscopex/placement_pack.py +++ b/backend/periscopex/placement_pack.py @@ -12,8 +12,8 @@ from typing import Literal from pydantic import BaseModel -from backend.pinscopex.functional_groups import FunctionalGroupsReport, PlacementIcGroup -from backend.pinscopex.models import DesignGraph, LayoutGraph, LayoutPad +from backend.periscopex.functional_groups import FunctionalGroupsReport, PlacementIcGroup +from backend.periscopex.models import DesignGraph, LayoutGraph, LayoutPad SkipReason = Literal[ "no_pcb_footprints", diff --git a/backend/pinscopex/power_margin_check.py b/backend/periscopex/power_margin_check.py similarity index 94% rename from backend/pinscopex/power_margin_check.py rename to backend/periscopex/power_margin_check.py index acd9bd7..2389550 100644 --- a/backend/pinscopex/power_margin_check.py +++ b/backend/periscopex/power_margin_check.py @@ -6,8 +6,8 @@ IC on the rail has a spec. Trace resistance is never estimated. from __future__ import annotations -from backend.pinscopex.led_current_check import _net_voltage, _parse_resistance -from backend.pinscopex.models import ( +from backend.periscopex.led_current_check import _net_voltage, _parse_resistance +from backend.periscopex.models import ( Component, ComponentConstraints, ComponentType, @@ -16,8 +16,8 @@ from backend.pinscopex.models import ( InductorSpecs, ResistorSpecs, ) -from backend.pinscopex.passive_rail_check import _is_ground_net -from backend.pinscopex.thermal_check import ( +from backend.periscopex.passive_rail_check import _is_ground_net +from backend.periscopex.thermal_check import ( _IOUT_MAX_KEYS, _LOAD_KEYS, _VIN_PIN, @@ -27,7 +27,7 @@ from backend.pinscopex.thermal_check import ( _pin_net_by_role, _specs_values, ) -from backend.pinscopex.validate import _match_constraints +from backend.periscopex.validate import _match_constraints _IQ_KEYS = ( "iq_a", "quiescent_current_a", "supply_current_a", "idd_a", "icc_a", @@ -139,7 +139,7 @@ def check_power_margin( reference="regulator Iout_max", net=vout, pins=[ref], - rule_id="PS-PWR-001", + rule_id="PE-PWR-001", )) # IR drop only through an explicit series R/ferrite on VIN or VOUT. @@ -178,6 +178,6 @@ def check_power_margin( reference="netlist series R", net=vin, pins=[r], - rule_id="PS-PWR-001", + rule_id="PE-PWR-001", )) return findings diff --git a/backend/pinscopex/quote_verify.py b/backend/periscopex/quote_verify.py similarity index 96% rename from backend/pinscopex/quote_verify.py rename to backend/periscopex/quote_verify.py index 9b5d9be..bf003e5 100644 --- a/backend/pinscopex/quote_verify.py +++ b/backend/periscopex/quote_verify.py @@ -11,9 +11,9 @@ import re from collections.abc import Callable from pathlib import Path -from backend.pinscopex.models import Finding -from backend.pinscopex.pdf_text import pdf_page_texts as extract_pdf_pages -from backend.pinscopex.utils import safe_mpn +from backend.periscopex.models import Finding +from backend.periscopex.pdf_text import pdf_page_texts as extract_pdf_pages +from backend.periscopex.utils import safe_mpn _MIN_QUOTE_CHARS = 12 _EMPTY_PAGE_ALNUM = 40 diff --git a/backend/pinscopex/resolve_passives.py b/backend/periscopex/resolve_passives.py similarity index 99% rename from backend/pinscopex/resolve_passives.py rename to backend/periscopex/resolve_passives.py index e5b4955..0c4a6f4 100644 --- a/backend/pinscopex/resolve_passives.py +++ b/backend/periscopex/resolve_passives.py @@ -8,7 +8,7 @@ import re from collections import defaultdict from pathlib import Path -from backend.pinscopex.models import ( +from backend.periscopex.models import ( CapacitorSpecs, ComponentSpecs, ComponentType, @@ -19,7 +19,7 @@ from backend.pinscopex.models import ( SimpleComponentSpecs, ValueDecoder, ) -from backend.pinscopex.parsers import parse_bom +from backend.periscopex.parsers import parse_bom # --------------------------------------------------------------------------- diff --git a/backend/pinscopex/review_fingerprint.py b/backend/periscopex/review_fingerprint.py similarity index 95% rename from backend/pinscopex/review_fingerprint.py rename to backend/periscopex/review_fingerprint.py index dcc0abf..e2826c7 100644 --- a/backend/pinscopex/review_fingerprint.py +++ b/backend/periscopex/review_fingerprint.py @@ -5,8 +5,8 @@ from __future__ import annotations import hashlib import json -from backend.pinscopex.models import ComponentType, DesignGraph -from backend.pinscopex.validate import _match_constraints +from backend.periscopex.models import ComponentType, DesignGraph +from backend.periscopex.validate import _match_constraints def ic_neighborhood_fingerprint( diff --git a/backend/pinscopex/review_workflow.py b/backend/periscopex/review_workflow.py similarity index 98% rename from backend/pinscopex/review_workflow.py rename to backend/periscopex/review_workflow.py index f32d506..5c7a047 100644 --- a/backend/pinscopex/review_workflow.py +++ b/backend/periscopex/review_workflow.py @@ -14,7 +14,7 @@ import json from datetime import datetime, timezone from typing import Any, Iterable, Literal -from backend.pinscopex.models import Finding +from backend.periscopex.models import Finding ReviewState = Literal["open", "false_positive", "accepted", "wontfix"] VALID_STATES: frozenset[str] = frozenset({"open", "false_positive", "accepted", "wontfix"}) diff --git a/backend/pinscopex/sequencing_check.py b/backend/periscopex/sequencing_check.py similarity index 94% rename from backend/pinscopex/sequencing_check.py rename to backend/periscopex/sequencing_check.py index 0fc48cc..9c15aa6 100644 --- a/backend/pinscopex/sequencing_check.py +++ b/backend/periscopex/sequencing_check.py @@ -7,20 +7,20 @@ from __future__ import annotations import re -from backend.pinscopex.models import ( +from backend.periscopex.models import ( ComponentConstraints, ComponentType, DesignGraph, Finding, ) -from backend.pinscopex.thermal_check import ( +from backend.periscopex.thermal_check import ( _VIN_PIN, _VOUT_PIN, _is_ldo, _pin_net_by_role, _specs_values, ) -from backend.pinscopex.validate import _match_constraints +from backend.periscopex.validate import _match_constraints _PG_RE = re.compile(r"(?:^|[_/])(PG|PGOOD|PWRGD|POWER_GOOD|POK)(?:$|[_/\d])", re.I) _EN_RE = re.compile( @@ -86,7 +86,7 @@ def check_power_sequencing( reference="power_sequence", net=den, pins=[dref, uref], - rule_id="PS-SEQ-001", + rule_id="PE-SEQ-001", )) continue if upg != den: @@ -104,6 +104,6 @@ def check_power_sequencing( reference="power_sequence", net=den, pins=[f"{uref}", f"{dref}"], - rule_id="PS-SEQ-001", + rule_id="PE-SEQ-001", )) return findings diff --git a/backend/pinscopex/si_check.py b/backend/periscopex/si_check.py similarity index 93% rename from backend/pinscopex/si_check.py rename to backend/periscopex/si_check.py index 0f3183a..29ad7b7 100644 --- a/backend/pinscopex/si_check.py +++ b/backend/periscopex/si_check.py @@ -8,8 +8,8 @@ from __future__ import annotations import math -from backend.pinscopex.models import DesignGraph, Finding, LayoutGraph, LayoutSegment -from backend.pinscopex.validate import _match_constraints +from backend.periscopex.models import DesignGraph, Finding, LayoutGraph, LayoutSegment +from backend.periscopex.validate import _match_constraints _PAIR_SUFFIXES = (("_DP", "_DM"), ("_P", "_N"), ("+", "-")) @@ -83,7 +83,7 @@ def check_si( status="ERROR", recommendation="Length-match the differential pair.", source="si_check", - rule_id="PS-SI-001", + rule_id="PE-SI-001", net=net, pins=[], source_page=page, diff --git a/backend/pinscopex/taxonomy.py b/backend/periscopex/taxonomy.py similarity index 100% rename from backend/pinscopex/taxonomy.py rename to backend/periscopex/taxonomy.py diff --git a/backend/pinscopex/thermal_check.py b/backend/periscopex/thermal_check.py similarity index 95% rename from backend/pinscopex/thermal_check.py rename to backend/periscopex/thermal_check.py index ccf46b8..221504c 100644 --- a/backend/pinscopex/thermal_check.py +++ b/backend/periscopex/thermal_check.py @@ -8,14 +8,14 @@ from __future__ import annotations import re -from backend.pinscopex.led_current_check import ( +from backend.periscopex.led_current_check import ( _leg_color, _net_voltage, _parse_resistance, _series_resistor, _vf, ) -from backend.pinscopex.models import ( +from backend.periscopex.models import ( Component, ComponentConstraints, ComponentType, @@ -23,9 +23,9 @@ from backend.pinscopex.models import ( Finding, ResistorSpecs, ) -from backend.pinscopex.passive_rail_check import _pin_name_tokens -from backend.pinscopex.resolve_passives import _parse_spice_value -from backend.pinscopex.validate import _match_constraints +from backend.periscopex.passive_rail_check import _pin_name_tokens +from backend.periscopex.resolve_passives import _parse_spice_value +from backend.periscopex.validate import _match_constraints _TA_C = 25.0 _TJ_WARN_C = 125.0 @@ -175,12 +175,12 @@ def _ldo_thermal( reference="thermal estimate", net=net, pins=[ref], - rule_id="PS-TH-001", + rule_id="PE-TH-001", )) continue tj = _TA_C + p * theta status = "WARNING" if tj >= _TJ_WARN_C else "INFO" - rule = "PS-TH-002" if status == "WARNING" else "PS-TH-001" + rule = "PE-TH-002" if status == "WARNING" else "PE-TH-001" out.append(Finding( designator=ref, mpn=comp.mpn or "", @@ -251,7 +251,7 @@ def _resistor_thermal(graph: DesignGraph) -> list[Finding]: reference="resistor power rating", net=net, pins=[rref], - rule_id="PS-TH-003", + rule_id="PE-TH-003", )) for ref, comp in sorted(graph.components.items()): @@ -293,6 +293,6 @@ def _resistor_thermal(graph: DesignGraph) -> list[Finding]: reference="resistor power rating", net=nets[0], pins=[ref], - rule_id="PS-TH-003", + rule_id="PE-TH-003", )) return out diff --git a/backend/pinscopex/utils.py b/backend/periscopex/utils.py similarity index 89% rename from backend/pinscopex/utils.py rename to backend/periscopex/utils.py index df43431..ec3eea3 100644 --- a/backend/pinscopex/utils.py +++ b/backend/periscopex/utils.py @@ -1,4 +1,4 @@ -"""Shared utility functions for the pinscopex core library.""" +"""Shared utility functions for the periscopex core library.""" from __future__ import annotations diff --git a/backend/pinscopex/validate.py b/backend/periscopex/validate.py similarity index 99% rename from backend/pinscopex/validate.py rename to backend/periscopex/validate.py index 70d8f12..de72978 100644 --- a/backend/pinscopex/validate.py +++ b/backend/periscopex/validate.py @@ -20,7 +20,7 @@ from dotenv import load_dotenv load_dotenv() -from backend.pinscopex.models import ( +from backend.periscopex.models import ( ComponentConstraints, ComponentType, DesignGraph, @@ -28,9 +28,9 @@ from backend.pinscopex.models import ( NetType, ValidationReport, ) -from backend.pinscopex.pin_function_tokens import parse_net_token -from backend.pinscopex.quote_verify import verify_finding_citations -from backend.pinscopex.validation_tools import ( +from backend.periscopex.pin_function_tokens import parse_net_token +from backend.periscopex.quote_verify import verify_finding_citations +from backend.periscopex.validation_tools import ( ALL_TOOLS, SUBMIT_REVIEW_SCHEMA, ConstraintsMap, @@ -136,7 +136,7 @@ INFO (worth noting but unlikely to cause problems). - **source_quote**: The exact verbatim sentence or clause from the datasheet \ that states the requirement. Copy it precisely, character-for-character (a \ short span, ~200 chars max) so it can be located and highlighted in the PDF. \ -ERROR and WARNING findings **must** include this field. Pinscope checks the \ +ERROR and WARNING findings **must** include this field. Periscope checks the \ quote against the extracted text of the cited page (±1); invented or \ paraphrased quotes are demoted to Unverified WARNING. Omit the field only \ when the requirement is shown solely in a figure or a rasterized table with \ @@ -996,7 +996,7 @@ def validate_design( model: str = "claude-sonnet-4-6", ) -> ValidationReport: """Load graph, review every IC against its datasheet, write report.""" - from backend.pinscopex.utils import safe_mpn + from backend.periscopex.utils import safe_mpn raw = json.loads(Path(graph_path).read_text()) graph = DesignGraph.model_validate(raw) diff --git a/backend/pinscopex/validation_tools.py b/backend/periscopex/validation_tools.py similarity index 99% rename from backend/pinscopex/validation_tools.py rename to backend/periscopex/validation_tools.py index 2182f38..7571be7 100644 --- a/backend/pinscopex/validation_tools.py +++ b/backend/periscopex/validation_tools.py @@ -13,11 +13,11 @@ from dataclasses import dataclass, field from pathlib import Path from typing import Any -from backend.pinscopex.models import ( +from backend.periscopex.models import ( ComponentConstraints, DesignGraph, ) -from backend.pinscopex.utils import safe_mpn +from backend.periscopex.utils import safe_mpn log = logging.getLogger(__name__) @@ -509,7 +509,7 @@ def get_datasheet_excerpt( return ("get_datasheet_excerpt called without per-review state — " "this is a bug, no excerpt returned.", None) - # Lazy import to avoid backend↔pinscopex circular dependency at module load. + # Lazy import to avoid backend↔periscopex circular dependency at module load. from backend.services.llm import PdfBlock designator = (designator or "").strip() @@ -776,7 +776,7 @@ SUBMIT_REVIEW_SCHEMA = { "type": "string", "description": ( "Required for ERROR and WARNING. Exact verbatim " - "datasheet text (max ~200 chars). Pinscope " + "datasheet text (max ~200 chars). Periscope " "checks it against the PDF page. Omit only if " "the evidence is a figure/scan with no text." ), diff --git a/backend/routers/admin.py b/backend/routers/admin.py index 77060f3..0a484ff 100644 --- a/backend/routers/admin.py +++ b/backend/routers/admin.py @@ -15,7 +15,7 @@ from fastapi.responses import JSONResponse from pydantic import BaseModel from backend.config import settings -from backend.pinscopex.utils import safe_mpn +from backend.periscopex.utils import safe_mpn from backend.routers.deps import get_storage from backend.services import admin_settings as settings_svc from backend.services.billing_hook import get_billing diff --git a/backend/routers/auth.py b/backend/routers/auth.py index 9796033..9235bab 100644 --- a/backend/routers/auth.py +++ b/backend/routers/auth.py @@ -1,4 +1,4 @@ -"""Local Pinscope auth endpoints (register / login / me).""" +"""Local Periscope auth endpoints (register / login / me).""" from __future__ import annotations diff --git a/backend/routers/contact.py b/backend/routers/contact.py index 76b8407..b558564 100644 --- a/backend/routers/contact.py +++ b/backend/routers/contact.py @@ -41,10 +41,10 @@ class ContactResponse(BaseModel): def _build_contact_message(data: ContactRequest) -> MIMEMultipart: """Build the contact form email.""" msg = MIMEMultipart("alternative") - msg["From"] = f"Pinscope <{settings.email_sender}>" + msg["From"] = f"Periscope <{settings.email_sender}>" msg["To"] = settings.contact_recipient msg["Reply-To"] = data.email - msg["Subject"] = f"[Pinscope Contact] {data.subject or 'New message'} from {data.name}" + msg["Subject"] = f"[Periscope Contact] {data.subject or 'New message'} from {data.name}" # Plain text lines = [ @@ -55,7 +55,7 @@ def _build_contact_message(data: ContactRequest) -> MIMEMultipart: lines.append(f"Company: {data.company}") if data.subject: lines.append(f"Subject: {data.subject}") - lines += ["", data.message, "", "— Sent from the Pinscope contact form"] + lines += ["", data.message, "", "— Sent from the Periscope contact form"] msg.attach(MIMEText("\n".join(lines), "plain")) # HTML @@ -94,7 +94,7 @@ def _build_contact_message(data: ContactRequest) -> MIMEMultipart: {rows}
{message}
-

Sent from the Pinscope contact form

+

Sent from the Periscope contact form

""" msg.attach(MIMEText(html_body, "html")) diff --git a/backend/routers/impedance.py b/backend/routers/impedance.py index d120e46..6aa19bd 100644 --- a/backend/routers/impedance.py +++ b/backend/routers/impedance.py @@ -10,7 +10,7 @@ from typing import Literal from fastapi import APIRouter, HTTPException, Request from pydantic import BaseModel -from backend.pinscopex.impedance import ( +from backend.periscopex.impedance import ( GeometryError, TraceGeometry, coupled_diff_z, @@ -21,13 +21,13 @@ from backend.pinscopex.impedance import ( stackup_targets, stripline_z0, ) -from backend.pinscopex.impedance_traces import ( +from backend.periscopex.impedance_traces import ( NET_WALK_PITCH_MM, analyze_specified_nets, ) -from backend.pinscopex.antenna_rf import build_antenna_report, build_design_recipe -from backend.pinscopex.models import DesignGraph, LayoutGraph -from backend.pinscopex.parsers_kicad_pcb import parse_kicad_pcb +from backend.periscopex.antenna_rf import build_antenna_report, build_design_recipe +from backend.periscopex.models import DesignGraph, LayoutGraph +from backend.periscopex.parsers_kicad_pcb import parse_kicad_pcb from backend.routers.deps import get_storage, resolve_or_404 from backend.services import projects as proj_svc diff --git a/backend/routers/pipeline.py b/backend/routers/pipeline.py index cdc23c6..27c0470 100644 --- a/backend/routers/pipeline.py +++ b/backend/routers/pipeline.py @@ -84,7 +84,7 @@ async def start(project_id: str, request: Request): # Idempotent enqueue: only one ``draft|complete|error|cancelled`` -> # ``queued`` transition can win. Concurrent /start clicks => 409. - from backend._version import PINSCOPE_VERSION + from backend._version import PERISCOPE_VERSION try: proj_svc.transition_status( storage, owner_user_id, project_id, @@ -92,7 +92,7 @@ async def start(project_id: str, request: Request): to_status=proj_svc.STATUS_QUEUED, cancel_requested=False, execution_name=None, - pinscope_version=PINSCOPE_VERSION, + periscope_version=PERISCOPE_VERSION, ) except proj_svc.StatusConflict: raise HTTPException(409, "Pipeline already running or queued") @@ -199,7 +199,7 @@ async def reprocess(project_id: str, request: Request, req: ReprocessRequest | N (default) skips ICs that already produced a review; ``all`` re-reviews every IC. """ - from backend._version import PINSCOPE_VERSION + from backend._version import PERISCOPE_VERSION storage = get_storage(request) owner_user_id, meta = await resolve_or_404(request, project_id) @@ -241,7 +241,7 @@ async def reprocess(project_id: str, request: Request, req: ReprocessRequest | N pause_checkpoint=None, pause_reason=None, completed_review_refs=keep_refs, - pinscope_version=PINSCOPE_VERSION, + periscope_version=PERISCOPE_VERSION, ) except proj_svc.StatusConflict: raise HTTPException(409, "Pipeline already running or queued") diff --git a/backend/routers/projects.py b/backend/routers/projects.py index 617e0fc..4408176 100644 --- a/backend/routers/projects.py +++ b/backend/routers/projects.py @@ -11,7 +11,7 @@ from pydantic import BaseModel MAX_UPLOAD_BYTES = 30 * 1024 * 1024 # 30 MB from backend.config import settings -from backend.pinscopex.utils import safe_mpn +from backend.periscopex.utils import safe_mpn from backend.routers.deps import get_storage, get_user_id, resolve_or_404 from backend.services import projects as proj_svc @@ -63,7 +63,7 @@ async def check_library(req: LibraryCheckRequest, request: Request): passive_resolved: list[str] = [] if req.passive_mpns: - from backend.pinscopex.resolve_passives import resolve_mpn + from backend.periscopex.resolve_passives import resolve_mpn passive_resolved = [ mpn for mpn in req.passive_mpns @@ -256,7 +256,7 @@ async def get_netlist_subdesigns(project_id: str, request: Request): ``selected`` list (None = "include everything") so the wizard can render the picker pre-populated. """ - from backend.pinscopex.parsers_edif import list_edif_subdesigns + from backend.periscopex.parsers_edif import list_edif_subdesigns import tempfile, os storage = get_storage(request) @@ -360,7 +360,7 @@ async def upload_bom( import os import tempfile - from backend.pinscopex.parsers import parse_bom + from backend.periscopex.parsers import parse_bom try: tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv") @@ -420,7 +420,7 @@ async def upload_bom( # simple → datasheet upload). Mirrors the bucket logic in # services/pipeline.py:_stage_bom_parse so the field is correct after # either path runs. - from backend.pinscopex.taxonomy import SIMPLE_TYPES, type_for_ref + from backend.periscopex.taxonomy import SIMPLE_TYPES, type_for_ref ic_mpns: list[str] = [] passive_mpns: list[str] = [] @@ -511,9 +511,9 @@ async def upload_netlist( raise HTTPException(404, "Project not found") user_id = result[0] # owner_user_id for storage paths - from backend.pinscopex.netlist_bundle import materialize_netlist_upload - from backend.pinscopex.parsers import parse_netlist_any, validate_netlist - from backend.pinscopex.parsers_edif import list_edif_subdesigns + from backend.periscopex.netlist_bundle import materialize_netlist_upload + from backend.periscopex.parsers import parse_netlist_any, validate_netlist + from backend.periscopex.parsers_edif import list_edif_subdesigns import tempfile blobs: list[tuple[str, bytes]] = [] @@ -616,7 +616,7 @@ async def upload_pcb(project_id: str, file: UploadFile, request: Request): if len(data) > MAX_UPLOAD_BYTES: raise HTTPException(413, f"File too large (max {MAX_UPLOAD_BYTES // 1024 // 1024} MB)") import tempfile, os - from backend.pinscopex.parsers_kicad_pcb import parse_kicad_pcb + from backend.periscopex.parsers_kicad_pcb import parse_kicad_pcb tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".kicad_pcb") try: @@ -647,7 +647,7 @@ def _build_designator_pins( (natural sort on refs and on pin numbers) so the wizard's dropdowns look identical regardless of netlist format. """ - from backend.pinscopex.utils import natural_sort_key + from backend.periscopex.utils import natural_sort_key by_ref: dict[str, dict[str, str]] = {ref: {} for ref in parts} for net_name, pins in nets.items(): @@ -1079,7 +1079,7 @@ async def lcsc_resolve_passive( # Catalog miss (ferrite, odd text): LLM path, charged if the logger has tokens. # Download taxonomy to a temp dir so auto_resolve_specs can read/write it. - # Mirrors the PipelineWorkspace pattern: pinscopex operates on local paths. + # Mirrors the PipelineWorkspace pattern: periscopex operates on local paths. api_logger = ApiLogger() with tempfile.TemporaryDirectory() as tmpdir: tax_dir = Path(tmpdir) / "taxonomy" diff --git a/backend/routers/reports.py b/backend/routers/reports.py index 04e050a..4ec9908 100644 --- a/backend/routers/reports.py +++ b/backend/routers/reports.py @@ -11,15 +11,15 @@ from fastapi import APIRouter, HTTPException, Request from fastapi.responses import JSONResponse, Response from pydantic import BaseModel -from backend.pinscopex.models import Finding -from backend.pinscopex.review_workflow import ( +from backend.periscopex.models import Finding +from backend.periscopex.review_workflow import ( ReviewError, apply_review_state, build_eco, eco_csv, sign_report, ) -from backend.pinscopex.utils import safe_mpn +from backend.periscopex.utils import safe_mpn from backend.routers.deps import get_storage, get_user_id, resolve_or_404 from backend.services import projects as proj_svc @@ -51,7 +51,7 @@ async def get_cad_bridge(project_id: str, request: Request): storage = get_storage(request) owner_user_id, _ = await resolve_or_404(request, project_id) prefix = proj_svc.project_prefix(owner_user_id, project_id) - key = f"{prefix}/pinscope-findings.json" + key = f"{prefix}/periscope-findings.json" if not storage.exists(key): raise HTTPException(404, "CAD bridge not found — run the pipeline first") return JSONResponse(storage.read_json(key)) diff --git a/backend/services/cost_estimator.py b/backend/services/cost_estimator.py index cd8561f..c77c1d2 100644 --- a/backend/services/cost_estimator.py +++ b/backend/services/cost_estimator.py @@ -27,10 +27,10 @@ from typing import Any, Literal from pydantic import BaseModel from backend.config import settings -from backend.pinscopex.parsers import parse_bom -from backend.pinscopex.resolve_passives import resolve_mpn -from backend.pinscopex.taxonomy import SIMPLE_TYPES, type_for_ref -from backend.pinscopex.utils import safe_mpn +from backend.periscopex.parsers import parse_bom +from backend.periscopex.resolve_passives import resolve_mpn +from backend.periscopex.taxonomy import SIMPLE_TYPES, type_for_ref +from backend.periscopex.utils import safe_mpn from backend.services import projects as proj_svc from backend.services.billing_hook import get_billing from backend.services.llm.pricing import CACHE_RATES, PRICING diff --git a/backend/services/datasheet_finder.py b/backend/services/datasheet_finder.py index 5c45362..c7bff42 100644 --- a/backend/services/datasheet_finder.py +++ b/backend/services/datasheet_finder.py @@ -1,6 +1,6 @@ """Automatic datasheet lookup — LCSC, manufacturer URLs, optional DigiKey. -DeepSeek-adapted Pinscope still needs the actual PDF. The original wizard +DeepSeek-adapted Periscope still needs the actual PDF. The original wizard only auto-fetched via DigiKey, which requires paid API keys and often fails when the manufacturer CDN blocks the download. @@ -39,7 +39,7 @@ _PDF_MAGIC = b"%PDF-" _MIN_PDF_SIZE = 5_000 _UA = ( "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " - "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Pinscope/2.8" + "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Periscope/2.8" ) _LCSC_BASE = "https://wmsc.lcsc.com/ftps/wm" @@ -154,7 +154,7 @@ def find_local_pdf(pdf_dir: Path, mpn: str) -> Path | None: ``ESP32-S31-WROOM-3`` matches ``ESP32-S31-WROOM-3-N16R16V.pdf`` and the reverse — packing / flash-size suffixes, not sibling dies (CH340 vs CH340E). """ - from backend.pinscopex.utils import safe_mpn + from backend.periscopex.utils import safe_mpn if not mpn or not pdf_dir.is_dir(): return None diff --git a/backend/services/datasheet_store.py b/backend/services/datasheet_store.py index a79915c..37c3d22 100644 --- a/backend/services/datasheet_store.py +++ b/backend/services/datasheet_store.py @@ -16,7 +16,7 @@ from __future__ import annotations import hashlib from pathlib import Path -from backend.pinscopex.utils import safe_mpn +from backend.periscopex.utils import safe_mpn from backend.services.storage import StorageBackend BLOB_PREFIX = "library/datasheets/blobs/" diff --git a/backend/services/dedupe_findings.py b/backend/services/dedupe_findings.py index 2ef9630..8b1617a 100644 --- a/backend/services/dedupe_findings.py +++ b/backend/services/dedupe_findings.py @@ -25,7 +25,7 @@ import time from datetime import datetime, timezone from typing import Awaitable, Callable -from backend.pinscopex.models import Finding +from backend.periscopex.models import Finding from backend.services.api_logs import ApiLogger from backend.services.llm import Message, TextBlock from backend.services.llm.factory import call_with_fallback diff --git a/backend/services/email.py b/backend/services/email.py index 859cc1c..39ee629 100644 --- a/backend/services/email.py +++ b/backend/services/email.py @@ -174,7 +174,7 @@ def _render_report_email( @@ -292,7 +292,7 @@ def _render_pipeline_started_email(
- Pinscope + Periscope Report Ready @@ -247,7 +247,7 @@ def _render_report_email(
- Pinscope · Agentic schematic validation + Periscope · Agentic schematic validation
@@ -448,7 +448,7 @@ def _build_report_message( ) -> MIMEMultipart: """Build the report-ready email message.""" msg = MIMEMultipart("alternative") - msg["From"] = f"Pinscope <{settings.email_sender}>" + msg["From"] = f"Periscope <{settings.email_sender}>" msg["To"] = to_email msg["Subject"] = f"Report ready: {project_name}" @@ -460,7 +460,7 @@ def _build_report_message( infos = summary.get("INFO", 0) text_body = ( f"Hi {recipient_name},\n\n" - f"Your Pinscope validation report for \"{project_name}\" is ready.\n\n" + f"Your Periscope validation report for \"{project_name}\" is ready.\n\n" f"Summary: {total} findings — {errors} errors, {warnings} warnings, {infos} info\n\n" f"View the report: {report_url}\n" ) @@ -485,7 +485,7 @@ def _build_paused_message( credits_needed_low: float, ) -> MIMEMultipart: msg = MIMEMultipart("alternative") - msg["From"] = f"Pinscope <{settings.email_sender}>" + msg["From"] = f"Periscope <{settings.email_sender}>" msg["To"] = to_email msg["Subject"] = f"Paused: {project_name} is waiting for credits" @@ -495,7 +495,7 @@ def _build_paused_message( text_body = ( f"Hi {recipient_name},\n\n" - f"Your Pinscope run for \"{project_name}\" paused because you're low on credits.\n\n" + f"Your Periscope run for \"{project_name}\" paused because you're low on credits.\n\n" f"{last_line}\n{stage_line}\n\n" f"Current balance: {balance:.2f} credits\n" f"Credits needed to finish (est): {credits_needed_low:.2f}+\n\n" @@ -510,13 +510,13 @@ def _build_topup_failed_message( amount_usd: float, reason: str, ) -> MIMEMultipart: msg = MIMEMultipart("alternative") - msg["From"] = f"Pinscope <{settings.email_sender}>" + msg["From"] = f"Periscope <{settings.email_sender}>" msg["To"] = to_email - msg["Subject"] = "Pinscope: auto top-up failed" + msg["Subject"] = "Periscope: auto top-up failed" manage_url = f"{settings.email_frontend_url}/credits" text_body = ( f"Hi {recipient_name},\n\n" - f"We tried to auto top-up your Pinscope balance with " + f"We tried to auto top-up your Periscope balance with " f"${amount_usd:.2f} but the charge failed.\n\n" f"Reason: {reason}\n\n" f"Auto top-up has been disabled until you update your payment method. " @@ -549,13 +549,13 @@ def _build_low_balance_message( to_email: str, recipient_name: str, balance: float, threshold: float, ) -> MIMEMultipart: msg = MIMEMultipart("alternative") - msg["From"] = f"Pinscope <{settings.email_sender}>" + msg["From"] = f"Periscope <{settings.email_sender}>" msg["To"] = to_email - msg["Subject"] = "Pinscope: low credit balance" + msg["Subject"] = "Periscope: low credit balance" credits_url = f"{settings.email_frontend_url}/credits" text_body = ( f"Hi {recipient_name},\n\n" - f"Your Pinscope credit balance has dropped to " + f"Your Periscope credit balance has dropped to " f"{balance:.2f} credits (below your threshold of {threshold:.2f}).\n\n" f"Top up here so your pipelines don't pause mid-run: {credits_url}\n" ) @@ -691,10 +691,10 @@ async def send_test_email(to_email: str) -> dict: result["step"] = "send" msg = MIMEMultipart("alternative") - msg["From"] = f"Pinscope <{settings.email_sender}>" + msg["From"] = f"Periscope <{settings.email_sender}>" msg["To"] = to_email - msg["Subject"] = "Pinscope email test" - msg.attach(MIMEText(f"Test email from Pinscope. Sender: {settings.email_sender}. Creds: {cred_type}", "plain")) + msg["Subject"] = "Periscope email test" + msg.attach(MIMEText(f"Test email from Periscope. Sender: {settings.email_sender}. Creds: {cred_type}", "plain")) import asyncio as _asyncio raw = base64.urlsafe_b64encode(msg.as_bytes()).decode("ascii") @@ -741,7 +741,7 @@ async def send_pipeline_started_email( # Build message msg = MIMEMultipart("alternative") - msg["From"] = f"Pinscope <{settings.email_sender}>" + msg["From"] = f"Periscope <{settings.email_sender}>" msg["To"] = to_email msg["Subject"] = f"Pipeline started: {project_name} ({num_components} components)" @@ -866,7 +866,7 @@ def _render_feedback_email(
- Pinscope + Periscope Pipeline Started @@ -398,7 +398,7 @@ def _render_pipeline_started_email(
- Pinscope · Agentic schematic validation + Periscope · Agentic schematic validation
@@ -1006,7 +1006,7 @@ async def send_feedback_received_email( to_email = settings.email_admin_notify subject_ctx = project_name or "general" msg = MIMEMultipart("alternative") - msg["From"] = f"Pinscope <{settings.email_sender}>" + msg["From"] = f"Periscope <{settings.email_sender}>" msg["To"] = to_email msg["Subject"] = f"Feedback ({type_label}): {subject_ctx}" @@ -1095,7 +1095,7 @@ def _render_feedback_reply_email(
- Pinscope + Periscope Feedback Received @@ -953,7 +953,7 @@ def _render_feedback_email(
- Pinscope · Agentic schematic validation + Periscope · Agentic schematic validation
{context_line} @@ -1124,7 +1124,7 @@ def _render_feedback_reply_email( @@ -1203,7 +1203,7 @@ async def send_feedback_reply_email( finding_designator: str | None = None, finding_mpn: str | None = None, ) -> None: - """Notify the original submitter that the Pinscope team replied. Fire-and-forget.""" + """Notify the original submitter that the Periscope team replied. Fire-and-forget.""" if not settings.use_email: return @@ -1229,15 +1229,15 @@ async def send_feedback_reply_email( first_name = full_name.split()[0] if full_name else "there" msg = MIMEMultipart("alternative") - msg["From"] = f"Pinscope <{settings.email_sender}>" + msg["From"] = f"Periscope <{settings.email_sender}>" msg["To"] = to_email - msg["Subject"] = "The Pinscope team replied to your feedback" + msg["Subject"] = "The Periscope team replied to your feedback" # Plain text fallback text_lines = [ f"Hi {first_name},", "", - "The Pinscope team just replied to your feedback.", + "The Periscope team just replied to your feedback.", "", "— Reply —", reply_text, @@ -1245,10 +1245,10 @@ async def send_feedback_reply_email( "— Your original message —", original_message, "", - f"View in Pinscope: {settings.email_frontend_url}/feedback", + f"View in Periscope: {settings.email_frontend_url}/feedback", "", "Thank you so much for taking the time to share your feedback — we truly value it.", - "— The Pinscope team", + "— The Periscope team", ] msg.attach(MIMEText("\n".join(text_lines), "plain")) diff --git a/backend/services/extraction.py b/backend/services/extraction.py index 7f21ad5..0a35c1b 100644 --- a/backend/services/extraction.py +++ b/backend/services/extraction.py @@ -17,8 +17,8 @@ import tempfile import time from pathlib import Path -from backend.pinscopex.utils import safe_mpn -from backend.pinscopex.models import ( +from backend.periscopex.utils import safe_mpn +from backend.periscopex.models import ( CapacitorSpecs, ComponentConstraints, ComponentModel, @@ -27,7 +27,7 @@ from backend.pinscopex.models import ( NetType, SimpleComponentSpecs, ) -from backend.pinscopex.taxonomy import ( +from backend.periscopex.taxonomy import ( TAXONOMY_DIR, add_subtype, format_for_prompt, @@ -399,13 +399,13 @@ def _coerce_abs_max(raw: object) -> list[dict]: def _coerce_layout_rules(raw: object) -> list[dict]: - from backend.pinscopex.layout_rules import validate_layout_rules + from backend.periscopex.layout_rules import validate_layout_rules rows, _errors = validate_layout_rules(raw if isinstance(raw, list) else []) return rows def _coerce_internal_features(raw: object): - from backend.pinscopex.models import InternalFeatures + from backend.periscopex.models import InternalFeatures if not isinstance(raw, dict): return None try: @@ -1088,7 +1088,7 @@ async def auto_resolve_specs( # Convert passive SimpleComponentSpecs to typed models if component_type == "passive": - from backend.pinscopex.resolve_passives import simple_to_typed_passive_specs + from backend.periscopex.resolve_passives import simple_to_typed_passive_specs typed = simple_to_typed_passive_specs(specs) return ComponentModel(mpn=mpn, specs=typed) @@ -1260,7 +1260,7 @@ async def resolve_from_value( ) if component_type == "passive": - from backend.pinscopex.resolve_passives import simple_to_typed_passive_specs + from backend.periscopex.resolve_passives import simple_to_typed_passive_specs typed = simple_to_typed_passive_specs(specs) return ComponentModel(mpn=mpn, specs=typed) diff --git a/backend/services/llm/local_skill.py b/backend/services/llm/local_skill.py index 338cb17..0a4cffb 100644 --- a/backend/services/llm/local_skill.py +++ b/backend/services/llm/local_skill.py @@ -62,7 +62,7 @@ def load_skill_validator(skill_name: str): if not path.is_file(): return None spec = importlib.util.spec_from_file_location( - f"pinscope_skill_{skill_name.replace('-', '_')}_validate", path, + f"periscope_skill_{skill_name.replace('-', '_')}_validate", path, ) if spec is None or spec.loader is None: return None diff --git a/backend/services/llm/pdf_ingest.py b/backend/services/llm/pdf_ingest.py index 60571c6..934c506 100644 --- a/backend/services/llm/pdf_ingest.py +++ b/backend/services/llm/pdf_ingest.py @@ -15,7 +15,7 @@ import logging import re from pathlib import Path -from backend.pinscopex.pdf_text import ( +from backend.periscopex.pdf_text import ( extract_pdf_document_text, fitz_page_text, page_is_sparse, diff --git a/backend/services/local_jwt.py b/backend/services/local_jwt.py index 90cf8f4..aff8b9b 100644 --- a/backend/services/local_jwt.py +++ b/backend/services/local_jwt.py @@ -1,4 +1,4 @@ -"""Pinscope local JWT helpers (HS256).""" +"""Periscope local JWT helpers (HS256).""" from __future__ import annotations @@ -21,7 +21,7 @@ def issue_token(user_id: str, email: str) -> str: payload = { "sub": user_id, "email": email, - "iss": "pinscope-local", + "iss": "periscope-local", "iat": now, "exp": now + timedelta(days=TOKEN_TTL_DAYS), } @@ -37,7 +37,7 @@ def decode_token(token: str) -> dict[str, Any] | None: token, secret, algorithms=[ALGORITHM], - issuer="pinscope-local", + issuer="periscope-local", options={"verify_aud": False}, leeway=10, ) diff --git a/backend/services/local_users.py b/backend/services/local_users.py index 8380490..e835ca6 100644 --- a/backend/services/local_users.py +++ b/backend/services/local_users.py @@ -1,4 +1,4 @@ -"""Local Pinscope user directory (self-host auth, no Clerk). +"""Local Periscope user directory (self-host auth, no Clerk). Users live under ``data/auth/users/{user_id}.json`` with an email index. Passwords use stdlib ``hashlib.scrypt``. diff --git a/backend/services/normalize_findings.py b/backend/services/normalize_findings.py index e481ea6..d909635 100644 --- a/backend/services/normalize_findings.py +++ b/backend/services/normalize_findings.py @@ -23,7 +23,7 @@ from datetime import datetime, timezone from typing import Awaitable, Callable from backend.config import settings -from backend.pinscopex.models import Finding +from backend.periscopex.models import Finding from backend.services.api_logs import ApiLogger from backend.services.llm import Message, TextBlock from backend.services.llm.factory import call_with_fallback diff --git a/backend/services/passive_from_distributor.py b/backend/services/passive_from_distributor.py index cdd6fca..c102b6c 100644 --- a/backend/services/passive_from_distributor.py +++ b/backend/services/passive_from_distributor.py @@ -8,8 +8,8 @@ from __future__ import annotations import re -from backend.pinscopex.models import ComponentModel, SimpleComponentSpecs -from backend.pinscopex.resolve_passives import simple_to_typed_passive_specs +from backend.periscopex.models import ComponentModel, SimpleComponentSpecs +from backend.periscopex.resolve_passives import simple_to_typed_passive_specs _CAP = re.compile( r"(?P\d+(?:\.\d+)?)\s*(?P[pnuμµmk])?\s*[fF]\b", diff --git a/backend/services/passive_from_mpn.py b/backend/services/passive_from_mpn.py index b210b19..5d7d1e4 100644 --- a/backend/services/passive_from_mpn.py +++ b/backend/services/passive_from_mpn.py @@ -9,8 +9,8 @@ from __future__ import annotations import re -from backend.pinscopex.models import ComponentModel, SimpleComponentSpecs -from backend.pinscopex.resolve_passives import simple_to_typed_passive_specs +from backend.periscopex.models import ComponentModel, SimpleComponentSpecs +from backend.periscopex.resolve_passives import simple_to_typed_passive_specs from backend.services.passive_from_distributor import _spice _SIZE = r"(?:0201|0402|0603|0805|1206|1210|1812|2010|2512)" diff --git a/backend/services/passive_from_value.py b/backend/services/passive_from_value.py index 7b1ba21..cb839cd 100644 --- a/backend/services/passive_from_value.py +++ b/backend/services/passive_from_value.py @@ -4,8 +4,8 @@ from __future__ import annotations import re -from backend.pinscopex.models import ComponentModel, SimpleComponentSpecs -from backend.pinscopex.resolve_passives import simple_to_typed_passive_specs +from backend.periscopex.models import ComponentModel, SimpleComponentSpecs +from backend.periscopex.resolve_passives import simple_to_typed_passive_specs from backend.services.passive_from_distributor import _spice _PLACEHOLDER = re.compile( diff --git a/backend/services/pipeline.py b/backend/services/pipeline.py index eb2f962..af554b9 100644 --- a/backend/services/pipeline.py +++ b/backend/services/pipeline.py @@ -33,15 +33,15 @@ from pathlib import Path from typing import Any, Awaitable, Callable -from backend.pinscopex.models import ComponentType -from backend.pinscopex.utils import natural_sort_key, safe_mpn -from backend.pinscopex.bom_summary import build_bom_summary -from backend.pinscopex.derating import build_derating_table -from backend.pinscopex.validate import _load_datasheets -from backend.pinscopex.graph import build_graph -from backend.pinscopex.parsers import parse_bom, parse_netlist_any -from backend.pinscopex.resolve_passives import SkippedItem, load_patterns, resolve_mpn -from backend.pinscopex.taxonomy import SIMPLE_TYPES, type_for_ref +from backend.periscopex.models import ComponentType +from backend.periscopex.utils import natural_sort_key, safe_mpn +from backend.periscopex.bom_summary import build_bom_summary +from backend.periscopex.derating import build_derating_table +from backend.periscopex.validate import _load_datasheets +from backend.periscopex.graph import build_graph +from backend.periscopex.parsers import parse_bom, parse_netlist_any +from backend.periscopex.resolve_passives import SkippedItem, load_patterns, resolve_mpn +from backend.periscopex.taxonomy import SIMPLE_TYPES, type_for_ref from backend.config import settings from backend.services import admin_settings as settings_svc @@ -196,7 +196,7 @@ def _cancel_gate_check(ctx: PipelineContext) -> None: class PipelineWorkspace: """Downloads project files from storage to a temp dir for pipeline execution. - The pinscopex core library operates on local paths. This context manager + The periscopex core library operates on local paths. This context manager downloads inputs at enter, provides local paths, and uploads results at exit. """ @@ -264,7 +264,7 @@ class PipelineWorkspace: self._upload_file("bom_summary.json") self._upload_file("derating.json") self._upload_file("report.json") - self._upload_file("pinscope-findings.json") + self._upload_file("periscope-findings.json") self._upload_file("review_fingerprints.json") self._upload_file("api_logs.jsonl") @@ -763,7 +763,7 @@ async def _stage_ic_extraction(ctx: PipelineContext) -> None: # Pre-categorize: workspace cache, library cache, or needs extraction from backend.config import settings as app_settings - from backend.pinscopex.layout_rules import needs_layout_rules_refresh + from backend.periscopex.layout_rules import needs_layout_rules_refresh layout_scan_ver = app_settings.get_default_model_version() _ic_cache: dict[str, tuple] = {} @@ -863,7 +863,7 @@ async def _stage_ic_extraction(ctx: PipelineContext) -> None: extracted_key = f"{ctx.ws.prefix}/extracted/{safe}.json" ctx.storage.upload_from_local(json_path, extracted_key) try: - from backend.pinscopex.library_gate import should_promote_extraction + from backend.periscopex.library_gate import should_promote_extraction payload = json.loads(json_path.read_text(encoding="utf-8")) ok, reason = should_promote_extraction(payload) @@ -1545,7 +1545,7 @@ def _write_layout_graph(ws: PipelineWorkspace, project_id: str) -> None: if not pcb.is_file(): return try: - from backend.pinscopex.parsers_kicad_pcb import parse_kicad_pcb + from backend.periscopex.parsers_kicad_pcb import parse_kicad_pcb layout = parse_kicad_pcb(pcb) out = ws.local_path("layout_graph.json") @@ -1561,8 +1561,8 @@ def _write_layout_graph(ws: PipelineWorkspace, project_id: str) -> None: def _write_functional_groups(ws: PipelineWorkspace, graph) -> None: """Layout F1: topology domains/groups (no mm). Fail-soft.""" try: - from backend.pinscopex.functional_groups import build_functional_groups - from backend.pinscopex.validate import _build_constraints_map, _load_datasheets + from backend.periscopex.functional_groups import build_functional_groups + from backend.periscopex.validate import _build_constraints_map, _load_datasheets extracted_dir = ws.local_path("extracted") cmap = {} @@ -1584,8 +1584,8 @@ def _write_impedance_nets(ws: PipelineWorkspace, graph) -> None: if not path.is_file(): return try: - from backend.pinscopex.impedance_traces import analyze_where_needed - from backend.pinscopex.models import LayoutGraph + from backend.periscopex.impedance_traces import analyze_where_needed + from backend.periscopex.models import LayoutGraph layout = LayoutGraph.model_validate_json(path.read_text()) report = analyze_where_needed(layout, graph) @@ -1792,11 +1792,11 @@ async def _stage_validation(ctx: PipelineContext) -> None: fp_path = ctx.ws.local_path("review_fingerprints.json") current_fp: dict[str, str] = {} try: - from backend.pinscopex.review_fingerprint import ( + from backend.periscopex.review_fingerprint import ( graph_ic_fingerprints, skip_unchanged_ics, ) - from backend.pinscopex.validate import _build_constraints_map, _load_datasheets + from backend.periscopex.validate import _build_constraints_map, _load_datasheets cmap = _build_constraints_map(_load_datasheets(extracted_dir)) current_fp = graph_ic_fingerprints(ctx.graph, cmap) diff --git a/backend/services/placement_pipeline.py b/backend/services/placement_pipeline.py index 55f6a3a..26f3f81 100644 --- a/backend/services/placement_pipeline.py +++ b/backend/services/placement_pipeline.py @@ -11,10 +11,10 @@ from __future__ import annotations import logging from pathlib import Path -from backend.pinscopex.functional_groups import build_placement_plan -from backend.pinscopex.graph import build_graph -from backend.pinscopex.models import ComponentConstraints, DesignGraph, LayoutGraph -from backend.pinscopex.placement_pack import build_placement_pack +from backend.periscopex.functional_groups import build_placement_plan +from backend.periscopex.graph import build_graph +from backend.periscopex.models import ComponentConstraints, DesignGraph, LayoutGraph +from backend.periscopex.placement_pack import build_placement_pack from backend.services import projects as proj_svc from backend.services.pipeline import PipelineWorkspace, broker from backend.services.storage import StorageBackend @@ -218,7 +218,7 @@ def _load_layout(ws: PipelineWorkspace) -> LayoutGraph | None: if not pcb.is_file(): return None try: - from backend.pinscopex.parsers_kicad_pcb import parse_kicad_pcb + from backend.periscopex.parsers_kicad_pcb import parse_kicad_pcb layout = parse_kicad_pcb(pcb) cached.write_text(layout.model_dump_json(indent=2) + "\n") diff --git a/backend/services/projects.py b/backend/services/projects.py index 35ca564..899609d 100644 --- a/backend/services/projects.py +++ b/backend/services/projects.py @@ -11,7 +11,7 @@ Each project lives at users/{user_id}/projects/{id}/ with: models/ — cached component specs design_graph.json — graph output report.json — validation report - pinscope-findings.json — KiCad cad-bridge (plugin pan-and-zoom) + periscope-findings.json — KiCad cad-bridge (plugin pan-and-zoom) Library (global, shared across users): library/extracted/{mpn}.json @@ -31,9 +31,9 @@ from typing import Any log = logging.getLogger(__name__) -from pydantic import BaseModel +from pydantic import AliasChoices, BaseModel, Field -from backend.pinscopex.utils import safe_mpn +from backend.periscopex.utils import safe_mpn from backend.services.storage import StaleGeneration, StorageBackend @@ -120,9 +120,13 @@ class ProjectMeta(BaseModel): pause_reason: str | None = None completed_review_refs: list[str] = [] # IC refs already reviewed (persists across pauses) - # Pinscope app version that generated the project's report. + # Periscope app version that generated the project's report. # Stamped on the first /start transition and preserved thereafter. - pinscope_version: str | None = None + # Accept legacy pinscope_version from project.json written before the rebrand. + periscope_version: str | None = Field( + default=None, + validation_alias=AliasChoices("periscope_version", "pinscope_version"), + ) # Worker bookkeeping (set by the API on enqueue, read by /events SSE # and by the stale-running sweeper). @@ -166,7 +170,7 @@ def completed_review_refs_for_retry( for ref in (report.get("review_errors") or {}): if ref: failed.add(str(ref)) - from backend.pinscopex.utils import natural_sort_key + from backend.periscopex.utils import natural_sort_key kept = [r for r in (meta.completed_review_refs or []) if r and r not in failed] return sorted(kept, key=natural_sort_key) @@ -484,7 +488,7 @@ def clear_project_extractions( "bom_summary.json", "derating.json", "report.json", - "pinscope-findings.json", + "periscope-findings.json", "review_fingerprints.json", "api_logs.jsonl", "graph_voltage_updates.json", @@ -519,7 +523,7 @@ def reopen_project( "bom_summary.json", "derating.json", "report.json", - "pinscope-findings.json", + "periscope-findings.json", "review_fingerprints.json", "api_logs.jsonl", "graph_voltage_updates.json", @@ -936,7 +940,7 @@ def library_has_datasheet( return key # 3. Pattern-based fallback for passives if patterns: - from backend.pinscopex.resolve_passives import resolve_mpn + from backend.periscopex.resolve_passives import resolve_mpn match = resolve_mpn(mpn, patterns) if match is not None: @@ -1121,11 +1125,11 @@ def list_library_patterns(storage: StorageBackend) -> list[str]: def load_library_patterns(storage: StorageBackend): """Load and parse all passive patterns from the library. - For local backend, delegates to pinscopex. For GCS, downloads to temp first. + For local backend, delegates to periscopex. For GCS, downloads to temp first. This function is only used by the library/check endpoint — during pipeline execution, patterns are loaded from the workspace temp directory. """ - from backend.pinscopex.resolve_passives import load_patterns + from backend.periscopex.resolve_passives import load_patterns from backend.services.storage import LocalStorageBackend diff --git a/backend/services/validation.py b/backend/services/validation.py index cbe25ce..0866273 100644 --- a/backend/services/validation.py +++ b/backend/services/validation.py @@ -21,7 +21,7 @@ from typing import Awaitable, Callable log = logging.getLogger(__name__) -from backend.pinscopex.models import ( +from backend.periscopex.models import ( ComponentConstraints, ComponentType, DesignGraph, @@ -30,7 +30,7 @@ from backend.pinscopex.models import ( NetType, ValidationReport, ) -from backend.pinscopex.validate import ( +from backend.periscopex.validate import ( SYSTEM_PROMPT, _MAX_REVIEW_TURNS, ReviewResult, @@ -41,30 +41,30 @@ from backend.pinscopex.validate import ( build_component_context, _parse_review, ) -from backend.pinscopex.quote_verify import verify_finding_citations -from backend.pinscopex.utils import safe_mpn -from backend.pinscopex.pin_mux_check import check_pin_mux_feasibility -from backend.pinscopex.led_current_check import check_led_current -from backend.pinscopex.passive_rail_check import ( +from backend.periscopex.quote_verify import verify_finding_citations +from backend.periscopex.utils import safe_mpn +from backend.periscopex.pin_mux_check import check_pin_mux_feasibility +from backend.periscopex.led_current_check import check_led_current +from backend.periscopex.passive_rail_check import ( check_i2c_pullups, check_reset_pullups, check_supply_decoupling, ) -from backend.pinscopex.bom_match_check import check_bom_schematic_match -from backend.pinscopex.hf_coverage_check import check_hf_decoupling_coverage -from backend.pinscopex.cad_bridge import annotate_findings_cad, build_cad_bridge, write_cad_bridge -from backend.pinscopex.filter_check import check_filters -from backend.pinscopex.thermal_check import check_thermal -from backend.pinscopex.power_margin_check import check_power_margin -from backend.pinscopex.sequencing_check import check_power_sequencing -from backend.pinscopex.dnp_check import check_dnp_enables -from backend.pinscopex.lifecycle import check_lifecycle, load_lifecycle_dir -from backend.pinscopex.errata_check import check_errata -from backend.pinscopex.internal_features_check import check_internal_features -from backend.pinscopex.placement_check import check_placement -from backend.pinscopex.si_check import check_si -from backend.pinscopex.crystal_cl_check import check_crystal_cl -from backend.pinscopex.nc_pin_check import check_nc_pins +from backend.periscopex.bom_match_check import check_bom_schematic_match +from backend.periscopex.hf_coverage_check import check_hf_decoupling_coverage +from backend.periscopex.cad_bridge import annotate_findings_cad, build_cad_bridge, write_cad_bridge +from backend.periscopex.filter_check import check_filters +from backend.periscopex.thermal_check import check_thermal +from backend.periscopex.power_margin_check import check_power_margin +from backend.periscopex.sequencing_check import check_power_sequencing +from backend.periscopex.dnp_check import check_dnp_enables +from backend.periscopex.lifecycle import check_lifecycle, load_lifecycle_dir +from backend.periscopex.errata_check import check_errata +from backend.periscopex.internal_features_check import check_internal_features +from backend.periscopex.placement_check import check_placement +from backend.periscopex.si_check import check_si +from backend.periscopex.crystal_cl_check import check_crystal_cl +from backend.periscopex.nc_pin_check import check_nc_pins TRACE_VERSION = 1 @@ -139,14 +139,14 @@ def _assistant_text(blocks) -> str: except Exception: log.exception("trace: assistant_text extraction failed") return "\n".join(parts) -from backend.pinscopex.validation_tools import ( +from backend.periscopex.validation_tools import ( ALL_TOOLS, SUBMIT_REVIEW_SCHEMA, ConstraintsMap, ExcerptState, execute_tool, ) -from backend.pinscopex.utils import safe_mpn +from backend.periscopex.utils import safe_mpn from backend.config import settings from backend.services.api_logs import ApiLogger @@ -639,7 +639,7 @@ def _find_pdf( then tries to download from the library. """ from backend.services.datasheet_finder import find_local_pdf - from backend.pinscopex.utils import safe_mpn as _safe + from backend.periscopex.utils import safe_mpn as _safe mpn = (mpn or "").strip() if not mpn: @@ -836,7 +836,7 @@ async def validate_design_async( try: prefix_id = (project_prefix or "").rstrip("/").rsplit("/", 1)[-1] bridge = build_cad_bridge(report, prefix_id or report.project) - write_cad_bridge(existing_path.with_name("pinscope-findings.json"), bridge) + write_cad_bridge(existing_path.with_name("periscope-findings.json"), bridge) except Exception: log.exception("cad bridge write failed") return report diff --git a/docker-compose.yml b/docker-compose.yml index 21fade5..2c6de20 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: context: . dockerfile: backend/Dockerfile - container_name: pinscope-backend + container_name: periscope-backend restart: unless-stopped env_file: @@ -24,7 +24,7 @@ services: - "8080:8080" networks: - - pinscope + - periscope frontend: build: @@ -34,7 +34,7 @@ services: NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:8080} NEXT_PUBLIC_AUTH_MODE: ${NEXT_PUBLIC_AUTH_MODE:-} - container_name: pinscope-frontend + container_name: periscope-frontend restart: unless-stopped depends_on: @@ -44,9 +44,9 @@ services: - "3000:3000" networks: - - pinscope + - periscope networks: - pinscope: + periscope: driver: bridge diff --git a/docs/piano-implementazione.md b/docs/piano-implementazione.md index 8b286d2..9615dd3 100644 --- a/docs/piano-implementazione.md +++ b/docs/piano-implementazione.md @@ -1,8 +1,8 @@ -# Piano di implementazione — Pinscope +# Piano di implementazione — Periscope Documento di lavoro **prima dello sviluppo**. La lista dell’utente è il minimo; sotto c’è anche ciò che serve perché quella lista non resti un insieme di moduli scollegati. -**Questo piano copre due prodotti.** Pinscope originale resta il primo. Layout/plugin/placement mm sono il secondo. Non mescolare i changelog né vendere il secondo come “un po’ di Pinscope in più”. +**Questo piano copre due prodotti.** Periscope originale resta il primo. Layout/plugin/placement mm sono il secondo. Non mescolare i changelog né vendere il secondo come “un po’ di Periscope in più”. Stato del codice di riferimento: branch `cursor/deepseek-71c5` (post DeepSeek V4.1, costi USD, replace BOM/netlist, parser KiCad, fingerprint review, auth multi-utente, PCB pad nets). @@ -10,7 +10,7 @@ Stato del codice di riferimento: branch `cursor/deepseek-71c5` (post DeepSeek V4 ## 0b. Roadmap DeepSeek / crescita (integrata) -Fonte originale: canvas *Pinscope: crescita e DeepSeek*. Qui lo stato operativo. +Fonte originale: canvas *Periscope: crescita e DeepSeek*. Qui lo stato operativo. | Fase | Voce | Stato | | --- | --- | --- | @@ -48,27 +48,27 @@ Obiettivo unico: **routing migliore** (loop corti, meno crossing, canali liberi) 1. **F1 polish (ora)** — satelliti classificatì; `other` nascosti; Domains/Rails = primary rail. 2. **C4 fill** — riestrazione IC → `layout_rules` con `max_distance_mm` dove il PDF lo dice (skill 1.10.0). -3. **PCB gate** — upload `.kicad_pcb` → `layout_graph.json` (footprint xy già usati da PS-PLC*). +3. **PCB gate** — upload `.kicad_pcb` → `layout_graph.json` (footprint xy già usati da PE-PLC*). 4. **F2 pack v1** — per ogni `decoupling_proximity` numerica: proporre xy satellite entro `max_distance_mm` dal pad (già skeleton); UI Pack lista proposte. 5. **F2 pack v2** — collisioni courtyard, stesso layer, ordine `assemble_order` per dominio, ancore IC fissi se già piazzati. 6. **F2 export** — scrivere posizioni proposte in file/plugin (pcbnew) senza muovere rame; `placement_check` resta verifica. -Output F1: `functional_groups.json` scritto in `graph_build`. Pipeline parallela Placement (`POST …/placement/start`) riscrive anche `placement_plan.json` senza toccare lo `status` di analisi, poi tenta F2 pack. Verifica PCB esistente resta `placement_check` (PS-PLC*) — non confondere con packing. +Output F1: `functional_groups.json` scritto in `graph_build`. Pipeline parallela Placement (`POST …/placement/start`) riscrive anche `placement_plan.json` senza toccare lo `status` di analisi, poi tenta F2 pack. Verifica PCB esistente resta `placement_check` (PE-PLC*) — non confondere con packing. --- ## 0. Due prodotti (stesso repo, due promesse) -| | **Pinscope** (oggi + wave A–B, C schema, F/H leggere) | **Pinscope Layout** (wave D parziale, G, C4+G2, plugin pcbnew) | +| | **Periscope** (oggi + wave A–B, C schema, F/H leggere) | **Periscope Layout** (wave D parziale, G, C4+G2, plugin pcbnew) | | --- | --- | --- | | Promessa | Lo schema rispetta il datasheet | Il rame rispetta datasheet + geometria | | File | BOM, netlist, `.kicad_sch` gerarchico | + `.kicad_pcb` | | Output | Finding su pin/net, derating, power tree | Distanze mm, 3W, creepage, skew, via EP | | Utente | Chi chiude lo schema | Chi sbroglia | -Farli nello stesso codebase (`pinscopex` + `LayoutGraph`) è ragionevole. Farli **nella stessa run obbligatoria** no: senza PCB il progetto deve restare un Pinscope completo, non “incompleto perché manca il gerber”. +Farli nello stesso codebase (`periscopex` + `LayoutGraph`) è ragionevole. Farli **nella stessa run obbligatoria** no: senza PCB il progetto deve restare un Periscope completo, non “incompleto perché manca il gerber”. -Nome in UI: tab **Layout** o prodotto “Layout checks” gated dal file `.kicad_pcb`. Il report schema non deve riempirsi di `PS-PLC` se il PCB non c’è. +Nome in UI: tab **Layout** o prodotto “Layout checks” gated dal file `.kicad_pcb`. Il report schema non deve riempirsi di `PE-PLC` se il PCB non c’è. Non serve un fork oggi. Serve disciplina: ogni PR dichiara se è Core o Layout. @@ -76,7 +76,7 @@ Non serve un fork oggi. Serve disciplina: ogni PR dichiara se è Core o Layout. ## 0. Contratto di prodotto (non negoziabile) -Pinscope oggi è un **validatore di schema**: BOM + netlist → grafo bipartito → check deterministici + review LLM con citazione datasheet. Non legge il PCB. +Periscope oggi è un **validatore di schema**: BOM + netlist → grafo bipartito → check deterministici + review LLM con citazione datasheet. Non legge il PCB. Molti punti della lista (larghezza traccia, 3W, creepage, CPW clearance, length matching) **non esistono senza geometria**. Il piano li tiene, ma li mette **dopo** un ingest layout. Se li si forza sullo schema si producono finding inventati. @@ -118,7 +118,7 @@ Da fare (Wave A1), in ordine: Regole: -1. Ogni nuovo check è una funzione pura in `backend/pinscopex/` che legge `DesignGraph` (+ opzionale layout). Niente SDK LLM dentro `pinscopex/`. +1. Ogni nuovo check è una funzione pura in `backend/periscopex/` che legge `DesignGraph` (+ opzionale layout). Niente SDK LLM dentro `periscopex/`. 2. I finding usano lo stesso schema (`Finding` in `models.py` / `frontend/src/lib/types.ts`). Campo `source` già distingue check automatici vs review. 3. Finding normalization resta **downgrade-only**. 4. Libreria condivisa: MPN exact-match. Niente fuzzy sul die. @@ -134,14 +134,14 @@ Aggiungere (backward compatible): | --- | --- | | `net` | Telemetry CAD, filtri, SI | | `pins[]` | Pan-and-zoom su U1.4 | -| `rule_id` | Plugin DRC (`PS-DEC-001`) | +| `rule_id` | Plugin DRC (`PE-DEC-001`) | | `cad_sheet` / `cad_uuid` | Sync plugin KiCad | | `variant` | DNP / ECO | | `severity_calibrated` | già implicito; non alzare in post | Passi: -1. Estendere `Finding` in `backend/pinscopex/models.py` e `frontend/src/lib/types.ts`. +1. Estendere `Finding` in `backend/periscopex/models.py` e `frontend/src/lib/types.ts`. 2. Aggiornare `assign_finding_ids`, export Excel, report UI (campi opzionali nascosti se null). 3. Test su `simple_project/` che i check esistenti ancora serializzano. @@ -157,12 +157,12 @@ Passi: | 2 Datasheet / errata / OCR blocchi | Pintable, excerpt, quote_verify, errata, `internal_features` | Nessun RAG vendor | **OK** | | 3 Impedenze / stackup | ImpedenceFinder: calcolatrice + **Z0 sulle tracce** dei net signal (stackup PCB) | CPWG non nel vendor | **OK** | | 4 Filtri | `check_filters` (solo con poli/numeri in specs) | Niente \(f_c\) inventata | **OK** | -| 5 Capacità PI | Decoupling sulla net; derating V; DC-bias/ESR se c’è il numero; mm sul PCB (`PS-PLC-001`) | — | **OK** | +| 5 Capacità PI | Decoupling sulla net; derating V; DC-bias/ESR se c’è il numero; mm sul PCB (`PE-PLC-001`) | — | **OK** | | 6 Elettrico | Pin mux; I2C/reset pull-up; LED; sequencing/IR/power margin se c’è il parametro | Senza numero in specs → skip | **OK** | | 7 RF | Tab **RF / Impedance**: verify matching + template geometry (IFA/meander/stub → SVG + `.kicad_mod`); Z0 feed se PCB | CPWG / auto-place into `.kicad_pcb` dopo | **Improved** | | 8 HV / isolation | — | Serve `layout_rules` + V/mm dal datasheet, non IEC inventato | — | | 9 Termico | `check_thermal` se θJA/I sono in specs; via courtyard vs `min_via_count` | Niente \(T_j\) senza parametro | **OK** | -| 10 SI / DNP | DNP enable; `PS-SI-001` solo con `length_match` mm | Niente 3W/crosstalk inventati | **OK** | +| 10 SI / DNP | DNP enable; `PE-SI-001` solo con `length_match` mm | Niente 3W/crosstalk inventati | **OK** | | 11 Lifecycle | `lifecycle_check` su cache distributore (EOL/NRND/RoHS esplicito) | Niente equivalente LLM | **OK** | | 13 Placement da datasheet | `layout_rules` + `.kicad_pcb`: PLC-001…004, same_layer, crystal, keepout, path | 3W/creepage/CPW/isolation senza numero | **OK** | @@ -176,7 +176,7 @@ Senza questi i moduli 1–12 non si misurano e i plugin mentono. **E1. KiCad gerarchico GA.** Obbligatorio. Vedi “Schema gerarchico” sopra. Senza flatten dei fogli il plugin e il `.kicad_pcb` non allineano i net. -**E2. Protocollo plugin KiCad** (`pinscope-cad-bridge` JSON). Un file per progetto: +**E2. Protocollo plugin KiCad** (`periscope-cad-bridge` JSON). Un file per progetto: ```json { @@ -184,14 +184,14 @@ Senza questi i moduli 1–12 non si misurano e i plugin mentono. "project_id": "...", "findings": [ { - "rule_id": "PS-MUX-001", + "rule_id": "PE-MUX-001", "ref": "U3", "pins": ["12"], "sheet": "...", "uuid": "...", "severity": "error", "message": "...", - "url": "https://pinscope.../report?finding=U3-001" + "url": "https://periscope.../report?finding=U3-001" } ] } @@ -219,7 +219,7 @@ Passi: quelli in “Schema gerarchico (più file)” + file-guide riscritta (nie Passi: 1. Pacchetto `plugins/kicad/` (action plugin Python, KiCad 9/10). -2. `pinscope-findings.json` dal report (E2). +2. `periscope-findings.json` dal report (E2). 3. Marcatori / focus `uuid` su **eeschema** (foglio figlio corretto) e, per finding layout, su **pcbnew**. 4. Pan-and-zoom: `FocusOnItem` / select symbol; se l’API 10 differisce, adapter sottile. @@ -235,7 +235,7 @@ Passi: 1. Da campi KiCad (`MPN`, `mpn`, `PN`, `lcsc`) già letti in `parsers_kicad.py` / `graph.py`. 2. Tabella conflitti: ref in schema senza MPN, MPN in BOM senza ref, mismatch Value. -3. Finding `source=bom_match` con `rule_id=PS-BOM-001`. +3. Finding `source=bom_match` con `rule_id=PE-BOM-001`. 4. UI wizard: riga rossa nel matching, non solo colonne. **Done when:** U1 in schema e U1 in BOM con MPN diversi → ERROR citabile. @@ -244,7 +244,7 @@ Passi: ## Wave B — Check deterministici schema (sblocca 4, 5, 6, 9, 10 DNP) -Obiettivo: meno LLM, più numeri. Ogni item = modulo `pinscopex` + test su grafo sintetico + riga in eval. +Obiettivo: meno LLM, più numeri. Ogni item = modulo `periscopex` + test su grafo sintetico + riga in eval. ### B1. Power tree & drop (6) @@ -253,7 +253,7 @@ Passi: 1. Riuse UI power tree esistente. 2. Per ogni IC: somma IQ + load stimato da specs se c’è; confronta con `Iout_max` LDO/Buck se estratto. 3. IR drop **solo se** esiste Rseries esplicito (shunt/ferrite) — niente stima di pista. -4. Finding `PS-PWR-001` margin fail. +4. Finding `PE-PWR-001` margin fail. ### B2. Sequencing (6) @@ -343,7 +343,7 @@ Passi: 1. Non scraping indiscriminato (TOS, HTML instabile). 2. Catalogo URL noti (TI `lit/er`, STM `errata`, Microchip). 3. DeepSeek `web_search` **opzionale** gated, citazione obbligatoria, stesso `quote_verify`. -4. Finding `PS-ERRATA-001` se il workaround (pull-up, bond-out) non è nello schema. +4. Finding `PE-ERRATA-001` se il workaround (pull-up, bond-out) non è nello schema. **Done when:** un MPN con errata nota in fixture produce finding; vendor senza URL → skip silenzioso loggato. @@ -407,7 +407,7 @@ Motore **standalone**, stile calcolatrice. I vincoli CAD sono export, non verit Passi: -1. `pinscopex/impedance.py`: microstrip, stripline, coupled diff, CPW — formule documentate + test numerici vs 3 valori ImpedanceFinder. +1. `periscopex/impedance.py`: microstrip, stripline, coupled diff, CPW — formule documentate + test numerici vs 3 valori ImpedanceFinder. 2. Input: `h`, `er`, `t`, `w`, `s`, `target_z`. 3. UI tab progetto “Impedance” (non LLM). @@ -461,7 +461,7 @@ Passi: Passi: -1. Length matching / intra-pair skew vs limite datasheet (USB/HDMI/PCIe). **OK** (`PS-SI-001`, solo `length_match` mm). +1. Length matching / intra-pair skew vs limite datasheet (USB/HDMI/PCIe). **OK** (`PE-SI-001`, solo `length_match` mm). 2. 3W: distanza centro-centro vs W aggressore. — skip senza numero (non IEC/USB folklore). 3. Creepage/clearance: profilo IEC 62368 (pollution, RMS V dai net). — skip senza V/mm nel datasheet. 4. Isolation barrier: bbox isolator + divieto piste LV nel courtyard HV. — skip senza regola. @@ -477,17 +477,17 @@ Passi: 1. Per ogni pin alimentazione del pintable: footprint pad xy sul `.kicad_pcb`; condensatori sul medesimo net (grafo); distanza euclidea pad-cap (pin cap verso GND/VDD). **OK** 2. Se `max_distance_mm` estratto: ERROR/WARNING se oltre. Se assente: skip (niente default 3 mm). **OK** -3. Via in pad / via sotto EP: contare via nel courtyard del thermal pad vs `min_via_count`. **OK** (`PS-PLC-002`) -4. Stesso layer: se `same_layer: true` e il cap è sull’altro lato senza via sotto il pin → WARNING. **OK** (`PS-PLC-003`) +3. Via in pad / via sotto EP: contare via nel courtyard del thermal pad vs `min_via_count`. **OK** (`PE-PLC-002`) +4. Stesso layer: se `same_layer: true` e il cap è sull’altro lato senza via sotto il pin → WARNING. **OK** (`PE-PLC-003`) 5. Piste: lunghezza net dal pin al cap = shortest path sui segmenti vs `max_distance_mm`. **OK** 6. Crystal: cap load vs pin XIN/XOUT (stessa metrica). **OK** (`X1`/`C9`/`C10`) -7. Finding `PS-PLC-001`…`004` con `pins`, `net`; plugin focus pcbnew. **OK** (keepout = `PS-PLC-004`) +7. Finding `PE-PLC-001`…`004` con `pins`, `net`; plugin focus pcbnew. **OK** (keepout = `PE-PLC-004`) Non confrontare una foto del layout TI con il board pixel-a-pixel. Solo vincoli numerici/topologici. -**Done when:** fixture PCB con C di decoupling a 15 mm da VDD (regola 2 mm) → `PS-PLC-001`; cap a 1 mm → niente finding. +**Done when:** fixture PCB con C di decoupling a 15 mm da VDD (regola 2 mm) → `PE-PLC-001`; cap a 1 mm → niente finding. -**Done when (G1+G2):** un `.kicad_pcb` di test (USB diff pair volutamente sbagliata) produce `PS-SI-001`. I net coincidono con lo schema gerarchico della stessa repo. +**Done when (G1+G2):** un `.kicad_pcb` di test (USB diff pair volutamente sbagliata) produce `PE-SI-001`. I net coincidono con lo schema gerarchico della stessa repo. --- @@ -529,7 +529,7 @@ Stima onesta: Wave A–B (schema) sono il ritorno; G è un secondo prodotto. Non ## Passi operativi per **ogni** check nuovo 1. Fixture grafo minimo in `tests/test_.py` (non solo `simple_project`). -2. Funzione in `pinscopex/` senza I/O. +2. Funzione in `periscopex/` senza I/O. 3. Registrare in `services/validation.py` accanto a pin_mux/LED. 4. `rule_id` + `source`. 5. Una riga changelog. @@ -557,6 +557,6 @@ Resta, senza inventare numeri: 1. HV / isolation (blocco 8) se il datasheet dà V/mm o keepout HV. 2. 3W / creepage / CPW solo con numero in `layout_rules` o dal calcolatore D2. -3. Un `.kicad_pcb` reale di progetto (non fixture USB inventata) per vedere `PS-PLC`/`PS-SI` sul board. +3. Un `.kicad_pcb` reale di progetto (non fixture USB inventata) per vedere `PE-PLC`/`PE-SI` sul board. Il plugin KiCad aspetta ancora verifica uuid + sheet su un progetto multi-foglio vero. diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md index 8432bc6..838ffc0 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -1,8 +1,8 @@ @AGENTS.md -# Pinscope Frontend +# Periscope Frontend -Next.js 16 app (App Router, Turbopack) providing a web UI for Pinscope schematic validation. Talks to the FastAPI backend at `localhost:8000`. +Next.js 16 app (App Router, Turbopack) providing a web UI for Periscope schematic validation. Talks to the FastAPI backend at `localhost:8000`. ## Architecture @@ -38,7 +38,7 @@ credit state only through `useCredits()` — both are inert in this repo. | Path | Purpose | |---|---| -| `src/lib/types.ts` | TS types mirroring `pinscopex/models.py` | +| `src/lib/types.ts` | TS types mirroring `periscopex/models.py` | | `src/lib/api.ts` | All data fetching — single integration point with backend | | `src/lib/mock-data.ts` | Pipeline step definitions for progress UI | | `src/components/report/` | Report viewer components + power tree React Flow graph + derating table + finding comments | @@ -97,6 +97,6 @@ Requires the backend running at `localhost:8000` (or set `NEXT_PUBLIC_API_URL`). - Keep all data fetching in `src/lib/api.ts` — don't scatter fetch calls across components - Report filters persist in URL search params (`?status=ERROR&component=U3&q=decoupling`) -- When modifying types, keep `src/lib/types.ts` in sync with `backend/pinscopex/models.py` +- When modifying types, keep `src/lib/types.ts` in sync with `backend/periscopex/models.py` - Use `font-mono` for technical values: designators (U1), MPNs, pin names, component values - Status colors: emerald = PASS, amber = WARNING, rose = ERROR, blue = accent/active diff --git a/frontend/content/changelog.md b/frontend/content/changelog.md index 4fe93ab..1a4b1ca 100644 --- a/frontend/content/changelog.md +++ b/frontend/content/changelog.md @@ -1,6 +1,14 @@ # Changelog -What's new in Pinscope. +What's new in Periscope. + +## 2.29.0 — 2026-09-13 — Rebrand to Periscope + +Product name, package (`periscopex`), Docker containers, deploy script, and default site URL are now **Periscope** (`https://periscope.michelebigi.it`). Deterministic finding IDs use the `PE-*` prefix. Existing project.json `pinscope_version` and browser storage keys are still read. + +- [Changed] Brand Pinscope → Periscope across UI, docs, and APIs. +- [Changed] `backend/pinscopex` → `backend/periscopex`; rule IDs `PS-*` → `PE-*`. +- [Changed] Default host / update script → `periscope.michelebigi.it` / `update-periscope.sh`. ## 2.28.12 — 2026-09-13 — Antenna templates: IFA / meander / stub + .kicad_mod @@ -99,7 +107,7 @@ Dedicated Placement job builds the routing-first topology plan without touching Routing-first floorplan foundation without inventing millimetres: domains and satellite role hints after graph build, plus deterministic crystal CL and NC-pin checks. - [New] `functional_groups.json` from `graph_build` (domains, IC groups, `role_hint`, attached `layout_rules`). -- [New] `crystal_cl_check` (PS-XTAL-*) and `nc_pin_check` (PS-NC-001) in the deterministic suite. +- [New] `crystal_cl_check` (PS-XTAL-*) and `nc_pin_check` (PE-NC-001) in the deterministic suite. - [Docs] Piano §0c Placement F1/F2; packing mm stays Layout F2. ## 2.27.1 — 2026-09-12 — DeepSeek roadmap integrations @@ -113,7 +121,7 @@ Close the open P0/P2 items from the growth plan: offline smoke on `simple_projec ## 2.27.0 — 2026-09-11 — Local multi-user auth -Self-host Pinscope accounts (email + password) so several people can share a project. Invite collaborators by email from the project page — same flow as cloud, without Clerk. +Self-host Periscope accounts (email + password) so several people can share a project. Invite collaborators by email from the project page — same flow as cloud, without Clerk. - [New] `AUTH_JWT_SECRET` enables register/login; first user is admin and inherits `users/local` projects. - [New] `/sign-in` and `/sign-up`; sidebar account menu. Set `NEXT_PUBLIC_AUTH_MODE=local` on the frontend build. @@ -166,38 +174,38 @@ A pipeline run with `.kicad_pcb` + stackup samples routed **signal** nets (power `layout_rules` `keepout` flags a foreign net whose track endpoint is inside the KiCad courtyard. Own net and missing courtyard skip. No invented analog/digital classes. -- [New] `PS-PLC-004` WARNING. Tests use `X1` / `/HFXIN` / `GND` from `simple_project`. +- [New] `PE-PLC-004` WARNING. Tests use `X1` / `/HFXIN` / `GND` from `simple_project`. ## 2.24.0 — 2026-09-10 — Crystal load caps and track path Load caps on XIN/XOUT use the same `max_distance_mm` as decoupling. If the PCB has segments on the net, the limit is shortest-path length, not a guessed “loop is too big” ratio. -- [New] Crystals (`X1` / C9 / C10 on `simple_project`) run `PS-PLC-001` when `layout_rules` has millimetres. +- [New] Crystals (`X1` / C9 / C10 on `simple_project`) run `PE-PLC-001` when `layout_rules` has millimetres. - [New] Detour tracks: path along segments vs the same `max_distance_mm`. No segments → euclidean pad distance. ## 2.23.0 — 2026-09-10 — same_layer decoupling If `layout_rules` sets `same_layer: true`, a decoupling cap on the opposite copper from the IC is a WARNING. A via inside the courtyard (calculated) is enough. Unset flag → skip. -- [New] `PS-PLC-003` WARNING when every placed cap on the net is on F vs B opposite the IC. +- [New] `PE-PLC-003` WARNING when every placed cap on the net is on F vs B opposite the IC. ## 2.22.0 — 2026-09-10 — Thermal vias vs min_via_count Via count is calculated inside the KiCad courtyard. The limit is the `min_via_count` parameter from `layout_rules`. No courtyard or no count → skip. No pad radius default. -- [New] `PS-PLC-002` when vias in courtyard < `min_via_count`. `simple_project` has no PCB so it stays silent. +- [New] `PE-PLC-002` when vias in courtyard < `min_via_count`. `simple_project` has no PCB so it stays silent. ## 2.21.0 — 2026-09-10 — Layout SI skew (datasheet mm only) Intra-pair skew is measured on the PCB only when `layout_rules` `length_match` has a number. 3W, creepage, and CPWG are not guessed. -- [New] `PS-SI-001` ERROR when a named pair (_DP/_DM, _P/_N) exceeds that millimetre. No mm in the datasheet → skip. +- [New] `PE-SI-001` ERROR when a named pair (_DP/_DM, _P/_N) exceeds that millimetre. No mm in the datasheet → skip. ## 2.20.0 — 2026-09-10 — Placement vs datasheet (PCB) -Decoupling distance is measured on the `.kicad_pcb` against `layout_rules`. No board → no `PS-PLC-001`. A null millimetre skips — no 3 mm default. +Decoupling distance is measured on the `.kicad_pcb` against `layout_rules`. No board → no `PE-PLC-001`. A null millimetre skips — no 3 mm default. -- [New] `PS-PLC-001` when a decoupling cap is farther than datasheet `max_distance_mm`. Empty `layout_rules` and missing caps skip. +- [New] `PE-PLC-001` when a decoupling cap is farther than datasheet `max_distance_mm`. Empty `layout_rules` and missing caps skip. ## 2.19.0 — 2026-09-10 — Finding review and ECO @@ -219,14 +227,14 @@ The Impedance tab uses the closed-form engine from ImpedenceFinder (Hammerstad Distributor lifecycle is a cached check, not a review scrape. Errata and layout_rules stay structured and skip when the catalog or the PDF has no number. -- [New] `PS-LF-001` EOL, `PS-LF-002` NRND, `PS-LF-003` explicit RoHS fail. Replacement only if the distributor lists it. Active / RoHS N/A / missing cache row are silent. -- [New] `PS-ERRATA-001` when a catalogued workaround pull-up is missing. No URL → skip. -- [New] `PS-INT-001` when `internal_features.pullup_pins` has no rail resistor. +- [New] `PE-LF-001` EOL, `PE-LF-002` NRND, `PE-LF-003` explicit RoHS fail. Replacement only if the distributor lists it. Active / RoHS N/A / missing cache row are silent. +- [New] `PE-ERRATA-001` when a catalogued workaround pull-up is missing. No URL → skip. +- [New] `PE-INT-001` when `internal_features.pullup_pins` has no rail resistor. - [New] `layout_rules` closed kinds; non-numeric `max_distance_mm` is stored as null. ## 2.16.0 — 2026-09-10 — KiCad cad-bridge -Pinscope writes `pinscope-findings.json` next to the report so a KiCad 9/10 action plugin can pan to the symbol uuid on the right sheet. +Periscope writes `periscope-findings.json` next to the report so a KiCad 9/10 action plugin can pan to the symbol uuid on the right sheet. - [New] E2 cad-bridge JSON (`version`, `ref`, `pins`, `sheet`, `uuid`, `severity`). Layout rule ids target pcbnew; others target eeschema. - [New] `.kicad_sch` symbols keep `cad_uuid` + `cad_sheet` in `cad_index` (child sheet, not the empty root). @@ -236,17 +244,17 @@ Pinscope writes `pinscope-findings.json` next to the report so a KiCad 9/10 acti Schema checks now compare regulator load to Iout_max, look at PG→EN when a sequence is declared, and treat DNP as a fitted-variant graph. -- [New] `PS-PWR-001` when specified IQ+I_load exceeds Iout_max, or an explicit series R/ferrite DCR drops >5% of the rail. Missing IQ and PCB traces are not guessed. -- [New] `PS-SEQ-001` WARNING if `power_sequence` is in IC specs and upstream PG does not net to downstream EN. -- [New] BOM `DNP`/`Fitted`/`Variant` on `bom_fields`. Fitted enable with only a DNP pull is `PS-DNP-001` ERROR; no DNP column skips the check. +- [New] `PE-PWR-001` when specified IQ+I_load exceeds Iout_max, or an explicit series R/ferrite DCR drops >5% of the rail. Missing IQ and PCB traces are not guessed. +- [New] `PE-SEQ-001` WARNING if `power_sequence` is in IC specs and upstream PG does not net to downstream EN. +- [New] BOM `DNP`/`Fitted`/`Variant` on `bom_fields`. Fitted enable with only a DNP pull is `PE-DNP-001` ERROR; no DNP column skips the check. ## 2.14.0 — 2026-09-10 — Filtri e termico schema Deterministic checks now match RC/LC/π/T filters and estimate LDO/resistor dissipation without inventing missing numbers. -- [New] Filter topology `PS-FLT-001` (fc INFO) / `PS-FLT-002` vs `adc_sample_rate` only when that spec exists. Ferrite DCR `PS-FLT-003` only with a datasheet limit. -- [New] LDO `P = I_load×(Vin−Vout)` and `Tj = 25 + P·θJA`. Missing θJA is `PS-TH-001` INFO. `Iout_max` is not treated as load. -- [New] Resistor `I²R` vs `power_rating_w` on LED paths and shunts with known ΔV (`PS-TH-003`). +- [New] Filter topology `PE-FLT-001` (fc INFO) / `PE-FLT-002` vs `adc_sample_rate` only when that spec exists. Ferrite DCR `PE-FLT-003` only with a datasheet limit. +- [New] LDO `P = I_load×(Vin−Vout)` and `Tj = 25 + P·θJA`. Missing θJA is `PE-TH-001` INFO. `Iout_max` is not treated as load. +- [New] Resistor `I²R` vs `power_rating_w` on LED paths and shunts with known ΔV (`PE-TH-003`). ## 2.13.0 — 2026-09-10 — DC-bias C_eff stima @@ -254,20 +262,20 @@ The derating table now shows an effective capacitance under DC bias for C0G/X7R/ - [New] `C_eff` column from an empirical V/Vrated table. Tantalum/electrolytic and unknown dielectrics are left blank. - [Improved] C0G/NP0 stays at nominal C; X7R at 50% of rated V is about 70% of C. -- [New] Bulk C without a ~100 nF ceramic is `PS-ESR-001` INFO (no invented Z(f) target). +- [New] Bulk C without a ~100 nF ceramic is `PE-ESR-001` INFO (no invented Z(f) target). ## 2.12.0 — 2026-09-10 — Pull-up sizing and LDO Cout Deterministic schema checks now size I2C pull-ups, flag NRST pull-downs, and look at LDO VOUT capacitance — still WARNING, never a invented datasheet µF ERROR. -- [New] I2C pull-up value vs a wide NXP UM10204 band (`PS-I2C-002`). 4.7 kΩ is in-band; missing values are not sized. -- [New] Active-low reset with a resistor to ground is `PS-RST-002`. -- [New] Regulator VOUT needs Cout (`PS-DEC-001`); 100 nF-only on VOUT is `PS-DEC-002`. MCU VDD 100 nF is not flagged. +- [New] I2C pull-up value vs a wide NXP UM10204 band (`PE-I2C-002`). 4.7 kΩ is in-band; missing values are not sized. +- [New] Active-low reset with a resistor to ground is `PE-RST-002`. +- [New] Regulator VOUT needs Cout (`PE-DEC-001`); 100 nF-only on VOUT is `PE-DEC-002`. MCU VDD 100 nF is not flagged. - [Improved] Pin-mux UART0 on the `simple_project` MSPM0 nets is covered in tests (SPI PICO/POCI already was). ## 2.11.0 — 2026-09-10 — DeepSeek V4.1 and re-analyze -Pinscope now defaults to DeepSeek-V4.1-Flash (`deepseek-flash`) for every LLM stage, shows API cost in dollars, and lets you replace the BOM and netlist on an existing project without deleting it. +Periscope now defaults to DeepSeek-V4.1-Flash (`deepseek-flash`) for every LLM stage, shows API cost in dollars, and lets you replace the BOM and netlist on an existing project without deleting it. - [New] Default model is `deepseek-flash` (native vision). Legacy `deepseek-v4-flash` / `deepseek-v4-flash-vision-exp` names still work; they route to V4.1. - [New] Replace BOM & netlist on a finished project and re-run the analysis. History, library cache, and prior spend stay on the same project. @@ -291,7 +299,7 @@ Chips and passives stay in a shared library after the first look, so the next bo ## 2.8.0 — 2026-08-27 — Automatic datasheets -Pinscope now finds datasheet PDFs on its own. You can still upload a file, but you no longer need DigiKey keys for the common case. +Periscope now finds datasheet PDFs on its own. You can still upload a file, but you no longer need DigiKey keys for the common case. - [New] Auto-fetch from LCSC (no API key) by manufacturer part number or LCSC code, with exact-MPN matching so a CH340E never silently becomes a CH340G. - [New] Direct Texas Instruments datasheet URLs (`ti.com/lit/ds/symlink/…`) as a second source for TI parts. @@ -300,7 +308,7 @@ Pinscope now finds datasheet PDFs on its own. You can still upload a file, but y ## 2.7.0 — 2026-08-27 — DeepSeek API -Pinscope now talks to DeepSeek by default. Extraction skills run locally; datasheet PDFs are converted to text (and page images on the vision model) because DeepSeek does not accept native PDF documents. +Periscope now talks to DeepSeek by default. Extraction skills run locally; datasheet PDFs are converted to text (and page images on the vision model) because DeepSeek does not accept native PDF documents. - [New] DeepSeek provider (`deepseek-v4-flash`, `deepseek-v4-pro`, `deepseek-v4-flash-vision-exp`) via the OpenAI-compatible Chat Completions API. - [New] Local skill runner: `skills/*/SKILL.md` is inlined and `validate.py` runs in-process — no Anthropic Console upload required. @@ -309,7 +317,7 @@ Pinscope now talks to DeepSeek by default. Extraction skills run locally; datash ## 2.6.0 — 2026-07-12 — Export Report to Excel -Download a project's findings as an Excel spreadsheet straight from the report — one click, ready to share, filter, or archive outside Pinscope. +Download a project's findings as an Excel spreadsheet straight from the report — one click, ready to share, filter, or archive outside Periscope. - [New] "Export Excel" button on the validation report. Every finding becomes a spreadsheet row — designator, part number, ID, severity, title, description, recommendation, and its datasheet source (page included) — sorted most-severe first. @@ -321,7 +329,7 @@ Schematic review now works through every functional area of a component before f ## 2.5.0 — 2026-07-02 — Light Mode -Pinscope now has a light theme. Toggle between light and dark with the sun/moon button — in the sidebar next to your account menu, or in the header on the website. +Periscope now has a light theme. Toggle between light and dark with the sun/moon button — in the sidebar next to your account menu, or in the header on the website. - [New] Theme toggle. Switch between light and dark mode anywhere in the app; your choice is remembered on this device. Everything defaults to dark, exactly as before, until you flip it. - [Improved] Every status color — error, warning, and pass badges, finding cards, the progress view, billing — is tuned for both themes, so reports stay legible either way. @@ -331,14 +339,14 @@ Pinscope now has a light theme. Toggle between light and dark with the sun/moon Two datasheet-grounded checks now run on every project, independent of the schematic review — catching a swapped-peripheral pin or an over-driven LED — plus a clear list of any components that had no datasheet to review against. -- [New] Pin-function feasibility check. Pinscope now flags when a net assigns an IC pin a peripheral function its silicon can't route — for example a `UART5_TX` net on a pin whose alternate-function table only offers `UART5_RX`. It's reported as an error straight from the datasheet's pin table and names the likely swap (TX↔RX, SDA↔SCL). It deliberately does not judge signal *direction* across an interface — a direct UART crosses TX↔RX while a transceiver runs straight through — so it only fires on physically impossible pin assignments, never on wiring style. -- [New] LED forward-current check. For each LED, Pinscope computes the forward current from the supply rail, the series resistor, and the LED's rated forward voltage, and flags any channel whose current exceeds the LED's rated maximum. Each color of an RGB LED is checked separately, and a leg with no current-limiting resistor at all is called out as a caution. +- [New] Pin-function feasibility check. Periscope now flags when a net assigns an IC pin a peripheral function its silicon can't route — for example a `UART5_TX` net on a pin whose alternate-function table only offers `UART5_RX`. It's reported as an error straight from the datasheet's pin table and names the likely swap (TX↔RX, SDA↔SCL). It deliberately does not judge signal *direction* across an interface — a direct UART crosses TX↔RX while a transceiver runs straight through — so it only fires on physically impossible pin assignments, never on wiring style. +- [New] LED forward-current check. For each LED, Periscope computes the forward current from the supply rail, the series resistor, and the LED's rated forward voltage, and flags any channel whose current exceeds the LED's rated maximum. Each color of an RGB LED is checked separately, and a leg with no current-limiting resistor at all is called out as a caution. - [New] "Not reviewed" list on the report. Components with no datasheet on file — for instance a do-not-populate footprint that isn't in the BOM — are now called out explicitly, so a mis-wired pin on an unreviewed part shows up as a known gap instead of being silently absent. - [New] Findings from these automatic checks carry an "Automated check" badge, so they're easy to tell apart from datasheet-review findings. ## 2.3.3 — 2026-06-08 — Faster Reviews -Multi-chip designs now review several times faster — Pinscope works through ICs in parallel instead of one at a time. +Multi-chip designs now review several times faster — Periscope works through ICs in parallel instead of one at a time. - [Improved] Datasheet extraction and schematic review now process multiple ICs at once, so reports on multi-IC projects come back substantially faster. The findings are unchanged — only the wait is shorter. @@ -364,7 +372,7 @@ EDIF 2.0.0 netlists upload alongside PADS-PCB, with a sub-design picker for file ## 2.3.0 — 2026-05-24 — LCSC Part Number Support -JLCPCB-style BOMs with LCSC part numbers (e.g. `C12044`) now work out of the box — Pinscope auto-detects the column, resolves each id to the real manufacturer part number, and shows you what it resolved to before the pipeline runs. +JLCPCB-style BOMs with LCSC part numbers (e.g. `C12044`) now work out of the box — Periscope auto-detects the column, resolves each id to the real manufacturer part number, and shows you what it resolved to before the pipeline runs. - [New] LCSC part numbers in the manufacturer part number column are auto-detected at BOM upload and converted to real MPNs. Works with JLCPCB / EasyEDA exports without any column renaming. - [New] Project setup now shows the LCSC → MPN mapping on each IC row in the datasheet step (e.g. `C12044 → TP4057-42-SOT26-R`), so you can see what each LCSC id became before the pipeline starts. @@ -378,7 +386,7 @@ Tabbed file upload guide with per-tool instructions, Xpedition coverage, and dir - [New] Documentation for exporting a PADS-PCB netlist from Siemens Xpedition Designer / DxDesigner (VX.2.x, including VX.2.14). - [Improved] File upload guide reorganized into tabs — KiCad, Altium, OrCAD/Allegro, Xpedition, EasyEDA, and Eagle each get their own panel. - [Improved] Netlist uploads now accept `.asc`, `.net`, `.NET`, and `.txt` directly — no more renaming required before upload. -- [Improved] File guide now calls out the difference between the PADS-PCB schematic netlist Pinscope needs and the `!PADS-POWERPCB` PCB-layout dump that some EDA tools also save as `.asc`. +- [Improved] File guide now calls out the difference between the PADS-PCB schematic netlist Periscope needs and the `!PADS-POWERPCB` PCB-layout dump that some EDA tools also save as `.asc`. ## 2.2.0 — 2026-05-20 — Cross-chip Datasheet Review diff --git a/frontend/content/file-guide.md b/frontend/content/file-guide.md index 478995e..a6cce51 100644 --- a/frontend/content/file-guide.md +++ b/frontend/content/file-guide.md @@ -1,29 +1,29 @@ # File Upload Guide -Pinscope needs two files from your EDA tool to review a schematic, and an optional KiCad board for layout: +Periscope needs two files from your EDA tool to review a schematic, and an optional KiCad board for layout: -- A **PADS-PCB ASCII netlist** or an **EDIF 2.0.0 netlist** — the circuit's connectivity. Pinscope accepts `.asc`, `.net`, `.NET`, `.txt` (PADS-PCB) and `.edn`, `.edif`, `.edf` (EDIF); the format is auto-detected from the file's first bytes. +- A **PADS-PCB ASCII netlist** or an **EDIF 2.0.0 netlist** — the circuit's connectivity. Periscope accepts `.asc`, `.net`, `.NET`, `.txt` (PADS-PCB) and `.edn`, `.edif`, `.edf` (EDIF); the format is auto-detected from the file's first bytes. - A **Bill of Materials** (CSV or XLSX) — mapping each reference designator to a manufacturer part number. - Optional: a **KiCad PCB** (`.kicad_pcb`) — placement, keepout, pair length, and net Z0. Schematic review still runs without it. ## Example files -New to Pinscope? Here's a complete set of files from [Phil's Lab](https://www.youtube.com/@PhilsLab)' KiCad 9 TI MSPM0 tutorial you can download and upload as a starter project: +New to Periscope? Here's a complete set of files from [Phil's Lab](https://www.youtube.com/@PhilsLab)' KiCad 9 TI MSPM0 tutorial you can download and upload as a starter project: - [TI-MSP-KICAD9-TUTORIAL.asc](/examples/TI-MSP-KICAD9-TUTORIAL.asc) — netlist - [TI-MSP-KICAD9-TUTORIAL.csv](/examples/TI-MSP-KICAD9-TUTORIAL.csv) — BOM -- [TI-MSP-KICAD9-TUTORIAL-SCHEMATIC.pdf](/examples/TI-MSP-KICAD9-TUTORIAL-SCHEMATIC.pdf) — schematic (for your reference; Pinscope doesn't need this) +- [TI-MSP-KICAD9-TUTORIAL-SCHEMATIC.pdf](/examples/TI-MSP-KICAD9-TUTORIAL-SCHEMATIC.pdf) — schematic (for your reference; Periscope doesn't need this) -The BOM below shows the shape Pinscope is looking for. +The BOM below shows the shape Periscope is looking for. ## The BOM -Pinscope auto-detects BOM columns by header name. After upload you'll confirm which column holds designators and which holds part numbers, so the exact header names don't matter — only that these columns exist. +Periscope auto-detects BOM columns by header name. After upload you'll confirm which column holds designators and which holds part numbers, so the exact header names don't matter — only that these columns exist. **Required** -- **Designator / Reference** — one row per part, or grouped references like `C1,C2,C5` in a single row (Pinscope expands these automatically). -- **Manufacturer Part Number (MPN)** — the full orderable part number. Pinscope uses this to look up datasheets, so `10uF 0805` on its own is **not** enough — it needs e.g. `GRM21BR61C106KE15L`. +- **Designator / Reference** — one row per part, or grouped references like `C1,C2,C5` in a single row (Periscope expands these automatically). +- **Manufacturer Part Number (MPN)** — the full orderable part number. Periscope uses this to look up datasheets, so `10uF 0805` on its own is **not** enough — it needs e.g. `GRM21BR61C106KE15L`. **Recommended** @@ -34,7 +34,7 @@ CSV and XLSX both work. For XLSX, the first worksheet is used. ## The Netlist -Pinscope accepts either a **PADS-PCB ASCII netlist** or an **EDIF 2.0.0 netlist** — the upload form auto-detects which one you sent based on the file's first bytes, so you don't have to pick a format. +Periscope accepts either a **PADS-PCB ASCII netlist** or an **EDIF 2.0.0 netlist** — the upload form auto-detects which one you sent based on the file's first bytes, so you don't have to pick a format. ### PADS-PCB ASCII @@ -55,18 +55,18 @@ U1.24 C1.1 *END* ``` -If your file starts with `*PADS-PCB*` and ends with `*END*`, you're good. Reference designators may contain spaces (e.g. `CV GND`) — Pinscope resolves them against your BOM. +If your file starts with `*PADS-PCB*` and ends with `*END*`, you're good. Reference designators may contain spaces (e.g. `CV GND`) — Periscope resolves them against your BOM. **Heads up — two different `.asc` files exist.** PADS (and tools that interop with PADS, like Xpedition) use the `.asc` extension for two unrelated things: -- The **schematic-exported netlist** starts with `*PADS-PCB*` and lists `*PART*` / `*NET*` sections. **This is what Pinscope wants.** -- The **full PCB layout dump** starts with `!PADS-POWERPCB-V…` and contains routing/footprint geometry. Pinscope cannot parse this. +- The **schematic-exported netlist** starts with `*PADS-PCB*` and lists `*PART*` / `*NET*` sections. **This is what Periscope wants.** +- The **full PCB layout dump** starts with `!PADS-POWERPCB-V…` and contains routing/footprint geometry. Periscope cannot parse this. If your upload errors with "No components found", check the first line of the file. ### EDIF 2.0.0 -EDIF is a vendor-neutral s-expression format. The first form is `(edif …`, with an `(edifVersion 2 0 0)` declaration near the top, libraries that define each cell's pin list, and a design library that lists `(instance …)` and `(net …)` forms. Pinscope has been verified against **Siemens xDX Designer / DxDesigner** exports; other EDIF 2.0.0 exporters (OrCAD, Altium, KiCad, Eagle) follow the same grammar and should work, but haven't been broadly tested. If your EDIF file doesn't parse, [contact us](/contact) and send a snippet — most fixes are small. +EDIF is a vendor-neutral s-expression format. The first form is `(edif …`, with an `(edifVersion 2 0 0)` declaration near the top, libraries that define each cell's pin list, and a design library that lists `(instance …)` and `(net …)` forms. Periscope has been verified against **Siemens xDX Designer / DxDesigner** exports; other EDIF 2.0.0 exporters (OrCAD, Altium, KiCad, Eagle) follow the same grammar and should work, but haven't been broadly tested. If your EDIF file doesn't parse, [contact us](/contact) and send a snippet — most fixes are small. If your tool exports both PADS-PCB and EDIF, PADS-PCB is the path more users have validated; use EDIF when it's the only option. @@ -127,17 +127,17 @@ Works in **Xpedition Designer / DxDesigner** (VX.2.x, including VX.2.14). Xpedit 3. **Tools → PCB Interface** → choose the **PADS** template (`pads2007.cfg` or equivalent). 4. Run the export. The output's first line should be `*PADS-PCB*` with `*PART*` and `*NET*` sections below. The file extension (`.txt`, `.net`, or `.asc`) doesn't matter — upload it as-is. -If your project is locked to the integrated Xpedition flow, **export EDIF instead** — DxDesigner's EDIF Exporter runs against any project type and Pinscope accepts the resulting `.edn` as an equivalent input. See **Netlist (EDIF alternative)** below. +If your project is locked to the integrated Xpedition flow, **export EDIF instead** — DxDesigner's EDIF Exporter runs against any project type and Periscope accepts the resulting `.edn` as an equivalent input. See **Netlist (EDIF alternative)** below. **Netlist (EDIF alternative)** 1. Open the schematic in Xpedition Designer / DxDesigner. 2. **File → Export → EDIF…** (older builds: **Tools → Run Tool → Edif Exporter**). 3. In the export dialog: - - **EDIF version**: **2.0.0** (Pinscope only supports 2.0.0) + - **EDIF version**: **2.0.0** (Periscope only supports 2.0.0) - **EDIF level**: **0** (the default) - **Output format**: **Netlist view** — make sure cells, instances, nets, and the `viewMap` back-annotation block are all included - - **Designator source**: include back-annotated designators (otherwise instances export as templates like `U?` / `R?` and Pinscope drops them) + - **Designator source**: include back-annotated designators (otherwise instances export as templates like `U?` / `R?` and Periscope drops them) 4. Save as `.edn` and upload it as the netlist. The file should start with `(edif …)` and contain `(edifVersion 2 0 0)` near the top. If neither PADS nor EDIF works for your project setup, send us the BOM and schematic PDF via [Contact](/contact) — we can usually help unblock the export. @@ -178,10 +178,10 @@ For both EasyEDA Standard and EasyEDA Pro. - **"No components found"** — your netlist is missing the `*PART*` section. Re-export specifically in PADS-PCB format (not Spice, Protel, or a generic text netlist). If the first line is `!PADS-POWERPCB-V…`, you uploaded the PCB layout dump instead of the schematic netlist — re-export from the schematic side. - **"No ground net found"** — your netlist has no net named `GND`, `VSS`, `AGND`, `DGND`, or similar. If you exported a sub-sheet, re-export the top sheet instead. -- **Unresolved parts after the pipeline runs** — a BOM row has no MPN, or the MPN wasn't found on DigiKey. Add the MPN, or rely on Pinscope's value fallback (fills in from the `Value` / `Comment` column). +- **Unresolved parts after the pipeline runs** — a BOM row has no MPN, or the MPN wasn't found on DigiKey. Add the MPN, or rely on Periscope's value fallback (fills in from the `Value` / `Comment` column). ## The KiCad PCB (optional) -Drop the KiCad **project folder** (or a zip of it) on Schematic — Pinscope takes the sheets and the `.kicad_pcb` if it is there. You can also add the board later. +Drop the KiCad **project folder** (or a zip of it) on Schematic — Periscope takes the sheets and the `.kicad_pcb` if it is there. You can also add the board later. Still stuck? [Contact us](/contact) with your netlist and BOM attached and we'll take a look. diff --git a/frontend/content/privacy.md b/frontend/content/privacy.md index a93c051..817f2f8 100644 --- a/frontend/content/privacy.md +++ b/frontend/content/privacy.md @@ -1,8 +1,8 @@ -# Pinscope Privacy Policy +# Periscope Privacy Policy **Last updated: April 5, 2026** -This Privacy Policy explains how Faradworks, Inc. ("Faradworks," "we," "us," and "our") collects, uses, and discloses information in connection with the Pinscope website (pinscope.ai), platform, and related services (the "Service"). +This Privacy Policy explains how Faradworks, Inc. ("Faradworks," "we," "us," and "our") collects, uses, and discloses information in connection with the Periscope website (periscope.michelebigi.it), platform, and related services (the "Service"). This Privacy Policy is intended for free users and self-serve paid users. Enterprise customers typically use the Service under a separate agreement and (if applicable) a data processing agreement ("DPA"), which may include additional privacy and security terms. diff --git a/frontend/content/terms.md b/frontend/content/terms.md index bb742c3..a8fad1c 100644 --- a/frontend/content/terms.md +++ b/frontend/content/terms.md @@ -1,8 +1,8 @@ -# Pinscope Terms of Service +# Periscope Terms of Service **Last updated: April 5, 2026** -These Terms of Service ("Terms") govern access to and use of the Pinscope platform (pinscope.ai) and related services (the "Service"). These Terms apply to free users and self-serve paid users. If you have a separate written agreement signed by Faradworks, Inc. (for example, an enterprise agreement), that agreement governs your use of the Service to the extent it conflicts with these Terms. +These Terms of Service ("Terms") govern access to and use of the Periscope platform (periscope.michelebigi.it) and related services (the "Service"). These Terms apply to free users and self-serve paid users. If you have a separate written agreement signed by Faradworks, Inc. (for example, an enterprise agreement), that agreement governs your use of the Service to the extent it conflicts with these Terms. These Terms incorporate Faradworks' [Privacy Policy](/privacy) and any policies referenced in the Service. @@ -16,7 +16,7 @@ By creating an account, clicking to accept these Terms (for example, by clicking **"Derived Data"** means technical artifacts generated solely to operate the Service, such as parsed text, indexes, embeddings, summaries, or extracted metadata. -**"Service"** means the Pinscope platform (pinscope.ai), including all features, APIs, and related services. +**"Service"** means the Periscope platform (periscope.michelebigi.it), including all features, APIs, and related services. **"Content"** refers collectively to Customer Content, Outputs, and Derived Data. @@ -170,15 +170,15 @@ Plans may include limits on reviews, tokens, files, API spend, usage allocations ### 11.2 Prepaid service credits. -Certain Services, plans, or features may allow or require you to prepay for future eligible Pinscope review services for professional, business, or organizational use by purchasing prepaid service credits ("Usage Credits"). Usage Credits represent a prepaid, limited, revocable, non-transferable license to access eligible Pinscope review services up to the applicable credited amount and may be used only for eligible Pinscope review charges as described in the Service. Faradworks may also, in its sole discretion, provide free or promotional credits ("Promotional Credits"), which may be subject to additional restrictions or expiration dates stated when issued. +Certain Services, plans, or features may allow or require you to prepay for future eligible Periscope review services for professional, business, or organizational use by purchasing prepaid service credits ("Usage Credits"). Usage Credits represent a prepaid, limited, revocable, non-transferable license to access eligible Periscope review services up to the applicable credited amount and may be used only for eligible Periscope review charges as described in the Service. Faradworks may also, in its sole discretion, provide free or promotional credits ("Promotional Credits"), which may be subject to additional restrictions or expiration dates stated when issued. ### 11.3 Credit characteristics and workspace scope. -Credits may be used only for eligible Pinscope review charges and may not be used for any other product or service unless Faradworks expressly states otherwise in the Service. Credits are not legal tender, are not currency, are not redeemable for cash, are not refundable except as required by law or expressly stated by Faradworks, do not constitute or confer any personal property right, and do not constitute a bank account, deposit account, stored-value account, digital wallet, payment instrument, or other monetary account. Credits are an internal service accounting mechanism that measures the amount of eligible Pinscope review services you have prepaid and are licensed to use. Any credit balance or similar amount displayed in the Service reflects only our record of remaining prepaid eligibility for future eligible review charges and does not represent money held on your behalf. Credits are non-transferable, may not be sold, assigned, gifted, or sublicensed, and may be used only by the workspace or account to which they are issued. If credits are issued to an organization or workspace, they belong to that workspace and may be consumed by authorized users acting within that workspace. +Credits may be used only for eligible Periscope review charges and may not be used for any other product or service unless Faradworks expressly states otherwise in the Service. Credits are not legal tender, are not currency, are not redeemable for cash, are not refundable except as required by law or expressly stated by Faradworks, do not constitute or confer any personal property right, and do not constitute a bank account, deposit account, stored-value account, digital wallet, payment instrument, or other monetary account. Credits are an internal service accounting mechanism that measures the amount of eligible Periscope review services you have prepaid and are licensed to use. Any credit balance or similar amount displayed in the Service reflects only our record of remaining prepaid eligibility for future eligible review charges and does not represent money held on your behalf. Credits are non-transferable, may not be sold, assigned, gifted, or sublicensed, and may be used only by the workspace or account to which they are issued. If credits are issued to an organization or workspace, they belong to that workspace and may be consumed by authorized users acting within that workspace. ### 11.4 Credit purchases and application to charges. -Your order for Usage Credits constitutes an offer to purchase those Usage Credits. Faradworks may accept or reject any purchase request in its discretion. Credits are issued when Faradworks confirms the purchase or otherwise makes the credits available in your account or workspace. Credits are applied to eligible Pinscope review charges in the manner described in the Service. Faradworks may reserve, deduct, reverse, release, or adjust credits to reflect quoted charges, completed usage, failed runs, duplicate requests, fraud checks, refunds, chargebacks, or billing corrections. Credit pricing, minimum purchase amounts, maximum purchase amounts, and applicable taxes will be shown in the Service or at checkout. Fees are exclusive of taxes unless stated otherwise. +Your order for Usage Credits constitutes an offer to purchase those Usage Credits. Faradworks may accept or reject any purchase request in its discretion. Credits are issued when Faradworks confirms the purchase or otherwise makes the credits available in your account or workspace. Credits are applied to eligible Periscope review charges in the manner described in the Service. Faradworks may reserve, deduct, reverse, release, or adjust credits to reflect quoted charges, completed usage, failed runs, duplicate requests, fraud checks, refunds, chargebacks, or billing corrections. Credit pricing, minimum purchase amounts, maximum purchase amounts, and applicable taxes will be shown in the Service or at checkout. Fees are exclusive of taxes unless stated otherwise. ### 11.5 Credit expiration, forfeiture, and promotional credits. diff --git a/frontend/public/favicon_io/site.webmanifest b/frontend/public/favicon_io/site.webmanifest index 57ec2d5..dce5d73 100644 --- a/frontend/public/favicon_io/site.webmanifest +++ b/frontend/public/favicon_io/site.webmanifest @@ -1,6 +1,6 @@ { - "name": "Pinscope", - "short_name": "Pinscope", + "name": "Periscope", + "short_name": "Periscope", "icons": [ { "src": "/favicon_io/android-chrome-192x192.png", diff --git a/frontend/src/app/(app)/admin/page.tsx b/frontend/src/app/(app)/admin/page.tsx index 64fd853..eb304c1 100644 --- a/frontend/src/app/(app)/admin/page.tsx +++ b/frontend/src/app/(app)/admin/page.tsx @@ -234,7 +234,7 @@ function ProjectsPanel() { // Stash the source project id and hand off to the dashboard, which // mounts the create-project dialog. The dialog fetches the full // Project on its end so we don't have to pass the entire object here. - window.sessionStorage.setItem("pinscopex:cloneAsNewProjectId", projectId); + window.sessionStorage.setItem("periscopex:cloneAsNewProjectId", projectId); router.push("/dashboard"); } diff --git a/frontend/src/app/(app)/dashboard/page.tsx b/frontend/src/app/(app)/dashboard/page.tsx index d402525..5490b03 100644 --- a/frontend/src/app/(app)/dashboard/page.tsx +++ b/frontend/src/app/(app)/dashboard/page.tsx @@ -20,7 +20,7 @@ import { useCredits } from "@/components/billing/credits-context"; import { OnboardingSurvey } from "@/components/dashboard/onboarding-survey"; type ViewMode = "cards" | "table"; -const VIEW_STORAGE_KEY = "pinscopex:dashboard:view"; +const VIEW_STORAGE_KEY = "periscopex:dashboard:view"; export default function DashboardPage() { return ( @@ -79,10 +79,10 @@ function DashboardContent() { // Admin handoff: "Rerun as new project" stashes a project ID here. const cloneId = typeof window !== "undefined" - ? window.sessionStorage.getItem("pinscopex:cloneAsNewProjectId") + ? window.sessionStorage.getItem("periscopex:cloneAsNewProjectId") : null; if (cloneId) { - window.sessionStorage.removeItem("pinscopex:cloneAsNewProjectId"); + window.sessionStorage.removeItem("periscopex:cloneAsNewProjectId"); fetchProject(cloneId) .then(setCloneAsNewProject) .catch(() => { diff --git a/frontend/src/app/(app)/feedback/page.tsx b/frontend/src/app/(app)/feedback/page.tsx index 31b57aa..b77436b 100644 --- a/frontend/src/app/(app)/feedback/page.tsx +++ b/frontend/src/app/(app)/feedback/page.tsx @@ -104,7 +104,7 @@ export default function FeedbackPage() { )} {expanded === t.ticket_id && t.admin_notes && (
-

Pinscope Team:

+

Periscope Team:

{t.admin_notes}

)} diff --git a/frontend/src/app/(app)/project/[id]/page.tsx b/frontend/src/app/(app)/project/[id]/page.tsx index 3948c2b..81b673a 100644 --- a/frontend/src/app/(app)/project/[id]/page.tsx +++ b/frontend/src/app/(app)/project/[id]/page.tsx @@ -46,6 +46,13 @@ import { ImpedancePanel } from "@/components/project/impedance-panel"; import { TopologyPanel } from "@/components/project/topology-panel"; import { PcbUploadButton } from "@/components/project/pcb-upload"; import { PdfViewerSheet } from "@/components/pdf/pdf-viewer-sheet"; +import { + deratingOverridesKey, + deratingSettingsKey, + legacyDeratingOverridesKey, + legacyDeratingSettingsKey, + migrateLocalKey, +} from "@/lib/storage-keys"; export default function ProjectDetailPage({ params, @@ -61,7 +68,10 @@ export default function ProjectDetailPage({ const [deratingSettings, setDeratingSettings] = useState(() => { if (typeof window === "undefined") return { ceramic: 50, tantalum: 50, electrolytic: 50 }; try { - const stored = localStorage.getItem(`pinscopex:derating-settings:${id}`); + const stored = migrateLocalKey( + deratingSettingsKey(id), + legacyDeratingSettingsKey(id), + ); return stored ? JSON.parse(stored) : { ceramic: 50, tantalum: 50, electrolytic: 50 }; } catch { return { ceramic: 50, tantalum: 50, electrolytic: 50 }; @@ -70,7 +80,10 @@ export default function ProjectDetailPage({ const [manualVoltages, setManualVoltages] = useState>(() => { if (typeof window === "undefined") return {}; try { - const stored = localStorage.getItem(`pinscopex:derating-overrides:${id}`); + const stored = migrateLocalKey( + deratingOverridesKey(id), + legacyDeratingOverridesKey(id), + ); return stored ? JSON.parse(stored) : {}; } catch { return {}; @@ -83,12 +96,12 @@ export default function ProjectDetailPage({ // Persist derating settings to localStorage useEffect(() => { - localStorage.setItem(`pinscopex:derating-settings:${id}`, JSON.stringify(deratingSettings)); + localStorage.setItem(deratingSettingsKey(id), JSON.stringify(deratingSettings)); }, [deratingSettings, id]); // Persist manual voltage overrides to localStorage useEffect(() => { - localStorage.setItem(`pinscopex:derating-overrides:${id}`, JSON.stringify(manualVoltages)); + localStorage.setItem(deratingOverridesKey(id), JSON.stringify(manualVoltages)); }, [manualVoltages, id]); const reload = useCallback(() => { @@ -458,7 +471,7 @@ export default function ProjectDetailPage({ - + )} @@ -1281,9 +1294,9 @@ function ReportVersionSection({

- Generated with Pinscope + Generated with Periscope

- v{pinscopeVersion} + v{periscopeVersion}
@@ -1299,7 +1312,7 @@ function PipelineErrorBanner({ }) { const [copied, setCopied] = useState(false); const detail = message ?? "Unknown error — no details were recorded."; - const shareText = `PinscopeX project ${projectId} failed: ${detail}`; + const shareText = `PeriscopeX project ${projectId} failed: ${detail}`; const handleCopy = async () => { try { diff --git a/frontend/src/app/(marketing)/changelog/page.tsx b/frontend/src/app/(marketing)/changelog/page.tsx index 67b796d..72b752b 100644 --- a/frontend/src/app/(marketing)/changelog/page.tsx +++ b/frontend/src/app/(marketing)/changelog/page.tsx @@ -6,7 +6,7 @@ import { pageMetadata } from "@/lib/site"; export const metadata = pageMetadata({ title: "Changelog", description: - "Recent updates and improvements to Pinscope — new EDA tool support, review accuracy improvements, and platform changes.", + "Recent updates and improvements to Periscope — new EDA tool support, review accuracy improvements, and platform changes.", path: "/changelog", }); diff --git a/frontend/src/app/(marketing)/contact/nav.tsx b/frontend/src/app/(marketing)/contact/nav.tsx index 28c82d3..3be5a69 100644 --- a/frontend/src/app/(marketing)/contact/nav.tsx +++ b/frontend/src/app/(marketing)/contact/nav.tsx @@ -14,7 +14,7 @@ export function Nav() {
- Pinscope + Periscope
- Pinscope + Periscope New Reply @@ -1113,7 +1113,7 @@ def _render_feedback_reply_email( Hi {_esc(recipient_first_name)},
- The Pinscope team just replied to your feedback. + The Periscope team just replied to your feedback.
@@ -1170,7 +1170,7 @@ def _render_feedback_reply_email( Thank you so much for taking the time to share your feedback — we truly value it.
- Pinscope team + Periscope team
{_esc(reply_text)} @@ -1155,12 +1155,12 @@ def _render_feedback_reply_email( - View in Pinscope → + View in Periscope →
- — The Pinscope team + — The Periscope team
@@ -1180,7 +1180,7 @@ def _render_feedback_reply_email(
- Pinscope · Agentic schematic validation + Periscope · Agentic schematic validation