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:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
+4
-4
@@ -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,
|
||||
+10
-10
@@ -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."
|
||||
),
|
||||
@@ -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,4 +1,4 @@
|
||||
"""Local Pinscope auth endpoints (register / login / me)."""
|
||||
"""Local Periscope auth endpoints (register / login / me)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/"
|
||||
|
||||
@@ -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
@@ -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 · Agentic schematic validation
|
||||
Periscope · 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 · Agentic schematic validation
|
||||
Periscope · 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 · Agentic schematic validation
|
||||
Periscope · 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 →</center>
|
||||
<center style="color:#ffffff;font-family:sans-serif;font-size:15px;font-weight:bold;">View in Periscope →</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 →
|
||||
View in Periscope →
|
||||
</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 · Agentic schematic validation
|
||||
Periscope · 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"))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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``.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user