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 <cursoragent@cursor.com>
This commit is contained in:
2026-09-13 20:02:04 +02:00
co-authored by Cursor
parent 556306bf4d
commit 8d2b85600f
177 changed files with 869 additions and 766 deletions
+2 -2
View File
@@ -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
+9 -9
View File
@@ -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
+1 -1
View File
@@ -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).
+3 -3
View File
@@ -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()
+2 -2
View File
@@ -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
+4 -4
View File
@@ -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,
)
+2 -2
View File
@@ -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"
@@ -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}) '
@@ -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,
@@ -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
@@ -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(
@@ -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 ""
@@ -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
@@ -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)"}
@@ -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
@@ -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
@@ -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):
@@ -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()):
@@ -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",
@@ -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"),
@@ -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
@@ -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]
@@ -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:
@@ -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
@@ -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",
@@ -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
@@ -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
@@ -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}"],
)
@@ -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)
@@ -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)
@@ -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,
@@ -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
@@ -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",
)
@@ -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"),
@@ -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",
@@ -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
@@ -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
@@ -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
# ---------------------------------------------------------------------------
@@ -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(
@@ -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"})
@@ -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
@@ -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,
@@ -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
@@ -1,4 +1,4 @@
"""Shared utility functions for the pinscopex core library."""
"""Shared utility functions for the periscopex core library."""
from __future__ import annotations
@@ -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)
@@ -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."
),
+1 -1
View File
@@ -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
+1 -1
View File
@@ -1,4 +1,4 @@
"""Local Pinscope auth endpoints (register / login / me)."""
"""Local Periscope auth endpoints (register / login / me)."""
from __future__ import annotations
+4 -4
View File
@@ -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}
</table>
<div style="margin-top: 16px; padding: 16px; background: #f9fafb; border-radius: 8px; font-size: 14px; line-height: 1.6; white-space: pre-wrap;">{message}</div>
<p style="margin-top: 24px; font-size: 12px; color: #888;">Sent from the Pinscope contact form</p>
<p style="margin-top: 24px; font-size: 12px; color: #888;">Sent from the Periscope contact form</p>
</div>"""
msg.attach(MIMEText(html_body, "html"))
+5 -5
View File
@@ -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
+4 -4
View File
@@ -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")
+11 -11
View File
@@ -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"
+4 -4
View File
@@ -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))
+4 -4
View File
@@ -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
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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/"
+1 -1
View File
@@ -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
+34 -34
View File
@@ -174,7 +174,7 @@ def _render_report_email(
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 20px; font-weight: 700; color: #ffffff; letter-spacing: -0.025em;">
Pinscope
Periscope
</td>
<td align="right" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; color: #9ca3af; text-transform: uppercase; letter-spacing: 0.05em;">
Report Ready
@@ -247,7 +247,7 @@ def _render_report_email(
<tr><td style="background-color: #f9fafb; padding: 20px 32px; border-top: 1px solid #e5e7eb;">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; color: #9ca3af;">
Pinscope &middot; Agentic schematic validation
Periscope &middot; Agentic schematic validation
</td></tr>
</table>
</td></tr>
@@ -292,7 +292,7 @@ def _render_pipeline_started_email(
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 20px; font-weight: 700; color: #ffffff; letter-spacing: -0.025em;">
Pinscope
Periscope
</td>
<td align="right" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; color: #9ca3af; text-transform: uppercase; letter-spacing: 0.05em;">
Pipeline Started
@@ -398,7 +398,7 @@ def _render_pipeline_started_email(
<tr><td style="background-color: #f9fafb; padding: 20px 32px; border-top: 1px solid #e5e7eb;">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; color: #9ca3af;">
Pinscope &middot; Agentic schematic validation
Periscope &middot; Agentic schematic validation
</td></tr>
</table>
</td></tr>
@@ -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(
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 20px; font-weight: 700; color: #ffffff; letter-spacing: -0.025em;">
Pinscope
Periscope
</td>
<td align="right" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; color: #9ca3af; text-transform: uppercase; letter-spacing: 0.05em;">
Feedback Received
@@ -953,7 +953,7 @@ def _render_feedback_email(
<tr><td style="background-color: #f9fafb; padding: 20px 32px; border-top: 1px solid #e5e7eb;">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; color: #9ca3af;">
Pinscope &middot; Agentic schematic validation
Periscope &middot; Agentic schematic validation
</td></tr>
</table>
</td></tr>
@@ -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(
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 20px; font-weight: 700; color: #ffffff; letter-spacing: -0.025em;">
Pinscope
Periscope
</td>
<td align="right" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; color: #9ca3af; text-transform: uppercase; letter-spacing: 0.05em;">
New Reply
@@ -1113,7 +1113,7 @@ def _render_feedback_reply_email(
Hi {_esc(recipient_first_name)},
</td></tr>
<tr><td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; color: #374151; padding-bottom: 18px;">
The Pinscope team just replied to your feedback.
The Periscope team just replied to your feedback.
</td></tr>
{context_line}
@@ -1124,7 +1124,7 @@ def _render_feedback_reply_email(
<tr><td style="padding: 18px 22px;">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 11px; font-weight: 600; color: #047857; text-transform: uppercase; letter-spacing: 0.05em; padding-bottom: 10px;">
Pinscope team
Periscope team
</td></tr>
<tr><td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; color: #064e3b; line-height: 1.55; white-space: pre-wrap;">
{_esc(reply_text)}
@@ -1155,12 +1155,12 @@ def _render_feedback_reply_email(
<!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" href="{feedback_url}" style="height:48px;v-text-anchor:middle;width:240px;" arcsize="14%" fillcolor="#3b82f6" stroke="f">
<w:anchorlock/>
<center style="color:#ffffff;font-family:sans-serif;font-size:15px;font-weight:bold;">View in Pinscope &rarr;</center>
<center style="color:#ffffff;font-family:sans-serif;font-size:15px;font-weight:bold;">View in Periscope &rarr;</center>
</v:roundrect>
<![endif]-->
<!--[if !mso]><!-->
<a href="{feedback_url}" target="_blank" style="display: inline-block; background-color: #3b82f6; color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; font-weight: 600; text-decoration: none; padding: 12px 32px; border-radius: 8px; letter-spacing: -0.01em;">
View in Pinscope &rarr;
View in Periscope &rarr;
</a>
<!--<![endif]-->
</td></tr>
@@ -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.
</td></tr>
<tr><td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; color: #374151; padding-top: 6px;">
— The Pinscope team
— The Periscope team
</td></tr>
</table>
@@ -1180,7 +1180,7 @@ def _render_feedback_reply_email(
<tr><td style="background-color: #f9fafb; padding: 20px 32px; border-top: 1px solid #e5e7eb;">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; color: #9ca3af;">
Pinscope &middot; Agentic schematic validation
Periscope &middot; Agentic schematic validation
</td></tr>
</table>
</td></tr>
@@ -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"))
+7 -7
View File
@@ -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)
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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,
+3 -3
View File
@@ -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,
)
+1 -1
View File
@@ -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``.
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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<num>\d+(?:\.\d+)?)\s*(?P<mul>[pnuμµmk])?\s*[fF]\b",
+2 -2
View File
@@ -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)"
+2 -2
View File
@@ -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(
+20 -20
View File
@@ -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)
+5 -5
View File
@@ -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")
+15 -11
View File
@@ -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
+26 -26
View File
@@ -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