Rebrand Pinscope to Periscope across product and codebase.

Rename the core package to periscopex, update UI/docs/Docker/deploy defaults to periscope.michelebigi.it, and keep legacy version/storage key aliases so existing projects keep working.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-13 20:02:04 +02:00
co-authored by Cursor
parent 556306bf4d
commit 8d2b85600f
177 changed files with 869 additions and 766 deletions
+4 -4
View File
@@ -27,10 +27,10 @@ from typing import Any, Literal
from pydantic import BaseModel
from backend.config import settings
from backend.pinscopex.parsers import parse_bom
from backend.pinscopex.resolve_passives import resolve_mpn
from backend.pinscopex.taxonomy import SIMPLE_TYPES, type_for_ref
from backend.pinscopex.utils import safe_mpn
from backend.periscopex.parsers import parse_bom
from backend.periscopex.resolve_passives import resolve_mpn
from backend.periscopex.taxonomy import SIMPLE_TYPES, type_for_ref
from backend.periscopex.utils import safe_mpn
from backend.services import projects as proj_svc
from backend.services.billing_hook import get_billing
from backend.services.llm.pricing import CACHE_RATES, PRICING
+3 -3
View File
@@ -1,6 +1,6 @@
"""Automatic datasheet lookup — LCSC, manufacturer URLs, optional DigiKey.
DeepSeek-adapted Pinscope still needs the actual PDF. The original wizard
DeepSeek-adapted Periscope still needs the actual PDF. The original wizard
only auto-fetched via DigiKey, which requires paid API keys and often
fails when the manufacturer CDN blocks the download.
@@ -39,7 +39,7 @@ _PDF_MAGIC = b"%PDF-"
_MIN_PDF_SIZE = 5_000
_UA = (
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Pinscope/2.8"
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Periscope/2.8"
)
_LCSC_BASE = "https://wmsc.lcsc.com/ftps/wm"
@@ -154,7 +154,7 @@ def find_local_pdf(pdf_dir: Path, mpn: str) -> Path | None:
``ESP32-S31-WROOM-3`` matches ``ESP32-S31-WROOM-3-N16R16V.pdf`` and the
reverse — packing / flash-size suffixes, not sibling dies (CH340 vs CH340E).
"""
from backend.pinscopex.utils import safe_mpn
from backend.periscopex.utils import safe_mpn
if not mpn or not pdf_dir.is_dir():
return None
+1 -1
View File
@@ -16,7 +16,7 @@ from __future__ import annotations
import hashlib
from pathlib import Path
from backend.pinscopex.utils import safe_mpn
from backend.periscopex.utils import safe_mpn
from backend.services.storage import StorageBackend
BLOB_PREFIX = "library/datasheets/blobs/"
+1 -1
View File
@@ -25,7 +25,7 @@ import time
from datetime import datetime, timezone
from typing import Awaitable, Callable
from backend.pinscopex.models import Finding
from backend.periscopex.models import Finding
from backend.services.api_logs import ApiLogger
from backend.services.llm import Message, TextBlock
from backend.services.llm.factory import call_with_fallback
+34 -34
View File
@@ -174,7 +174,7 @@ def _render_report_email(
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 20px; font-weight: 700; color: #ffffff; letter-spacing: -0.025em;">
Pinscope
Periscope
</td>
<td align="right" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; color: #9ca3af; text-transform: uppercase; letter-spacing: 0.05em;">
Report Ready
@@ -247,7 +247,7 @@ def _render_report_email(
<tr><td style="background-color: #f9fafb; padding: 20px 32px; border-top: 1px solid #e5e7eb;">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; color: #9ca3af;">
Pinscope &middot; Agentic schematic validation
Periscope &middot; Agentic schematic validation
</td></tr>
</table>
</td></tr>
@@ -292,7 +292,7 @@ def _render_pipeline_started_email(
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 20px; font-weight: 700; color: #ffffff; letter-spacing: -0.025em;">
Pinscope
Periscope
</td>
<td align="right" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; color: #9ca3af; text-transform: uppercase; letter-spacing: 0.05em;">
Pipeline Started
@@ -398,7 +398,7 @@ def _render_pipeline_started_email(
<tr><td style="background-color: #f9fafb; padding: 20px 32px; border-top: 1px solid #e5e7eb;">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; color: #9ca3af;">
Pinscope &middot; Agentic schematic validation
Periscope &middot; Agentic schematic validation
</td></tr>
</table>
</td></tr>
@@ -448,7 +448,7 @@ def _build_report_message(
) -> MIMEMultipart:
"""Build the report-ready email message."""
msg = MIMEMultipart("alternative")
msg["From"] = f"Pinscope <{settings.email_sender}>"
msg["From"] = f"Periscope <{settings.email_sender}>"
msg["To"] = to_email
msg["Subject"] = f"Report ready: {project_name}"
@@ -460,7 +460,7 @@ def _build_report_message(
infos = summary.get("INFO", 0)
text_body = (
f"Hi {recipient_name},\n\n"
f"Your Pinscope validation report for \"{project_name}\" is ready.\n\n"
f"Your Periscope validation report for \"{project_name}\" is ready.\n\n"
f"Summary: {total} findings — {errors} errors, {warnings} warnings, {infos} info\n\n"
f"View the report: {report_url}\n"
)
@@ -485,7 +485,7 @@ def _build_paused_message(
credits_needed_low: float,
) -> MIMEMultipart:
msg = MIMEMultipart("alternative")
msg["From"] = f"Pinscope <{settings.email_sender}>"
msg["From"] = f"Periscope <{settings.email_sender}>"
msg["To"] = to_email
msg["Subject"] = f"Paused: {project_name} is waiting for credits"
@@ -495,7 +495,7 @@ def _build_paused_message(
text_body = (
f"Hi {recipient_name},\n\n"
f"Your Pinscope run for \"{project_name}\" paused because you're low on credits.\n\n"
f"Your Periscope run for \"{project_name}\" paused because you're low on credits.\n\n"
f"{last_line}\n{stage_line}\n\n"
f"Current balance: {balance:.2f} credits\n"
f"Credits needed to finish (est): {credits_needed_low:.2f}+\n\n"
@@ -510,13 +510,13 @@ def _build_topup_failed_message(
amount_usd: float, reason: str,
) -> MIMEMultipart:
msg = MIMEMultipart("alternative")
msg["From"] = f"Pinscope <{settings.email_sender}>"
msg["From"] = f"Periscope <{settings.email_sender}>"
msg["To"] = to_email
msg["Subject"] = "Pinscope: auto top-up failed"
msg["Subject"] = "Periscope: auto top-up failed"
manage_url = f"{settings.email_frontend_url}/credits"
text_body = (
f"Hi {recipient_name},\n\n"
f"We tried to auto top-up your Pinscope balance with "
f"We tried to auto top-up your Periscope balance with "
f"${amount_usd:.2f} but the charge failed.\n\n"
f"Reason: {reason}\n\n"
f"Auto top-up has been disabled until you update your payment method. "
@@ -549,13 +549,13 @@ def _build_low_balance_message(
to_email: str, recipient_name: str, balance: float, threshold: float,
) -> MIMEMultipart:
msg = MIMEMultipart("alternative")
msg["From"] = f"Pinscope <{settings.email_sender}>"
msg["From"] = f"Periscope <{settings.email_sender}>"
msg["To"] = to_email
msg["Subject"] = "Pinscope: low credit balance"
msg["Subject"] = "Periscope: low credit balance"
credits_url = f"{settings.email_frontend_url}/credits"
text_body = (
f"Hi {recipient_name},\n\n"
f"Your Pinscope credit balance has dropped to "
f"Your Periscope credit balance has dropped to "
f"{balance:.2f} credits (below your threshold of {threshold:.2f}).\n\n"
f"Top up here so your pipelines don't pause mid-run: {credits_url}\n"
)
@@ -691,10 +691,10 @@ async def send_test_email(to_email: str) -> dict:
result["step"] = "send"
msg = MIMEMultipart("alternative")
msg["From"] = f"Pinscope <{settings.email_sender}>"
msg["From"] = f"Periscope <{settings.email_sender}>"
msg["To"] = to_email
msg["Subject"] = "Pinscope email test"
msg.attach(MIMEText(f"Test email from Pinscope. Sender: {settings.email_sender}. Creds: {cred_type}", "plain"))
msg["Subject"] = "Periscope email test"
msg.attach(MIMEText(f"Test email from Periscope. Sender: {settings.email_sender}. Creds: {cred_type}", "plain"))
import asyncio as _asyncio
raw = base64.urlsafe_b64encode(msg.as_bytes()).decode("ascii")
@@ -741,7 +741,7 @@ async def send_pipeline_started_email(
# Build message
msg = MIMEMultipart("alternative")
msg["From"] = f"Pinscope <{settings.email_sender}>"
msg["From"] = f"Periscope <{settings.email_sender}>"
msg["To"] = to_email
msg["Subject"] = f"Pipeline started: {project_name} ({num_components} components)"
@@ -866,7 +866,7 @@ def _render_feedback_email(
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 20px; font-weight: 700; color: #ffffff; letter-spacing: -0.025em;">
Pinscope
Periscope
</td>
<td align="right" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; color: #9ca3af; text-transform: uppercase; letter-spacing: 0.05em;">
Feedback Received
@@ -953,7 +953,7 @@ def _render_feedback_email(
<tr><td style="background-color: #f9fafb; padding: 20px 32px; border-top: 1px solid #e5e7eb;">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; color: #9ca3af;">
Pinscope &middot; Agentic schematic validation
Periscope &middot; Agentic schematic validation
</td></tr>
</table>
</td></tr>
@@ -1006,7 +1006,7 @@ async def send_feedback_received_email(
to_email = settings.email_admin_notify
subject_ctx = project_name or "general"
msg = MIMEMultipart("alternative")
msg["From"] = f"Pinscope <{settings.email_sender}>"
msg["From"] = f"Periscope <{settings.email_sender}>"
msg["To"] = to_email
msg["Subject"] = f"Feedback ({type_label}): {subject_ctx}"
@@ -1095,7 +1095,7 @@ def _render_feedback_reply_email(
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 20px; font-weight: 700; color: #ffffff; letter-spacing: -0.025em;">
Pinscope
Periscope
</td>
<td align="right" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; color: #9ca3af; text-transform: uppercase; letter-spacing: 0.05em;">
New Reply
@@ -1113,7 +1113,7 @@ def _render_feedback_reply_email(
Hi {_esc(recipient_first_name)},
</td></tr>
<tr><td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; color: #374151; padding-bottom: 18px;">
The Pinscope team just replied to your feedback.
The Periscope team just replied to your feedback.
</td></tr>
{context_line}
@@ -1124,7 +1124,7 @@ def _render_feedback_reply_email(
<tr><td style="padding: 18px 22px;">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 11px; font-weight: 600; color: #047857; text-transform: uppercase; letter-spacing: 0.05em; padding-bottom: 10px;">
Pinscope team
Periscope team
</td></tr>
<tr><td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; color: #064e3b; line-height: 1.55; white-space: pre-wrap;">
{_esc(reply_text)}
@@ -1155,12 +1155,12 @@ def _render_feedback_reply_email(
<!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" href="{feedback_url}" style="height:48px;v-text-anchor:middle;width:240px;" arcsize="14%" fillcolor="#3b82f6" stroke="f">
<w:anchorlock/>
<center style="color:#ffffff;font-family:sans-serif;font-size:15px;font-weight:bold;">View in Pinscope &rarr;</center>
<center style="color:#ffffff;font-family:sans-serif;font-size:15px;font-weight:bold;">View in Periscope &rarr;</center>
</v:roundrect>
<![endif]-->
<!--[if !mso]><!-->
<a href="{feedback_url}" target="_blank" style="display: inline-block; background-color: #3b82f6; color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; font-weight: 600; text-decoration: none; padding: 12px 32px; border-radius: 8px; letter-spacing: -0.01em;">
View in Pinscope &rarr;
View in Periscope &rarr;
</a>
<!--<![endif]-->
</td></tr>
@@ -1170,7 +1170,7 @@ def _render_feedback_reply_email(
Thank you so much for taking the time to share your feedback — we truly value it.
</td></tr>
<tr><td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; color: #374151; padding-top: 6px;">
— The Pinscope team
— The Periscope team
</td></tr>
</table>
@@ -1180,7 +1180,7 @@ def _render_feedback_reply_email(
<tr><td style="background-color: #f9fafb; padding: 20px 32px; border-top: 1px solid #e5e7eb;">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; color: #9ca3af;">
Pinscope &middot; Agentic schematic validation
Periscope &middot; Agentic schematic validation
</td></tr>
</table>
</td></tr>
@@ -1203,7 +1203,7 @@ async def send_feedback_reply_email(
finding_designator: str | None = None,
finding_mpn: str | None = None,
) -> None:
"""Notify the original submitter that the Pinscope team replied. Fire-and-forget."""
"""Notify the original submitter that the Periscope team replied. Fire-and-forget."""
if not settings.use_email:
return
@@ -1229,15 +1229,15 @@ async def send_feedback_reply_email(
first_name = full_name.split()[0] if full_name else "there"
msg = MIMEMultipart("alternative")
msg["From"] = f"Pinscope <{settings.email_sender}>"
msg["From"] = f"Periscope <{settings.email_sender}>"
msg["To"] = to_email
msg["Subject"] = "The Pinscope team replied to your feedback"
msg["Subject"] = "The Periscope team replied to your feedback"
# Plain text fallback
text_lines = [
f"Hi {first_name},",
"",
"The Pinscope team just replied to your feedback.",
"The Periscope team just replied to your feedback.",
"",
"— Reply —",
reply_text,
@@ -1245,10 +1245,10 @@ async def send_feedback_reply_email(
"— Your original message —",
original_message,
"",
f"View in Pinscope: {settings.email_frontend_url}/feedback",
f"View in Periscope: {settings.email_frontend_url}/feedback",
"",
"Thank you so much for taking the time to share your feedback — we truly value it.",
"— The Pinscope team",
"— The Periscope team",
]
msg.attach(MIMEText("\n".join(text_lines), "plain"))
+7 -7
View File
@@ -17,8 +17,8 @@ import tempfile
import time
from pathlib import Path
from backend.pinscopex.utils import safe_mpn
from backend.pinscopex.models import (
from backend.periscopex.utils import safe_mpn
from backend.periscopex.models import (
CapacitorSpecs,
ComponentConstraints,
ComponentModel,
@@ -27,7 +27,7 @@ from backend.pinscopex.models import (
NetType,
SimpleComponentSpecs,
)
from backend.pinscopex.taxonomy import (
from backend.periscopex.taxonomy import (
TAXONOMY_DIR,
add_subtype,
format_for_prompt,
@@ -399,13 +399,13 @@ def _coerce_abs_max(raw: object) -> list[dict]:
def _coerce_layout_rules(raw: object) -> list[dict]:
from backend.pinscopex.layout_rules import validate_layout_rules
from backend.periscopex.layout_rules import validate_layout_rules
rows, _errors = validate_layout_rules(raw if isinstance(raw, list) else [])
return rows
def _coerce_internal_features(raw: object):
from backend.pinscopex.models import InternalFeatures
from backend.periscopex.models import InternalFeatures
if not isinstance(raw, dict):
return None
try:
@@ -1088,7 +1088,7 @@ async def auto_resolve_specs(
# Convert passive SimpleComponentSpecs to typed models
if component_type == "passive":
from backend.pinscopex.resolve_passives import simple_to_typed_passive_specs
from backend.periscopex.resolve_passives import simple_to_typed_passive_specs
typed = simple_to_typed_passive_specs(specs)
return ComponentModel(mpn=mpn, specs=typed)
@@ -1260,7 +1260,7 @@ async def resolve_from_value(
)
if component_type == "passive":
from backend.pinscopex.resolve_passives import simple_to_typed_passive_specs
from backend.periscopex.resolve_passives import simple_to_typed_passive_specs
typed = simple_to_typed_passive_specs(specs)
return ComponentModel(mpn=mpn, specs=typed)
+1 -1
View File
@@ -62,7 +62,7 @@ def load_skill_validator(skill_name: str):
if not path.is_file():
return None
spec = importlib.util.spec_from_file_location(
f"pinscope_skill_{skill_name.replace('-', '_')}_validate", path,
f"periscope_skill_{skill_name.replace('-', '_')}_validate", path,
)
if spec is None or spec.loader is None:
return None
+1 -1
View File
@@ -15,7 +15,7 @@ import logging
import re
from pathlib import Path
from backend.pinscopex.pdf_text import (
from backend.periscopex.pdf_text import (
extract_pdf_document_text,
fitz_page_text,
page_is_sparse,
+3 -3
View File
@@ -1,4 +1,4 @@
"""Pinscope local JWT helpers (HS256)."""
"""Periscope local JWT helpers (HS256)."""
from __future__ import annotations
@@ -21,7 +21,7 @@ def issue_token(user_id: str, email: str) -> str:
payload = {
"sub": user_id,
"email": email,
"iss": "pinscope-local",
"iss": "periscope-local",
"iat": now,
"exp": now + timedelta(days=TOKEN_TTL_DAYS),
}
@@ -37,7 +37,7 @@ def decode_token(token: str) -> dict[str, Any] | None:
token,
secret,
algorithms=[ALGORITHM],
issuer="pinscope-local",
issuer="periscope-local",
options={"verify_aud": False},
leeway=10,
)
+1 -1
View File
@@ -1,4 +1,4 @@
"""Local Pinscope user directory (self-host auth, no Clerk).
"""Local Periscope user directory (self-host auth, no Clerk).
Users live under ``data/auth/users/{user_id}.json`` with an email index.
Passwords use stdlib ``hashlib.scrypt``.
+1 -1
View File
@@ -23,7 +23,7 @@ from datetime import datetime, timezone
from typing import Awaitable, Callable
from backend.config import settings
from backend.pinscopex.models import Finding
from backend.periscopex.models import Finding
from backend.services.api_logs import ApiLogger
from backend.services.llm import Message, TextBlock
from backend.services.llm.factory import call_with_fallback
+2 -2
View File
@@ -8,8 +8,8 @@ from __future__ import annotations
import re
from backend.pinscopex.models import ComponentModel, SimpleComponentSpecs
from backend.pinscopex.resolve_passives import simple_to_typed_passive_specs
from backend.periscopex.models import ComponentModel, SimpleComponentSpecs
from backend.periscopex.resolve_passives import simple_to_typed_passive_specs
_CAP = re.compile(
r"(?P<num>\d+(?:\.\d+)?)\s*(?P<mul>[pnuμµmk])?\s*[fF]\b",
+2 -2
View File
@@ -9,8 +9,8 @@ from __future__ import annotations
import re
from backend.pinscopex.models import ComponentModel, SimpleComponentSpecs
from backend.pinscopex.resolve_passives import simple_to_typed_passive_specs
from backend.periscopex.models import ComponentModel, SimpleComponentSpecs
from backend.periscopex.resolve_passives import simple_to_typed_passive_specs
from backend.services.passive_from_distributor import _spice
_SIZE = r"(?:0201|0402|0603|0805|1206|1210|1812|2010|2512)"
+2 -2
View File
@@ -4,8 +4,8 @@ from __future__ import annotations
import re
from backend.pinscopex.models import ComponentModel, SimpleComponentSpecs
from backend.pinscopex.resolve_passives import simple_to_typed_passive_specs
from backend.periscopex.models import ComponentModel, SimpleComponentSpecs
from backend.periscopex.resolve_passives import simple_to_typed_passive_specs
from backend.services.passive_from_distributor import _spice
_PLACEHOLDER = re.compile(
+20 -20
View File
@@ -33,15 +33,15 @@ from pathlib import Path
from typing import Any, Awaitable, Callable
from backend.pinscopex.models import ComponentType
from backend.pinscopex.utils import natural_sort_key, safe_mpn
from backend.pinscopex.bom_summary import build_bom_summary
from backend.pinscopex.derating import build_derating_table
from backend.pinscopex.validate import _load_datasheets
from backend.pinscopex.graph import build_graph
from backend.pinscopex.parsers import parse_bom, parse_netlist_any
from backend.pinscopex.resolve_passives import SkippedItem, load_patterns, resolve_mpn
from backend.pinscopex.taxonomy import SIMPLE_TYPES, type_for_ref
from backend.periscopex.models import ComponentType
from backend.periscopex.utils import natural_sort_key, safe_mpn
from backend.periscopex.bom_summary import build_bom_summary
from backend.periscopex.derating import build_derating_table
from backend.periscopex.validate import _load_datasheets
from backend.periscopex.graph import build_graph
from backend.periscopex.parsers import parse_bom, parse_netlist_any
from backend.periscopex.resolve_passives import SkippedItem, load_patterns, resolve_mpn
from backend.periscopex.taxonomy import SIMPLE_TYPES, type_for_ref
from backend.config import settings
from backend.services import admin_settings as settings_svc
@@ -196,7 +196,7 @@ def _cancel_gate_check(ctx: PipelineContext) -> None:
class PipelineWorkspace:
"""Downloads project files from storage to a temp dir for pipeline execution.
The pinscopex core library operates on local paths. This context manager
The periscopex core library operates on local paths. This context manager
downloads inputs at enter, provides local paths, and uploads results at exit.
"""
@@ -264,7 +264,7 @@ class PipelineWorkspace:
self._upload_file("bom_summary.json")
self._upload_file("derating.json")
self._upload_file("report.json")
self._upload_file("pinscope-findings.json")
self._upload_file("periscope-findings.json")
self._upload_file("review_fingerprints.json")
self._upload_file("api_logs.jsonl")
@@ -763,7 +763,7 @@ async def _stage_ic_extraction(ctx: PipelineContext) -> None:
# Pre-categorize: workspace cache, library cache, or needs extraction
from backend.config import settings as app_settings
from backend.pinscopex.layout_rules import needs_layout_rules_refresh
from backend.periscopex.layout_rules import needs_layout_rules_refresh
layout_scan_ver = app_settings.get_default_model_version()
_ic_cache: dict[str, tuple] = {}
@@ -863,7 +863,7 @@ async def _stage_ic_extraction(ctx: PipelineContext) -> None:
extracted_key = f"{ctx.ws.prefix}/extracted/{safe}.json"
ctx.storage.upload_from_local(json_path, extracted_key)
try:
from backend.pinscopex.library_gate import should_promote_extraction
from backend.periscopex.library_gate import should_promote_extraction
payload = json.loads(json_path.read_text(encoding="utf-8"))
ok, reason = should_promote_extraction(payload)
@@ -1545,7 +1545,7 @@ def _write_layout_graph(ws: PipelineWorkspace, project_id: str) -> None:
if not pcb.is_file():
return
try:
from backend.pinscopex.parsers_kicad_pcb import parse_kicad_pcb
from backend.periscopex.parsers_kicad_pcb import parse_kicad_pcb
layout = parse_kicad_pcb(pcb)
out = ws.local_path("layout_graph.json")
@@ -1561,8 +1561,8 @@ def _write_layout_graph(ws: PipelineWorkspace, project_id: str) -> None:
def _write_functional_groups(ws: PipelineWorkspace, graph) -> None:
"""Layout F1: topology domains/groups (no mm). Fail-soft."""
try:
from backend.pinscopex.functional_groups import build_functional_groups
from backend.pinscopex.validate import _build_constraints_map, _load_datasheets
from backend.periscopex.functional_groups import build_functional_groups
from backend.periscopex.validate import _build_constraints_map, _load_datasheets
extracted_dir = ws.local_path("extracted")
cmap = {}
@@ -1584,8 +1584,8 @@ def _write_impedance_nets(ws: PipelineWorkspace, graph) -> None:
if not path.is_file():
return
try:
from backend.pinscopex.impedance_traces import analyze_where_needed
from backend.pinscopex.models import LayoutGraph
from backend.periscopex.impedance_traces import analyze_where_needed
from backend.periscopex.models import LayoutGraph
layout = LayoutGraph.model_validate_json(path.read_text())
report = analyze_where_needed(layout, graph)
@@ -1792,11 +1792,11 @@ async def _stage_validation(ctx: PipelineContext) -> None:
fp_path = ctx.ws.local_path("review_fingerprints.json")
current_fp: dict[str, str] = {}
try:
from backend.pinscopex.review_fingerprint import (
from backend.periscopex.review_fingerprint import (
graph_ic_fingerprints,
skip_unchanged_ics,
)
from backend.pinscopex.validate import _build_constraints_map, _load_datasheets
from backend.periscopex.validate import _build_constraints_map, _load_datasheets
cmap = _build_constraints_map(_load_datasheets(extracted_dir))
current_fp = graph_ic_fingerprints(ctx.graph, cmap)
+5 -5
View File
@@ -11,10 +11,10 @@ from __future__ import annotations
import logging
from pathlib import Path
from backend.pinscopex.functional_groups import build_placement_plan
from backend.pinscopex.graph import build_graph
from backend.pinscopex.models import ComponentConstraints, DesignGraph, LayoutGraph
from backend.pinscopex.placement_pack import build_placement_pack
from backend.periscopex.functional_groups import build_placement_plan
from backend.periscopex.graph import build_graph
from backend.periscopex.models import ComponentConstraints, DesignGraph, LayoutGraph
from backend.periscopex.placement_pack import build_placement_pack
from backend.services import projects as proj_svc
from backend.services.pipeline import PipelineWorkspace, broker
from backend.services.storage import StorageBackend
@@ -218,7 +218,7 @@ def _load_layout(ws: PipelineWorkspace) -> LayoutGraph | None:
if not pcb.is_file():
return None
try:
from backend.pinscopex.parsers_kicad_pcb import parse_kicad_pcb
from backend.periscopex.parsers_kicad_pcb import parse_kicad_pcb
layout = parse_kicad_pcb(pcb)
cached.write_text(layout.model_dump_json(indent=2) + "\n")
+15 -11
View File
@@ -11,7 +11,7 @@ Each project lives at users/{user_id}/projects/{id}/ with:
models/ cached component specs
design_graph.json graph output
report.json validation report
pinscope-findings.json KiCad cad-bridge (plugin pan-and-zoom)
periscope-findings.json KiCad cad-bridge (plugin pan-and-zoom)
Library (global, shared across users):
library/extracted/{mpn}.json
@@ -31,9 +31,9 @@ from typing import Any
log = logging.getLogger(__name__)
from pydantic import BaseModel
from pydantic import AliasChoices, BaseModel, Field
from backend.pinscopex.utils import safe_mpn
from backend.periscopex.utils import safe_mpn
from backend.services.storage import StaleGeneration, StorageBackend
@@ -120,9 +120,13 @@ class ProjectMeta(BaseModel):
pause_reason: str | None = None
completed_review_refs: list[str] = [] # IC refs already reviewed (persists across pauses)
# Pinscope app version that generated the project's report.
# Periscope app version that generated the project's report.
# Stamped on the first /start transition and preserved thereafter.
pinscope_version: str | None = None
# Accept legacy pinscope_version from project.json written before the rebrand.
periscope_version: str | None = Field(
default=None,
validation_alias=AliasChoices("periscope_version", "pinscope_version"),
)
# Worker bookkeeping (set by the API on enqueue, read by /events SSE
# and by the stale-running sweeper).
@@ -166,7 +170,7 @@ def completed_review_refs_for_retry(
for ref in (report.get("review_errors") or {}):
if ref:
failed.add(str(ref))
from backend.pinscopex.utils import natural_sort_key
from backend.periscopex.utils import natural_sort_key
kept = [r for r in (meta.completed_review_refs or []) if r and r not in failed]
return sorted(kept, key=natural_sort_key)
@@ -484,7 +488,7 @@ def clear_project_extractions(
"bom_summary.json",
"derating.json",
"report.json",
"pinscope-findings.json",
"periscope-findings.json",
"review_fingerprints.json",
"api_logs.jsonl",
"graph_voltage_updates.json",
@@ -519,7 +523,7 @@ def reopen_project(
"bom_summary.json",
"derating.json",
"report.json",
"pinscope-findings.json",
"periscope-findings.json",
"review_fingerprints.json",
"api_logs.jsonl",
"graph_voltage_updates.json",
@@ -936,7 +940,7 @@ def library_has_datasheet(
return key
# 3. Pattern-based fallback for passives
if patterns:
from backend.pinscopex.resolve_passives import resolve_mpn
from backend.periscopex.resolve_passives import resolve_mpn
match = resolve_mpn(mpn, patterns)
if match is not None:
@@ -1121,11 +1125,11 @@ def list_library_patterns(storage: StorageBackend) -> list[str]:
def load_library_patterns(storage: StorageBackend):
"""Load and parse all passive patterns from the library.
For local backend, delegates to pinscopex. For GCS, downloads to temp first.
For local backend, delegates to periscopex. For GCS, downloads to temp first.
This function is only used by the library/check endpoint during pipeline
execution, patterns are loaded from the workspace temp directory.
"""
from backend.pinscopex.resolve_passives import load_patterns
from backend.periscopex.resolve_passives import load_patterns
from backend.services.storage import LocalStorageBackend
+26 -26
View File
@@ -21,7 +21,7 @@ from typing import Awaitable, Callable
log = logging.getLogger(__name__)
from backend.pinscopex.models import (
from backend.periscopex.models import (
ComponentConstraints,
ComponentType,
DesignGraph,
@@ -30,7 +30,7 @@ from backend.pinscopex.models import (
NetType,
ValidationReport,
)
from backend.pinscopex.validate import (
from backend.periscopex.validate import (
SYSTEM_PROMPT,
_MAX_REVIEW_TURNS,
ReviewResult,
@@ -41,30 +41,30 @@ from backend.pinscopex.validate import (
build_component_context,
_parse_review,
)
from backend.pinscopex.quote_verify import verify_finding_citations
from backend.pinscopex.utils import safe_mpn
from backend.pinscopex.pin_mux_check import check_pin_mux_feasibility
from backend.pinscopex.led_current_check import check_led_current
from backend.pinscopex.passive_rail_check import (
from backend.periscopex.quote_verify import verify_finding_citations
from backend.periscopex.utils import safe_mpn
from backend.periscopex.pin_mux_check import check_pin_mux_feasibility
from backend.periscopex.led_current_check import check_led_current
from backend.periscopex.passive_rail_check import (
check_i2c_pullups,
check_reset_pullups,
check_supply_decoupling,
)
from backend.pinscopex.bom_match_check import check_bom_schematic_match
from backend.pinscopex.hf_coverage_check import check_hf_decoupling_coverage
from backend.pinscopex.cad_bridge import annotate_findings_cad, build_cad_bridge, write_cad_bridge
from backend.pinscopex.filter_check import check_filters
from backend.pinscopex.thermal_check import check_thermal
from backend.pinscopex.power_margin_check import check_power_margin
from backend.pinscopex.sequencing_check import check_power_sequencing
from backend.pinscopex.dnp_check import check_dnp_enables
from backend.pinscopex.lifecycle import check_lifecycle, load_lifecycle_dir
from backend.pinscopex.errata_check import check_errata
from backend.pinscopex.internal_features_check import check_internal_features
from backend.pinscopex.placement_check import check_placement
from backend.pinscopex.si_check import check_si
from backend.pinscopex.crystal_cl_check import check_crystal_cl
from backend.pinscopex.nc_pin_check import check_nc_pins
from backend.periscopex.bom_match_check import check_bom_schematic_match
from backend.periscopex.hf_coverage_check import check_hf_decoupling_coverage
from backend.periscopex.cad_bridge import annotate_findings_cad, build_cad_bridge, write_cad_bridge
from backend.periscopex.filter_check import check_filters
from backend.periscopex.thermal_check import check_thermal
from backend.periscopex.power_margin_check import check_power_margin
from backend.periscopex.sequencing_check import check_power_sequencing
from backend.periscopex.dnp_check import check_dnp_enables
from backend.periscopex.lifecycle import check_lifecycle, load_lifecycle_dir
from backend.periscopex.errata_check import check_errata
from backend.periscopex.internal_features_check import check_internal_features
from backend.periscopex.placement_check import check_placement
from backend.periscopex.si_check import check_si
from backend.periscopex.crystal_cl_check import check_crystal_cl
from backend.periscopex.nc_pin_check import check_nc_pins
TRACE_VERSION = 1
@@ -139,14 +139,14 @@ def _assistant_text(blocks) -> str:
except Exception:
log.exception("trace: assistant_text extraction failed")
return "\n".join(parts)
from backend.pinscopex.validation_tools import (
from backend.periscopex.validation_tools import (
ALL_TOOLS,
SUBMIT_REVIEW_SCHEMA,
ConstraintsMap,
ExcerptState,
execute_tool,
)
from backend.pinscopex.utils import safe_mpn
from backend.periscopex.utils import safe_mpn
from backend.config import settings
from backend.services.api_logs import ApiLogger
@@ -639,7 +639,7 @@ def _find_pdf(
then tries to download from the library.
"""
from backend.services.datasheet_finder import find_local_pdf
from backend.pinscopex.utils import safe_mpn as _safe
from backend.periscopex.utils import safe_mpn as _safe
mpn = (mpn or "").strip()
if not mpn:
@@ -836,7 +836,7 @@ async def validate_design_async(
try:
prefix_id = (project_prefix or "").rstrip("/").rsplit("/", 1)[-1]
bridge = build_cad_bridge(report, prefix_id or report.project)
write_cad_bridge(existing_path.with_name("pinscope-findings.json"), bridge)
write_cad_bridge(existing_path.with_name("periscope-findings.json"), bridge)
except Exception:
log.exception("cad bridge write failed")
return report