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,6 +1,6 @@
|
|||||||
# Pinscope — Agentic Schematic Validation
|
# Periscope — Agentic Schematic Validation
|
||||||
|
|
||||||
Pinscope validates hardware schematics against component datasheets. It extracts constraints from PDFs, parses netlists and BOMs into a queryable graph, and runs an agentic validation loop to flag design violations.
|
Periscope validates hardware schematics against component datasheets. It extracts constraints from PDFs, parses netlists and BOMs into a queryable graph, and runs an agentic validation loop to flag design violations.
|
||||||
|
|
||||||
> **Open-core note.** This is the open-source core. A small set of files are
|
> **Open-core note.** This is the open-source core. A small set of files are
|
||||||
> "gateway-owned seams" — pass-through stubs here (`frontend/src/proxy.ts`,
|
> "gateway-owned seams" — pass-through stubs here (`frontend/src/proxy.ts`,
|
||||||
@@ -18,7 +18,7 @@ Three layers:
|
|||||||
|
|
||||||
| Layer | Location | Purpose |
|
| Layer | Location | Purpose |
|
||||||
|-------|----------|---------|
|
|-------|----------|---------|
|
||||||
| **Core library** | `backend/pinscopex/` | Models, parsers, graph builder, agentic validator, passive resolver, taxonomy, BOM summary, derating |
|
| **Core library** | `backend/periscopex/` | Models, parsers, graph builder, agentic validator, passive resolver, taxonomy, BOM summary, derating |
|
||||||
| **Backend** | `backend/` | FastAPI app — async pipeline orchestration, SSE progress, project/file storage |
|
| **Backend** | `backend/` | FastAPI app — async pipeline orchestration, SSE progress, project/file storage |
|
||||||
| **Frontend** | `frontend/` | Next.js 16 app — project dashboard, pipeline progress, report viewer, derating, admin dashboard |
|
| **Frontend** | `frontend/` | Next.js 16 app — project dashboard, pipeline progress, report viewer, derating, admin dashboard |
|
||||||
|
|
||||||
@@ -55,17 +55,17 @@ Files: `.asc` (PADS-PCB netlist; `.edn` EDIF 2.0.0 also accepted), `.csv`/`.xlsx
|
|||||||
- **Cross-IC finding dedup** — After all per-IC reviews complete, a single pass (`services/dedupe_findings.py`) collapses one physical interface defect reported from both endpoints into a single finding. Gated by `cross_ic_dedup_enabled`; fail-soft.
|
- **Cross-IC finding dedup** — After all per-IC reviews complete, a single pass (`services/dedupe_findings.py`) collapses one physical interface defect reported from both endpoints into a single finding. Gated by `cross_ic_dedup_enabled`; fail-soft.
|
||||||
- **Capacitor voltage derating** — Deterministic derating table computed from graph (ceramic/tantalum/electrolytic percentages, pass/fail per capacitor)
|
- **Capacitor voltage derating** — Deterministic derating table computed from graph (ceramic/tantalum/electrolytic percentages, pass/fail per capacitor)
|
||||||
- **Deterministic checks over heuristics** — Exact checks where possible
|
- **Deterministic checks over heuristics** — Exact checks where possible
|
||||||
- **Zero coupling between layers** — Backend calls pinscopex functions with paths; frontend talks to backend via REST + SSE
|
- **Zero coupling between layers** — Backend calls periscopex functions with paths; frontend talks to backend via REST + SSE
|
||||||
- **Library deduplication** — Shared library (`library/extracted/`, `library/patterns/`, `library/models/`, `library/passives/`, `library/datasheets/`) caches extractions across projects
|
- **Library deduplication** — Shared library (`library/extracted/`, `library/patterns/`, `library/models/`, `library/passives/`, `library/datasheets/`) caches extractions across projects
|
||||||
- **Content-addressed datasheets** — `library/datasheets/blobs/{md5}.pdf` stores unique PDFs once; `library/datasheets/refs/{safe_mpn}.json` maps MPNs to blobs (dedupe + multi-MPN sharing)
|
- **Content-addressed datasheets** — `library/datasheets/blobs/{md5}.pdf` stores unique PDFs once; `library/datasheets/refs/{safe_mpn}.json` maps MPNs to blobs (dedupe + multi-MPN sharing)
|
||||||
- **Taxonomy-driven extraction** — Living component taxonomy (`taxonomy/`) with per-subtype classification and specs schemas
|
- **Taxonomy-driven extraction** — Living component taxonomy (`taxonomy/`) with per-subtype classification and specs schemas
|
||||||
- **Per-stage model config** — Each pipeline stage can use a different Claude model (e.g., Sonnet for review, Haiku for auto-resolve)
|
- **Per-stage model config** — Each pipeline stage can use a different Claude model (e.g., Sonnet for review, Haiku for auto-resolve)
|
||||||
- **API call logging** — Every Claude API call is logged with token counts, cost, and timing per pipeline run
|
- **API call logging** — Every Claude API call is logged with token counts, cost, and timing per pipeline run
|
||||||
- **Report versioning** — Each project run is stamped with the current app version on the first `/start` transition (`ProjectMeta.pinscope_version`). The version comes from `frontend/content/changelog.md`'s latest `##` heading — single source of truth — read at backend startup via `backend/_version.py`.
|
- **Report versioning** — Each project run is stamped with the current app version on the first `/start` transition (`ProjectMeta.periscope_version`). The version comes from `frontend/content/changelog.md`'s latest `##` heading — single source of truth — read at backend startup via `backend/_version.py`.
|
||||||
|
|
||||||
## Datasheet Extraction
|
## Datasheet Extraction
|
||||||
|
|
||||||
Extracted data lives in `library/extracted/` (shared) or per-project under the storage backend. One JSON per MPN, schema in `backend/pinscopex/models.py`.
|
Extracted data lives in `library/extracted/` (shared) or per-project under the storage backend. One JSON per MPN, schema in `backend/periscopex/models.py`.
|
||||||
|
|
||||||
Per-MPN IC extraction captures:
|
Per-MPN IC extraction captures:
|
||||||
1. **Pintable** — Pin number + name (required), description + alt functions (optional)
|
1. **Pintable** — Pin number + name (required), description + alt functions (optional)
|
||||||
@@ -131,8 +131,8 @@ All `ComponentConstraints` extracted JSON files carry a `model_version` semver f
|
|||||||
|
|
||||||
- Write tests against `simple_project/` — it's the ground truth
|
- Write tests against `simple_project/` — it's the ground truth
|
||||||
- Netlist parser and BOM parser are pure functions with no side effects
|
- Netlist parser and BOM parser are pure functions with no side effects
|
||||||
- All data structures use Pydantic models in `backend/pinscopex/models.py`
|
- All data structures use Pydantic models in `backend/periscopex/models.py`
|
||||||
- Frontend types in `frontend/src/lib/types.ts` must stay in sync with `backend/pinscopex/models.py`
|
- Frontend types in `frontend/src/lib/types.ts` must stay in sync with `backend/periscopex/models.py`
|
||||||
- Extraction prompts live in `skills/` (SKILL.md + schema.json + validate.py) and run locally against DeepSeek
|
- Extraction prompts live in `skills/` (SKILL.md + schema.json + validate.py) and run locally against DeepSeek
|
||||||
- **Never swallow exceptions silently** — prefer logging or re-raising over bare `except: continue`. Silent failures hide real bugs.
|
- **Never swallow exceptions silently** — prefer logging or re-raising over bare `except: continue`. Silent failures hide real bugs.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Pinscope (DeepSeek)
|
# Periscope (DeepSeek)
|
||||||
|
|
||||||
Pinscope reviews schematics the way a good senior engineer does: with the datasheets open.
|
Periscope reviews schematics the way a good senior engineer does: with the datasheets open.
|
||||||
|
|
||||||
This tree is adapted from [manvalan/pinscope](https://github.com/manvalan/pinscope) so the pipeline talks to the **DeepSeek API** (`deepseek-flash`, with legacy aliases still accepted). Do not use Anthropic.
|
This tree is adapted from [manvalan/pinscope](https://github.com/manvalan/pinscope) so the pipeline talks to the **DeepSeek API** (`deepseek-flash`, with legacy aliases still accepted). Do not use Anthropic.
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ Give it a netlist, a BOM, and your datasheet PDFs. It builds a graph of your des
|
|||||||
|
|
||||||
## What changed for DeepSeek
|
## What changed for DeepSeek
|
||||||
|
|
||||||
DeepSeek's Chat Completions API is OpenAI-compatible but **does not accept native PDF documents**. Pinscope therefore:
|
DeepSeek's Chat Completions API is OpenAI-compatible but **does not accept native PDF documents**. Periscope therefore:
|
||||||
|
|
||||||
1. **Extracts datasheet text** with `pypdf` (page-marked) and sends it as chat content.
|
1. **Extracts datasheet text** with `pypdf` (page-marked) and sends it as chat content.
|
||||||
2. **Renders pages to JPEG** with PyMuPDF when the stage uses a vision model, so pin diagrams and tables survive.
|
2. **Renders pages to JPEG** with PyMuPDF when the stage uses a vision model, so pin diagrams and tables survive.
|
||||||
@@ -66,18 +66,18 @@ docker compose up --build
|
|||||||
|
|
||||||
Backend on port 8080, frontend on port 3000.
|
Backend on port 8080, frontend on port 3000.
|
||||||
|
|
||||||
### Update a live instance (e.g. pinscope.michelebigi.it)
|
### Update a live instance (e.g. periscope.michelebigi.it)
|
||||||
|
|
||||||
On the server, from the Pinscope checkout:
|
On the server, from the Periscope checkout:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/update-pinscope.sh
|
./scripts/update-periscope.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
The script pulls the current branch, writes `NEXT_PUBLIC_API_URL` / `CORS_ORIGINS` for `https://pinscope.michelebigi.it`, rebuilds both Docker images, and leaves `data/` alone. First run: put `DEEPSEEK_API_KEY` in `.env` at the repo root (compose reads that file). `--no-pull` skips git. `SITE=https://other.host ./scripts/update-pinscope.sh` overrides the public URL.
|
The script pulls the current branch, writes `NEXT_PUBLIC_API_URL` / `CORS_ORIGINS` for `https://periscope.michelebigi.it`, rebuilds both Docker images, and leaves `data/` alone. First run: put `DEEPSEEK_API_KEY` in `.env` at the repo root (compose reads that file). `--no-pull` skips git. `SITE=https://other.host ./scripts/update-periscope.sh` overrides the public URL.
|
||||||
|
|
||||||
Do not set `ENVIRONMENT=production` unless Clerk auth is configured — that flag refuses to boot with auth disabled.
|
Do not set `ENVIRONMENT=production` unless Clerk auth is configured — that flag refuses to boot with auth disabled.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
AGPL-3.0, same as upstream Pinscope. For commercial licensing of the original, write to dev@faradworks.com.
|
AGPL-3.0, same as upstream Pinscope (Faradworks). For commercial licensing of the original, write to dev@faradworks.com.
|
||||||
|
|||||||
@@ -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.
|
# Copy to backend/.env and fill in values. Only DEEPSEEK_API_KEY is required.
|
||||||
|
|
||||||
# -- AI (DeepSeek, default) --------------------------------------------------
|
# -- 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"]
|
CORS_ORIGINS=["http://localhost:3000","http://127.0.0.1:3000","http://localhost:18742","http://127.0.0.1:18742"]
|
||||||
|
|
||||||
# -- Auth (self-host) --------------------------------------------------------
|
# -- 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,
|
# multi-user project collaborators (invite by email). Clerk keys, if set,
|
||||||
# take priority over local auth.
|
# take priority over local auth.
|
||||||
# AUTH_JWT_SECRET=change-me-to-a-long-random-string
|
# 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
|
## Running
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ backend/
|
|||||||
├── _version.py # Reads app version from frontend/content/changelog.md (single source of truth)
|
├── _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
|
├── Dockerfile # Python 3.12-slim, copies taxonomy/ + changelog.md for runtime
|
||||||
├── skills_manifest.json # Claude Console Skill IDs (extract-pintable, extract-pattern, extract-specs)
|
├── 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()
|
│ ├── utils.py # Shared utilities: safe_mpn(), natural_sort_key()
|
||||||
│ └── resolve_passives.py # Passive MPN pattern matching + value decoders (R/C/L)
|
│ └── resolve_passives.py # Passive MPN pattern matching + value decoders (R/C/L)
|
||||||
├── middleware/
|
├── 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`
|
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
|
## 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. **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.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.
|
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)
|
5. **BOM Summary** — Collate components from design graph (no AI)
|
||||||
6. **Derating Table** — Capacitor voltage derating computation (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.
|
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
|
## Key Patterns
|
||||||
|
|
||||||
- **StorageBackend protocol** — all file I/O is abstracted; swap local/GCS via `GCS_BUCKET` env var
|
- **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.
|
- **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)
|
- **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
|
- **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`)
|
- **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
|
- **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)
|
- **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
|
- **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
|
- **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
|
- **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
|
- **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`)
|
- Keep all storage operations in `services/projects.py` (uses `StorageBackend`)
|
||||||
- Routers are thin — validate input, call service, return response
|
- Routers are thin — validate input, call service, return response
|
||||||
- Thread `user_id` from `request.state` through to all service calls
|
- 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
|
- 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
|
# Extraction skills (SKILL.md + validate.py) — required for DeepSeek/Gemini
|
||||||
COPY skills/ /app/skills/
|
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
|
COPY frontend/content/changelog.md /app/changelog.md
|
||||||
|
|
||||||
# ImpedenceFinder closed-form engine (no OpenEMS / pcbnew).
|
# 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 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
|
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)
|
@lru_cache(maxsize=1)
|
||||||
def get_pinscope_version() -> str:
|
def get_periscope_version() -> str:
|
||||||
for path in _candidate_paths():
|
for path in _candidate_paths():
|
||||||
try:
|
try:
|
||||||
text = path.read_text(encoding="utf-8")
|
text = path.read_text(encoding="utf-8")
|
||||||
@@ -35,4 +35,4 @@ def get_pinscope_version() -> str:
|
|||||||
return "unknown"
|
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_publishable_key: str = ""
|
||||||
clerk_jwks_url: 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.
|
# accounts and multi-user project collaborators without Clerk.
|
||||||
auth_jwt_secret: str = ""
|
auth_jwt_secret: str = ""
|
||||||
# Comma-separated emails that become admin on register (in addition to the
|
# 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)
|
# 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_region: str = "us-central1"
|
||||||
pipeline_worker_project: str = "" # GCP project id; defaults to GOOGLE_CLOUD_PROJECT or metadata
|
pipeline_worker_project: str = "" # GCP project id; defaults to GOOGLE_CLOUD_PROJECT or metadata
|
||||||
pipeline_worker_timeout_seconds: int = 3600
|
pipeline_worker_timeout_seconds: int = 3600
|
||||||
|
|||||||
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
"""PinscopeX backend — FastAPI application."""
|
"""PeriscopeX backend — FastAPI application."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -36,7 +36,7 @@ async def lifespan(app: FastAPI):
|
|||||||
if env == "production" and not settings.use_auth:
|
if env == "production" and not settings.use_auth:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Production requires authentication: set AUTH_JWT_SECRET "
|
"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:
|
if not settings.use_auth:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -44,7 +44,7 @@ async def lifespan(app: FastAPI):
|
|||||||
"This is only safe for local development."
|
"This is only safe for local development."
|
||||||
)
|
)
|
||||||
elif settings.use_local_auth:
|
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:
|
elif settings.use_clerk:
|
||||||
logger.info("Clerk authentication enabled")
|
logger.info("Clerk authentication enabled")
|
||||||
if not settings.billing_enabled:
|
if not settings.billing_enabled:
|
||||||
@@ -130,7 +130,7 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|||||||
|
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="PinscopeX",
|
title="PeriscopeX",
|
||||||
description="Agentic schematic validation API",
|
description="Agentic schematic validation API",
|
||||||
lifespan=lifespan,
|
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
|
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:
|
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:
|
if request.url.path in _SKIP_PATHS:
|
||||||
return "anonymous"
|
return "anonymous"
|
||||||
|
|
||||||
|
|||||||
@@ -290,9 +290,9 @@ def _segments_to_kicad_mod(
|
|||||||
lines = [
|
lines = [
|
||||||
f'(footprint "{name}"',
|
f'(footprint "{name}"',
|
||||||
" (version 20240108)",
|
" (version 20240108)",
|
||||||
' (generator "pinscope")',
|
' (generator "periscope")',
|
||||||
' (layer "F.Cu")',
|
' (layer "F.Cu")',
|
||||||
f' (descr "Pinscope {template.upper()} PCB antenna template '
|
f' (descr "Periscope {template.upper()} PCB antenna template '
|
||||||
f'— not EM-validated")',
|
f'— not EM-validated")',
|
||||||
" (attr smd)",
|
" (attr smd)",
|
||||||
f' (pad "1" smd circle (at 0 0) (size {w_mm * 2:.4f} {w_mm * 2:.4f}) '
|
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 pydantic import BaseModel
|
||||||
|
|
||||||
from backend.pinscopex.antenna_geometry import (
|
from backend.periscopex.antenna_geometry import (
|
||||||
AntennaGeometry,
|
AntennaGeometry,
|
||||||
AntennaTemplate,
|
AntennaTemplate,
|
||||||
build_geometry,
|
build_geometry,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.impedance import GeometryError, solve_width
|
from backend.periscopex.impedance import GeometryError, solve_width
|
||||||
from backend.pinscopex.models import (
|
from backend.periscopex.models import (
|
||||||
ComponentType,
|
ComponentType,
|
||||||
DesignGraph,
|
DesignGraph,
|
||||||
LayoutGraph,
|
LayoutGraph,
|
||||||
@@ -7,7 +7,7 @@ we never invent orphans from a format that has no schematic properties.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from backend.pinscopex.models import Finding
|
from backend.periscopex.models import Finding
|
||||||
|
|
||||||
|
|
||||||
def _norm_mpn(value: object) -> str:
|
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."
|
"validated against a datasheet and may indicate a stale BOM."
|
||||||
),
|
),
|
||||||
recommendation=f"Remove {ref} from the BOM or add it to the schematic.",
|
recommendation=f"Remove {ref} from the BOM or add it to the schematic.",
|
||||||
rule_id="PS-BOM-002",
|
rule_id="PE-BOM-002",
|
||||||
pins=[],
|
pins=[],
|
||||||
))
|
))
|
||||||
continue
|
continue
|
||||||
@@ -67,7 +67,7 @@ def check_bom_schematic_match(
|
|||||||
recommendation=(
|
recommendation=(
|
||||||
f"Make {ref}'s BOM and schematic MPN identical, then re-run."
|
f"Make {ref}'s BOM and schematic MPN identical, then re-run."
|
||||||
),
|
),
|
||||||
rule_id="PS-BOM-001",
|
rule_id="PE-BOM-001",
|
||||||
pins=[ref],
|
pins=[ref],
|
||||||
))
|
))
|
||||||
return findings
|
return findings
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from backend.pinscopex.models import ComponentType, DesignGraph
|
from backend.periscopex.models import ComponentType, DesignGraph
|
||||||
from backend.pinscopex.utils import natural_sort_key
|
from backend.periscopex.utils import natural_sort_key
|
||||||
|
|
||||||
|
|
||||||
def build_bom_summary(
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
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
|
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(
|
def annotate_findings_cad(
|
||||||
@@ -54,7 +54,7 @@ def build_cad_bridge(
|
|||||||
*,
|
*,
|
||||||
url_base: str = "",
|
url_base: str = "",
|
||||||
) -> dict:
|
) -> 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] = []
|
findings: list[dict] = []
|
||||||
for f in report.findings:
|
for f in report.findings:
|
||||||
fid = f.finding_id or ""
|
fid = f.finding_id or ""
|
||||||
@@ -6,12 +6,12 @@ never invent a stray default. Without CL in specs → skip.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from backend.pinscopex.functional_groups import (
|
from backend.periscopex.functional_groups import (
|
||||||
_cap_farads,
|
_cap_farads,
|
||||||
_is_ground_net,
|
_is_ground_net,
|
||||||
load_capacitance_farads,
|
load_capacitance_farads,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.models import (
|
from backend.periscopex.models import (
|
||||||
Component,
|
Component,
|
||||||
ComponentType,
|
ComponentType,
|
||||||
DesignGraph,
|
DesignGraph,
|
||||||
@@ -46,7 +46,7 @@ def check_crystal_cl(graph: DesignGraph) -> list[Finding]:
|
|||||||
why="Crystal load capacitance needs a matched C1/C2 pair.",
|
why="Crystal load capacitance needs a matched C1/C2 pair.",
|
||||||
recommendation="Add or value the two load capacitors on XIN/XOUT.",
|
recommendation="Add or value the two load capacitors on XIN/XOUT.",
|
||||||
reference="netlist topology",
|
reference="netlist topology",
|
||||||
rule_id="PS-XTAL-001",
|
rule_id="PE-XTAL-001",
|
||||||
pins=[ref],
|
pins=[ref],
|
||||||
))
|
))
|
||||||
continue
|
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.",
|
why="Series combination of load caps exceeds specified CL without needing stray.",
|
||||||
recommendation="Reduce load caps or confirm the datasheet CL value.",
|
recommendation="Reduce load caps or confirm the datasheet CL value.",
|
||||||
reference="netlist topology",
|
reference="netlist topology",
|
||||||
rule_id="PS-XTAL-002",
|
rule_id="PE-XTAL-002",
|
||||||
pins=[ref, c1.reference, c2.reference],
|
pins=[ref, c1.reference, c2.reference],
|
||||||
))
|
))
|
||||||
elif series < cl * 0.5:
|
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.",
|
why="Without stray capacitance in specs, effective CL cannot be fully checked.",
|
||||||
recommendation="Confirm Cstray or populate load_capacitance / stray in crystal specs.",
|
recommendation="Confirm Cstray or populate load_capacitance / stray in crystal specs.",
|
||||||
reference="netlist topology",
|
reference="netlist topology",
|
||||||
rule_id="PS-XTAL-003",
|
rule_id="PE-XTAL-003",
|
||||||
pins=[ref, c1.reference, c2.reference],
|
pins=[ref, c1.reference, c2.reference],
|
||||||
))
|
))
|
||||||
continue
|
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.",
|
why="Effective load capacitance should stay near the crystal's specified CL.",
|
||||||
recommendation="Adjust C1/C2 so C_eff ≈ CL.",
|
recommendation="Adjust C1/C2 so C_eff ≈ CL.",
|
||||||
reference="netlist topology",
|
reference="netlist topology",
|
||||||
rule_id="PS-XTAL-002",
|
rule_id="PE-XTAL-002",
|
||||||
pins=[ref, c1.reference, c2.reference],
|
pins=[ref, c1.reference, c2.reference],
|
||||||
))
|
))
|
||||||
return findings
|
return findings
|
||||||
@@ -4,9 +4,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from backend.pinscopex.models import ComponentType, DesignGraph, NetType
|
from backend.periscopex.models import ComponentType, DesignGraph, NetType
|
||||||
from backend.pinscopex.resolve_passives import _format_value
|
from backend.periscopex.resolve_passives import _format_value
|
||||||
from backend.pinscopex.utils import natural_sort_key
|
from backend.periscopex.utils import natural_sort_key
|
||||||
|
|
||||||
# Dielectric strings that indicate ceramic capacitors
|
# Dielectric strings that indicate ceramic capacitors
|
||||||
_CERAMIC_DIELECTRICS = {"X7R", "X5R", "C0G", "NP0", "Y5V", "X7S", "X6S", "X8R", "C0G (NP0)"}
|
_CERAMIC_DIELECTRICS = {"X7R", "X5R", "C0G", "NP0", "Y5V", "X7S", "X6S", "X8R", "C0G (NP0)"}
|
||||||
@@ -8,18 +8,18 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from backend.pinscopex.models import (
|
from backend.periscopex.models import (
|
||||||
ComponentConstraints,
|
ComponentConstraints,
|
||||||
ComponentType,
|
ComponentType,
|
||||||
DesignGraph,
|
DesignGraph,
|
||||||
Finding,
|
Finding,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.passive_rail_check import (
|
from backend.periscopex.passive_rail_check import (
|
||||||
_is_ground_net,
|
_is_ground_net,
|
||||||
_is_power_net,
|
_is_power_net,
|
||||||
_pin_name_tokens,
|
_pin_name_tokens,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.validate import _match_constraints
|
from backend.periscopex.validate import _match_constraints
|
||||||
|
|
||||||
_EN_RE = re.compile(
|
_EN_RE = re.compile(
|
||||||
r"(?:^|[_/])(EN|ENA|ENABLE|n?SHDN|nEN|EN_N|CHIP_EN)(?:$|[_/\d])",
|
r"(?:^|[_/])(EN|ENA|ENABLE|n?SHDN|nEN|EN_N|CHIP_EN)(?:$|[_/\d])",
|
||||||
@@ -106,7 +106,7 @@ def check_dnp_enables(
|
|||||||
reference="BOM DNP/fitted",
|
reference="BOM DNP/fitted",
|
||||||
net=net,
|
net=net,
|
||||||
pins=[f"{ref}.{pin_num}"],
|
pins=[f"{ref}.{pin_num}"],
|
||||||
rule_id="PS-DNP-001",
|
rule_id="PE-DNP-001",
|
||||||
variant=str(variant) if variant else None,
|
variant=str(variant) if variant else None,
|
||||||
))
|
))
|
||||||
return findings
|
return findings
|
||||||
@@ -5,12 +5,12 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from backend.pinscopex.models import ComponentConstraints, DesignGraph, Finding
|
from backend.periscopex.models import ComponentConstraints, DesignGraph, Finding
|
||||||
from backend.pinscopex.passive_rail_check import (
|
from backend.periscopex.passive_rail_check import (
|
||||||
_pin_name_tokens,
|
_pin_name_tokens,
|
||||||
_resistor_to_power,
|
_resistor_to_power,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.validate import _match_constraints
|
from backend.periscopex.validate import _match_constraints
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ def check_errata(
|
|||||||
reference=url,
|
reference=url,
|
||||||
net=net,
|
net=net,
|
||||||
pins=[f"{ref}.{pin_name}"],
|
pins=[f"{ref}.{pin_name}"],
|
||||||
rule_id="PS-ERRATA-001",
|
rule_id="PE-ERRATA-001",
|
||||||
))
|
))
|
||||||
return findings
|
return findings
|
||||||
|
|
||||||
@@ -12,26 +12,26 @@ from pathlib import Path
|
|||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from backend.pinscopex.models import DesignGraph, Finding, ValidationReport
|
from backend.periscopex.models import DesignGraph, Finding, ValidationReport
|
||||||
from backend.pinscopex.pin_mux_check import check_pin_mux_feasibility
|
from backend.periscopex.pin_mux_check import check_pin_mux_feasibility
|
||||||
from backend.pinscopex.led_current_check import check_led_current
|
from backend.periscopex.led_current_check import check_led_current
|
||||||
from backend.pinscopex.passive_rail_check import (
|
from backend.periscopex.passive_rail_check import (
|
||||||
check_i2c_pullups,
|
check_i2c_pullups,
|
||||||
check_reset_pullups,
|
check_reset_pullups,
|
||||||
check_supply_decoupling,
|
check_supply_decoupling,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.bom_match_check import check_bom_schematic_match
|
from backend.periscopex.bom_match_check import check_bom_schematic_match
|
||||||
from backend.pinscopex.hf_coverage_check import check_hf_decoupling_coverage
|
from backend.periscopex.hf_coverage_check import check_hf_decoupling_coverage
|
||||||
from backend.pinscopex.filter_check import check_filters
|
from backend.periscopex.filter_check import check_filters
|
||||||
from backend.pinscopex.thermal_check import check_thermal
|
from backend.periscopex.thermal_check import check_thermal
|
||||||
from backend.pinscopex.power_margin_check import check_power_margin
|
from backend.periscopex.power_margin_check import check_power_margin
|
||||||
from backend.pinscopex.sequencing_check import check_power_sequencing
|
from backend.periscopex.sequencing_check import check_power_sequencing
|
||||||
from backend.pinscopex.dnp_check import check_dnp_enables
|
from backend.periscopex.dnp_check import check_dnp_enables
|
||||||
from backend.pinscopex.lifecycle import check_lifecycle
|
from backend.periscopex.lifecycle import check_lifecycle
|
||||||
from backend.pinscopex.errata_check import check_errata
|
from backend.periscopex.errata_check import check_errata
|
||||||
from backend.pinscopex.internal_features_check import check_internal_features
|
from backend.periscopex.internal_features_check import check_internal_features
|
||||||
from backend.pinscopex.placement_check import check_placement
|
from backend.periscopex.placement_check import check_placement
|
||||||
from backend.pinscopex.si_check import check_si
|
from backend.periscopex.si_check import check_si
|
||||||
|
|
||||||
|
|
||||||
class EvalScores(BaseModel):
|
class EvalScores(BaseModel):
|
||||||
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|||||||
import math
|
import math
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from backend.pinscopex.models import (
|
from backend.periscopex.models import (
|
||||||
Component,
|
Component,
|
||||||
ComponentConstraints,
|
ComponentConstraints,
|
||||||
ComponentType,
|
ComponentType,
|
||||||
@@ -18,14 +18,14 @@ from backend.pinscopex.models import (
|
|||||||
Finding,
|
Finding,
|
||||||
InductorSpecs,
|
InductorSpecs,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.passive_rail_check import (
|
from backend.periscopex.passive_rail_check import (
|
||||||
_cap_farads,
|
_cap_farads,
|
||||||
_is_ground_net,
|
_is_ground_net,
|
||||||
_is_power_net,
|
_is_power_net,
|
||||||
_pin_name_tokens,
|
_pin_name_tokens,
|
||||||
_resistor_ohms,
|
_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")
|
_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")
|
_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",
|
reference="netlist topology",
|
||||||
net=net,
|
net=net,
|
||||||
pins=[designator],
|
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):
|
if adc_hz is not None and not (0.1 * adc_hz <= fc <= 20 * adc_hz):
|
||||||
return Finding(
|
return Finding(
|
||||||
@@ -197,7 +197,7 @@ def _filter_finding(
|
|||||||
reference="netlist topology",
|
reference="netlist topology",
|
||||||
net=net,
|
net=net,
|
||||||
pins=[designator],
|
pins=[designator],
|
||||||
rule_id="PS-FLT-002",
|
rule_id="PE-FLT-002",
|
||||||
)
|
)
|
||||||
rec = (
|
rec = (
|
||||||
"fc is within a wide band of the IC sample/data rate."
|
"fc is within a wide band of the IC sample/data rate."
|
||||||
@@ -216,7 +216,7 @@ def _filter_finding(
|
|||||||
reference="netlist topology",
|
reference="netlist topology",
|
||||||
net=net,
|
net=net,
|
||||||
pins=[designator],
|
pins=[designator],
|
||||||
rule_id="PS-FLT-001",
|
rule_id="PE-FLT-001",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -351,7 +351,7 @@ def check_filters(
|
|||||||
reference="IC specs",
|
reference="IC specs",
|
||||||
net=analog,
|
net=analog,
|
||||||
pins=[ref],
|
pins=[ref],
|
||||||
rule_id="PS-FLT-003",
|
rule_id="PE-FLT-003",
|
||||||
))
|
))
|
||||||
|
|
||||||
for ref, comp in sorted(graph.components.items()):
|
for ref, comp in sorted(graph.components.items()):
|
||||||
@@ -14,7 +14,7 @@ from typing import Any, Literal
|
|||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from backend.pinscopex.models import (
|
from backend.periscopex.models import (
|
||||||
CapacitorSpecs,
|
CapacitorSpecs,
|
||||||
Component,
|
Component,
|
||||||
ComponentConstraints,
|
ComponentConstraints,
|
||||||
@@ -23,7 +23,7 @@ from backend.pinscopex.models import (
|
|||||||
NetType,
|
NetType,
|
||||||
SimpleComponentSpecs,
|
SimpleComponentSpecs,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.resolve_passives import _parse_spice_value
|
from backend.periscopex.resolve_passives import _parse_spice_value
|
||||||
|
|
||||||
RoleHint = Literal[
|
RoleHint = Literal[
|
||||||
"decoupling",
|
"decoupling",
|
||||||
@@ -6,8 +6,8 @@ import json
|
|||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from backend.pinscopex.utils import safe_mpn
|
from backend.periscopex.utils import safe_mpn
|
||||||
from backend.pinscopex.models import (
|
from backend.periscopex.models import (
|
||||||
CadIndexEntry,
|
CadIndexEntry,
|
||||||
Component,
|
Component,
|
||||||
ComponentConstraints,
|
ComponentConstraints,
|
||||||
@@ -23,8 +23,8 @@ from backend.pinscopex.models import (
|
|||||||
|
|
||||||
# Datasheets are loaded here for pin-name enrichment during graph build,
|
# Datasheets are loaded here for pin-name enrichment during graph build,
|
||||||
# but NOT embedded into the graph. The validator loads them separately.
|
# but NOT embedded into the graph. The validator loads them separately.
|
||||||
from backend.pinscopex.parsers import parse_bom, parse_netlist_any
|
from backend.periscopex.parsers import parse_bom, parse_netlist_any
|
||||||
from backend.pinscopex.resolve_passives import SkippedItem, resolve_bom, resolved_to_specs
|
from backend.periscopex.resolve_passives import SkippedItem, resolve_bom, resolved_to_specs
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Component type classification
|
# Component type classification
|
||||||
@@ -291,7 +291,7 @@ def build_graph(
|
|||||||
if pcb_path is not None:
|
if pcb_path is not None:
|
||||||
pcb = Path(pcb_path)
|
pcb = Path(pcb_path)
|
||||||
if pcb.is_file():
|
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)
|
layout = parse_kicad_pcb(pcb)
|
||||||
pcb_nets = nets_from_pcb(layout)
|
pcb_nets = nets_from_pcb(layout)
|
||||||
@@ -300,7 +300,7 @@ def build_graph(
|
|||||||
for ref, fp in layout.footprints.items():
|
for ref, fp in layout.footprints.items():
|
||||||
parts.setdefault(ref, fp.footprint or "")
|
parts.setdefault(ref, fp.footprint or "")
|
||||||
if fmt.startswith("kicad"):
|
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():
|
for ref, extra in kicad_part_fields(netlist_path).items():
|
||||||
schematic_fields[ref] = {
|
schematic_fields[ref] = {
|
||||||
"mpn": extra.get("mpn"),
|
"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 __future__ import annotations
|
||||||
|
|
||||||
from backend.pinscopex.models import ComponentType, DesignGraph, Finding, NetType
|
from backend.periscopex.models import ComponentType, DesignGraph, Finding, NetType
|
||||||
from backend.pinscopex.passive_rail_check import (
|
from backend.periscopex.passive_rail_check import (
|
||||||
_cap_farads,
|
_cap_farads,
|
||||||
_is_ground_net,
|
_is_ground_net,
|
||||||
_is_ic_supply_pin,
|
_is_ic_supply_pin,
|
||||||
@@ -15,7 +15,7 @@ from backend.pinscopex.passive_rail_check import (
|
|||||||
_is_regulator_output_pin,
|
_is_regulator_output_pin,
|
||||||
_pin_label,
|
_pin_label,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.validate import _match_constraints
|
from backend.periscopex.validate import _match_constraints
|
||||||
|
|
||||||
_BULK_MIN_F = 1e-6
|
_BULK_MIN_F = 1e-6
|
||||||
_HF_MAX_F = 1e-6
|
_HF_MAX_F = 1e-6
|
||||||
@@ -104,6 +104,6 @@ def check_hf_decoupling_coverage(
|
|||||||
reference="netlist topology (stima)",
|
reference="netlist topology (stima)",
|
||||||
net=net_name,
|
net=net_name,
|
||||||
pins=[f"{ref}.{pin_num}"],
|
pins=[f"{ref}.{pin_num}"],
|
||||||
rule_id="PS-ESR-001",
|
rule_id="PE-ESR-001",
|
||||||
))
|
))
|
||||||
return findings
|
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`,
|
All Z0 numbers come from ImpedenceFinder (`vendor/impedancefinder`,
|
||||||
Hammerstad-Jensen / Cohn as in KiCad pcb_calculator). This module only
|
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:
|
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 = [
|
lines = [
|
||||||
"(version 1)",
|
"(version 1)",
|
||||||
"# Pinscope impedance advice (ImpedanceFinder solver) — apply in pcbnew.",
|
"# Periscope impedance advice (ImpedanceFinder solver) — apply in pcbnew.",
|
||||||
]
|
]
|
||||||
mapping = (
|
mapping = (
|
||||||
("microstrip_50", "PINSCOPE_50OHM", "50Ohm"),
|
("microstrip_50", "PERISCOPE_50OHM", "50Ohm"),
|
||||||
("diff_90", "PINSCOPE_90OHM_USB", "90Ohm"),
|
("diff_90", "PERISCOPE_90OHM_USB", "90Ohm"),
|
||||||
("diff_100", "PINSCOPE_100OHM_DIFF", "100Ohm"),
|
("diff_100", "PERISCOPE_100OHM_DIFF", "100Ohm"),
|
||||||
)
|
)
|
||||||
for key, rule, netclass in mapping:
|
for key, rule, netclass in mapping:
|
||||||
r = targets[key]
|
r = targets[key]
|
||||||
@@ -23,8 +23,8 @@ from impedancefinder.model import (
|
|||||||
ZonePolygon,
|
ZonePolygon,
|
||||||
)
|
)
|
||||||
|
|
||||||
from backend.pinscopex.impedance import GeometryError
|
from backend.periscopex.impedance import GeometryError
|
||||||
from backend.pinscopex.models import DesignGraph, LayoutGraph, NetType
|
from backend.periscopex.models import DesignGraph, LayoutGraph, NetType
|
||||||
|
|
||||||
|
|
||||||
def _stackup(layout: LayoutGraph) -> Stackup:
|
def _stackup(layout: LayoutGraph) -> Stackup:
|
||||||
+4
-4
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from backend.pinscopex.models import ComponentConstraints, DesignGraph, Finding
|
from backend.periscopex.models import ComponentConstraints, DesignGraph, Finding
|
||||||
from backend.pinscopex.passive_rail_check import (
|
from backend.periscopex.passive_rail_check import (
|
||||||
_pin_name_tokens,
|
_pin_name_tokens,
|
||||||
_resistor_to_power,
|
_resistor_to_power,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.validate import _match_constraints
|
from backend.periscopex.validate import _match_constraints
|
||||||
|
|
||||||
|
|
||||||
def check_internal_features(
|
def check_internal_features(
|
||||||
@@ -51,6 +51,6 @@ def check_internal_features(
|
|||||||
reference="internal_features",
|
reference="internal_features",
|
||||||
net=net,
|
net=net,
|
||||||
pins=[f"{ref}.{pin_name}"],
|
pins=[f"{ref}.{pin_name}"],
|
||||||
rule_id="PS-INT-001",
|
rule_id="PE-INT-001",
|
||||||
))
|
))
|
||||||
return findings
|
return findings
|
||||||
@@ -17,8 +17,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from backend.pinscopex.models import ComponentType, DesignGraph, Finding, NetType
|
from backend.periscopex.models import ComponentType, DesignGraph, Finding, NetType
|
||||||
from backend.pinscopex.resolve_passives import _parse_spice_value
|
from backend.periscopex.resolve_passives import _parse_spice_value
|
||||||
|
|
||||||
_COLOR_TOKENS = {
|
_COLOR_TOKENS = {
|
||||||
"R": "red", "RED": "red",
|
"R": "red", "RED": "red",
|
||||||
@@ -8,8 +8,8 @@ from pathlib import Path
|
|||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from backend.pinscopex.models import ComponentType, DesignGraph, Finding
|
from backend.periscopex.models import ComponentType, DesignGraph, Finding
|
||||||
from backend.pinscopex.utils import safe_mpn
|
from backend.periscopex.utils import safe_mpn
|
||||||
|
|
||||||
_EOL = re.compile(
|
_EOL = re.compile(
|
||||||
r"\b(obsolete|eol|end\s*of\s*life|discontinued|last\s*time\s*buy|ltb)\b",
|
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,
|
recommendation=rec_txt,
|
||||||
reference=rec.source or "distributor",
|
reference=rec.source or "distributor",
|
||||||
pins=[ref],
|
pins=[ref],
|
||||||
rule_id="PS-LF-001",
|
rule_id="PE-LF-001",
|
||||||
))
|
))
|
||||||
elif rec.lifecycle == "nrnd":
|
elif rec.lifecycle == "nrnd":
|
||||||
findings.append(Finding(
|
findings.append(Finding(
|
||||||
@@ -202,7 +202,7 @@ def check_lifecycle(
|
|||||||
recommendation="Prefer an Active orderable if the design is new.",
|
recommendation="Prefer an Active orderable if the design is new.",
|
||||||
reference=rec.source or "distributor",
|
reference=rec.source or "distributor",
|
||||||
pins=[ref],
|
pins=[ref],
|
||||||
rule_id="PS-LF-002",
|
rule_id="PE-LF-002",
|
||||||
))
|
))
|
||||||
if rec.rohs_compliant is False:
|
if rec.rohs_compliant is False:
|
||||||
findings.append(Finding(
|
findings.append(Finding(
|
||||||
@@ -216,6 +216,6 @@ def check_lifecycle(
|
|||||||
recommendation="Choose a RoHS-compliant orderable of the same MPN family.",
|
recommendation="Choose a RoHS-compliant orderable of the same MPN family.",
|
||||||
reference=rec.source or "distributor",
|
reference=rec.source or "distributor",
|
||||||
pins=[ref],
|
pins=[ref],
|
||||||
rule_id="PS-LF-003",
|
rule_id="PE-LF-003",
|
||||||
))
|
))
|
||||||
return findings
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ def _check_subtype(v: object) -> str | None:
|
|||||||
"""Shared pre-validator for component_subtype fields."""
|
"""Shared pre-validator for component_subtype fields."""
|
||||||
if v is None or v == "":
|
if v is None or v == "":
|
||||||
return None
|
return None
|
||||||
from backend.pinscopex.taxonomy import validate_subtype
|
from backend.periscopex.taxonomy import validate_subtype
|
||||||
return validate_subtype(str(v))
|
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
|
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
|
net: str | None = None # net name for CAD telemetry / SI filters
|
||||||
pins: list[str] = [] # e.g. ["U3.54"] for pan-and-zoom
|
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_sheet: str | None = None # schematic sheet filename for plugin sync
|
||||||
cad_uuid: str | None = None # KiCad symbol/pin uuid
|
cad_uuid: str | None = None # KiCad symbol/pin uuid
|
||||||
variant: str | None = None # DNP / ECO / assembly variant
|
variant: str | None = None # DNP / ECO / assembly variant
|
||||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from backend.pinscopex.models import (
|
from backend.periscopex.models import (
|
||||||
ComponentConstraints,
|
ComponentConstraints,
|
||||||
ComponentType,
|
ComponentType,
|
||||||
DesignGraph,
|
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.",
|
why="No-connect pins should remain unconnected or on an explicit NC net.",
|
||||||
recommendation="Leave the NC pin floating or disconnect the net.",
|
recommendation="Leave the NC pin floating or disconnect the net.",
|
||||||
reference="pintable",
|
reference="pintable",
|
||||||
rule_id="PS-NC-001",
|
rule_id="PE-NC-001",
|
||||||
net=net,
|
net=net,
|
||||||
pins=[f"{ref}.{pin_num}"],
|
pins=[f"{ref}.{pin_num}"],
|
||||||
)
|
)
|
||||||
@@ -11,7 +11,7 @@ import zipfile
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
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_BUNDLE_BYTES = 30 * 1024 * 1024
|
||||||
_MAX_ZIP_MEMBERS = 400
|
_MAX_ZIP_MEMBERS = 400
|
||||||
@@ -144,7 +144,7 @@ def find_bom(work: Path) -> Path | None:
|
|||||||
|
|
||||||
|
|
||||||
def _sheetfiles_of(path: Path) -> list[str]:
|
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")
|
text = path.read_text(encoding="utf-8", errors="replace")
|
||||||
tree = _parse_sexp(text)
|
tree = _parse_sexp(text)
|
||||||
@@ -200,10 +200,10 @@ def parse_netlist_any(
|
|||||||
sample = p.read_bytes()[:2048]
|
sample = p.read_bytes()[:2048]
|
||||||
fmt = detect_netlist_format(sample)
|
fmt = detect_netlist_format(sample)
|
||||||
if fmt == "edif":
|
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)
|
parts, nets = parse_edif_netlist(p, include_subdesigns=include_subdesigns)
|
||||||
elif fmt.startswith("kicad"):
|
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)
|
parts, nets, _ = parse_kicad(p)
|
||||||
else:
|
else:
|
||||||
parts, nets = parse_netlist(p, known_refs=known_refs)
|
parts, nets = parse_netlist(p, known_refs=known_refs)
|
||||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from backend.pinscopex.models import (
|
from backend.periscopex.models import (
|
||||||
LayoutDielectric,
|
LayoutDielectric,
|
||||||
LayoutFootprint,
|
LayoutFootprint,
|
||||||
LayoutGraph,
|
LayoutGraph,
|
||||||
@@ -17,7 +17,7 @@ from backend.pinscopex.models import (
|
|||||||
LayoutVia,
|
LayoutVia,
|
||||||
LayoutZone,
|
LayoutZone,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.parsers_kicad import (
|
from backend.periscopex.parsers_kicad import (
|
||||||
_at,
|
_at,
|
||||||
_fnum,
|
_fnum,
|
||||||
_kid,
|
_kid,
|
||||||
+10
-10
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from backend.pinscopex.models import (
|
from backend.periscopex.models import (
|
||||||
CapacitorSpecs,
|
CapacitorSpecs,
|
||||||
Component,
|
Component,
|
||||||
ComponentConstraints,
|
ComponentConstraints,
|
||||||
@@ -19,9 +19,9 @@ from backend.pinscopex.models import (
|
|||||||
NetType,
|
NetType,
|
||||||
ResistorSpecs,
|
ResistorSpecs,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.validate import _match_constraints
|
from backend.periscopex.validate import _match_constraints
|
||||||
from backend.pinscopex.led_current_check import _parse_resistance
|
from backend.periscopex.led_current_check import _parse_resistance
|
||||||
from backend.pinscopex.resolve_passives import _parse_spice_value
|
from backend.periscopex.resolve_passives import _parse_spice_value
|
||||||
|
|
||||||
_SUPPLY_PIN_RE = re.compile(
|
_SUPPLY_PIN_RE = re.compile(
|
||||||
r"(?:^|[_/])(VDD|VCC|VDDA|VDDD|VDDIO|DVDD|AVDD|IOVDD|VDD33|VDD18|"
|
r"(?:^|[_/])(VDD|VCC|VDDA|VDDD|VDDIO|DVDD|AVDD|IOVDD|VDD33|VDD18|"
|
||||||
@@ -114,7 +114,7 @@ def check_supply_decoupling(
|
|||||||
reference="netlist topology",
|
reference="netlist topology",
|
||||||
net=net_name,
|
net=net_name,
|
||||||
pins=[f"{ref}.{pin_num}"],
|
pins=[f"{ref}.{pin_num}"],
|
||||||
rule_id="PS-DEC-001",
|
rule_id="PE-DEC-001",
|
||||||
))
|
))
|
||||||
continue
|
continue
|
||||||
min_f = _VOUT_MIN_FARADS if role == "output" else _VDD_MIN_FARADS
|
min_f = _VOUT_MIN_FARADS if role == "output" else _VDD_MIN_FARADS
|
||||||
@@ -144,7 +144,7 @@ def check_supply_decoupling(
|
|||||||
reference="netlist topology",
|
reference="netlist topology",
|
||||||
net=net_name,
|
net=net_name,
|
||||||
pins=[f"{ref}.{pin_num}"],
|
pins=[f"{ref}.{pin_num}"],
|
||||||
rule_id="PS-DEC-002",
|
rule_id="PE-DEC-002",
|
||||||
))
|
))
|
||||||
return findings
|
return findings
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ def check_i2c_pullups(
|
|||||||
reference="NXP UM10204 (wide bound)",
|
reference="NXP UM10204 (wide bound)",
|
||||||
net=net_name,
|
net=net_name,
|
||||||
pins=[f"{ref}.{pin_num}"],
|
pins=[f"{ref}.{pin_num}"],
|
||||||
rule_id="PS-I2C-002",
|
rule_id="PE-I2C-002",
|
||||||
))
|
))
|
||||||
continue
|
continue
|
||||||
pin_label = _pin_label(cons, pin_num, net_name)
|
pin_label = _pin_label(cons, pin_num, net_name)
|
||||||
@@ -228,7 +228,7 @@ def check_i2c_pullups(
|
|||||||
reference="netlist topology",
|
reference="netlist topology",
|
||||||
net=net_name,
|
net=net_name,
|
||||||
pins=[f"{ref}.{pin_num}"],
|
pins=[f"{ref}.{pin_num}"],
|
||||||
rule_id="PS-I2C-001",
|
rule_id="PE-I2C-001",
|
||||||
))
|
))
|
||||||
return findings
|
return findings
|
||||||
|
|
||||||
@@ -285,7 +285,7 @@ def check_reset_pullups(
|
|||||||
reference="netlist topology",
|
reference="netlist topology",
|
||||||
net=net_name,
|
net=net_name,
|
||||||
pins=[f"{ref}.{pin_num}"],
|
pins=[f"{ref}.{pin_num}"],
|
||||||
rule_id="PS-RST-002",
|
rule_id="PE-RST-002",
|
||||||
))
|
))
|
||||||
if _resistor_to_power(graph, net_name):
|
if _resistor_to_power(graph, net_name):
|
||||||
continue
|
continue
|
||||||
@@ -311,7 +311,7 @@ def check_reset_pullups(
|
|||||||
reference="netlist topology",
|
reference="netlist topology",
|
||||||
net=net_name,
|
net=net_name,
|
||||||
pins=[f"{ref}.{pin_num}"],
|
pins=[f"{ref}.{pin_num}"],
|
||||||
rule_id="PS-RST-001",
|
rule_id="PE-RST-001",
|
||||||
))
|
))
|
||||||
return findings
|
return findings
|
||||||
|
|
||||||
@@ -16,19 +16,19 @@ perspective is ambiguous).
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from backend.pinscopex.models import (
|
from backend.periscopex.models import (
|
||||||
ComponentConstraints,
|
ComponentConstraints,
|
||||||
ComponentType,
|
ComponentType,
|
||||||
DesignGraph,
|
DesignGraph,
|
||||||
Finding,
|
Finding,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.pin_function_tokens import (
|
from backend.periscopex.pin_function_tokens import (
|
||||||
complement,
|
complement,
|
||||||
normalize_functions,
|
normalize_functions,
|
||||||
parse_net_token,
|
parse_net_token,
|
||||||
signals_for_peripheral,
|
signals_for_peripheral,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.validate import _match_constraints
|
from backend.periscopex.validate import _match_constraints
|
||||||
|
|
||||||
|
|
||||||
def check_pin_mux_feasibility(
|
def check_pin_mux_feasibility(
|
||||||
@@ -171,5 +171,5 @@ def _feasibility_finding(
|
|||||||
reference=f"{mpn or ref} alternate-function table",
|
reference=f"{mpn or ref} alternate-function table",
|
||||||
net=net_name,
|
net=net_name,
|
||||||
pins=[f"{ref}.{pin_num}"],
|
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
|
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.
|
has a numeric max_distance_mm. Null millimetres skip — no 3 mm default.
|
||||||
Thermal vias (`PS-PLC-002`) skip without courtyard vertices and without
|
Thermal vias (`PE-PLC-002`) skip without courtyard vertices and without
|
||||||
min_via_count — no invented pad radius. same_layer (`PS-PLC-003`) uses
|
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 boolean parameter plus footprint layers from the PCB. Crystals use
|
||||||
the same decoupling_proximity rule. Track length is shortest path on
|
the same decoupling_proximity rule. Track length is shortest path on
|
||||||
segments vs max_distance_mm — no invented “much larger than euclidean”.
|
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
|
from __future__ import annotations
|
||||||
@@ -15,7 +15,7 @@ from __future__ import annotations
|
|||||||
import heapq
|
import heapq
|
||||||
import math
|
import math
|
||||||
|
|
||||||
from backend.pinscopex.models import (
|
from backend.periscopex.models import (
|
||||||
ComponentConstraints,
|
ComponentConstraints,
|
||||||
ComponentType,
|
ComponentType,
|
||||||
DesignGraph,
|
DesignGraph,
|
||||||
@@ -23,7 +23,7 @@ from backend.pinscopex.models import (
|
|||||||
LayoutGraph,
|
LayoutGraph,
|
||||||
LayoutPad,
|
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:
|
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",
|
status="ERROR",
|
||||||
recommendation="Place the decoupling capacitor closer to the supply pin.",
|
recommendation="Place the decoupling capacitor closer to the supply pin.",
|
||||||
source="placement_check",
|
source="placement_check",
|
||||||
rule_id="PS-PLC-001",
|
rule_id="PE-PLC-001",
|
||||||
net=net,
|
net=net,
|
||||||
pins=[pin_no],
|
pins=[pin_no],
|
||||||
source_page=rule.get("source_page"),
|
source_page=rule.get("source_page"),
|
||||||
@@ -231,7 +231,7 @@ def _same_layer_finding(ref, comp, cons, rule, graph: DesignGraph, layout: Layou
|
|||||||
status="WARNING",
|
status="WARNING",
|
||||||
recommendation="Place the decoupling capacitor on the same layer or add a via in the courtyard.",
|
recommendation="Place the decoupling capacitor on the same layer or add a via in the courtyard.",
|
||||||
source="placement_check",
|
source="placement_check",
|
||||||
rule_id="PS-PLC-003",
|
rule_id="PE-PLC-003",
|
||||||
net=net,
|
net=net,
|
||||||
pins=[pin_no],
|
pins=[pin_no],
|
||||||
source_page=rule.get("source_page"),
|
source_page=rule.get("source_page"),
|
||||||
@@ -274,7 +274,7 @@ def _thermal_via_finding(ref, comp, cons, rule, layout: LayoutGraph) -> list[Fin
|
|||||||
status="ERROR",
|
status="ERROR",
|
||||||
recommendation="Add vias in the thermal pad courtyard.",
|
recommendation="Add vias in the thermal pad courtyard.",
|
||||||
source="placement_check",
|
source="placement_check",
|
||||||
rule_id="PS-PLC-002",
|
rule_id="PE-PLC-002",
|
||||||
pins=[pin] if pin else [],
|
pins=[pin] if pin else [],
|
||||||
source_page=rule.get("source_page"),
|
source_page=rule.get("source_page"),
|
||||||
)]
|
)]
|
||||||
@@ -308,7 +308,7 @@ def _keepout_finding(ref, comp, cons, rule, graph: DesignGraph, layout: LayoutGr
|
|||||||
status="WARNING",
|
status="WARNING",
|
||||||
recommendation="Keep other nets out of the courtyard.",
|
recommendation="Keep other nets out of the courtyard.",
|
||||||
source="placement_check",
|
source="placement_check",
|
||||||
rule_id="PS-PLC-004",
|
rule_id="PE-PLC-004",
|
||||||
net=net,
|
net=net,
|
||||||
pins=[pin_no],
|
pins=[pin_no],
|
||||||
source_page=rule.get("source_page"),
|
source_page=rule.get("source_page"),
|
||||||
@@ -12,8 +12,8 @@ from typing import Literal
|
|||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from backend.pinscopex.functional_groups import FunctionalGroupsReport, PlacementIcGroup
|
from backend.periscopex.functional_groups import FunctionalGroupsReport, PlacementIcGroup
|
||||||
from backend.pinscopex.models import DesignGraph, LayoutGraph, LayoutPad
|
from backend.periscopex.models import DesignGraph, LayoutGraph, LayoutPad
|
||||||
|
|
||||||
SkipReason = Literal[
|
SkipReason = Literal[
|
||||||
"no_pcb_footprints",
|
"no_pcb_footprints",
|
||||||
@@ -6,8 +6,8 @@ IC on the rail has a spec. Trace resistance is never estimated.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from backend.pinscopex.led_current_check import _net_voltage, _parse_resistance
|
from backend.periscopex.led_current_check import _net_voltage, _parse_resistance
|
||||||
from backend.pinscopex.models import (
|
from backend.periscopex.models import (
|
||||||
Component,
|
Component,
|
||||||
ComponentConstraints,
|
ComponentConstraints,
|
||||||
ComponentType,
|
ComponentType,
|
||||||
@@ -16,8 +16,8 @@ from backend.pinscopex.models import (
|
|||||||
InductorSpecs,
|
InductorSpecs,
|
||||||
ResistorSpecs,
|
ResistorSpecs,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.passive_rail_check import _is_ground_net
|
from backend.periscopex.passive_rail_check import _is_ground_net
|
||||||
from backend.pinscopex.thermal_check import (
|
from backend.periscopex.thermal_check import (
|
||||||
_IOUT_MAX_KEYS,
|
_IOUT_MAX_KEYS,
|
||||||
_LOAD_KEYS,
|
_LOAD_KEYS,
|
||||||
_VIN_PIN,
|
_VIN_PIN,
|
||||||
@@ -27,7 +27,7 @@ from backend.pinscopex.thermal_check import (
|
|||||||
_pin_net_by_role,
|
_pin_net_by_role,
|
||||||
_specs_values,
|
_specs_values,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.validate import _match_constraints
|
from backend.periscopex.validate import _match_constraints
|
||||||
|
|
||||||
_IQ_KEYS = (
|
_IQ_KEYS = (
|
||||||
"iq_a", "quiescent_current_a", "supply_current_a", "idd_a", "icc_a",
|
"iq_a", "quiescent_current_a", "supply_current_a", "idd_a", "icc_a",
|
||||||
@@ -139,7 +139,7 @@ def check_power_margin(
|
|||||||
reference="regulator Iout_max",
|
reference="regulator Iout_max",
|
||||||
net=vout,
|
net=vout,
|
||||||
pins=[ref],
|
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.
|
# 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",
|
reference="netlist series R",
|
||||||
net=vin,
|
net=vin,
|
||||||
pins=[r],
|
pins=[r],
|
||||||
rule_id="PS-PWR-001",
|
rule_id="PE-PWR-001",
|
||||||
))
|
))
|
||||||
return findings
|
return findings
|
||||||
@@ -11,9 +11,9 @@ import re
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from backend.pinscopex.models import Finding
|
from backend.periscopex.models import Finding
|
||||||
from backend.pinscopex.pdf_text import pdf_page_texts as extract_pdf_pages
|
from backend.periscopex.pdf_text import pdf_page_texts as extract_pdf_pages
|
||||||
from backend.pinscopex.utils import safe_mpn
|
from backend.periscopex.utils import safe_mpn
|
||||||
|
|
||||||
_MIN_QUOTE_CHARS = 12
|
_MIN_QUOTE_CHARS = 12
|
||||||
_EMPTY_PAGE_ALNUM = 40
|
_EMPTY_PAGE_ALNUM = 40
|
||||||
@@ -8,7 +8,7 @@ import re
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from backend.pinscopex.models import (
|
from backend.periscopex.models import (
|
||||||
CapacitorSpecs,
|
CapacitorSpecs,
|
||||||
ComponentSpecs,
|
ComponentSpecs,
|
||||||
ComponentType,
|
ComponentType,
|
||||||
@@ -19,7 +19,7 @@ from backend.pinscopex.models import (
|
|||||||
SimpleComponentSpecs,
|
SimpleComponentSpecs,
|
||||||
ValueDecoder,
|
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 hashlib
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from backend.pinscopex.models import ComponentType, DesignGraph
|
from backend.periscopex.models import ComponentType, DesignGraph
|
||||||
from backend.pinscopex.validate import _match_constraints
|
from backend.periscopex.validate import _match_constraints
|
||||||
|
|
||||||
|
|
||||||
def ic_neighborhood_fingerprint(
|
def ic_neighborhood_fingerprint(
|
||||||
@@ -14,7 +14,7 @@ import json
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Iterable, Literal
|
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"]
|
ReviewState = Literal["open", "false_positive", "accepted", "wontfix"]
|
||||||
VALID_STATES: frozenset[str] = frozenset({"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
|
import re
|
||||||
|
|
||||||
from backend.pinscopex.models import (
|
from backend.periscopex.models import (
|
||||||
ComponentConstraints,
|
ComponentConstraints,
|
||||||
ComponentType,
|
ComponentType,
|
||||||
DesignGraph,
|
DesignGraph,
|
||||||
Finding,
|
Finding,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.thermal_check import (
|
from backend.periscopex.thermal_check import (
|
||||||
_VIN_PIN,
|
_VIN_PIN,
|
||||||
_VOUT_PIN,
|
_VOUT_PIN,
|
||||||
_is_ldo,
|
_is_ldo,
|
||||||
_pin_net_by_role,
|
_pin_net_by_role,
|
||||||
_specs_values,
|
_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)
|
_PG_RE = re.compile(r"(?:^|[_/])(PG|PGOOD|PWRGD|POWER_GOOD|POK)(?:$|[_/\d])", re.I)
|
||||||
_EN_RE = re.compile(
|
_EN_RE = re.compile(
|
||||||
@@ -86,7 +86,7 @@ def check_power_sequencing(
|
|||||||
reference="power_sequence",
|
reference="power_sequence",
|
||||||
net=den,
|
net=den,
|
||||||
pins=[dref, uref],
|
pins=[dref, uref],
|
||||||
rule_id="PS-SEQ-001",
|
rule_id="PE-SEQ-001",
|
||||||
))
|
))
|
||||||
continue
|
continue
|
||||||
if upg != den:
|
if upg != den:
|
||||||
@@ -104,6 +104,6 @@ def check_power_sequencing(
|
|||||||
reference="power_sequence",
|
reference="power_sequence",
|
||||||
net=den,
|
net=den,
|
||||||
pins=[f"{uref}", f"{dref}"],
|
pins=[f"{uref}", f"{dref}"],
|
||||||
rule_id="PS-SEQ-001",
|
rule_id="PE-SEQ-001",
|
||||||
))
|
))
|
||||||
return findings
|
return findings
|
||||||
@@ -8,8 +8,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import math
|
import math
|
||||||
|
|
||||||
from backend.pinscopex.models import DesignGraph, Finding, LayoutGraph, LayoutSegment
|
from backend.periscopex.models import DesignGraph, Finding, LayoutGraph, LayoutSegment
|
||||||
from backend.pinscopex.validate import _match_constraints
|
from backend.periscopex.validate import _match_constraints
|
||||||
|
|
||||||
_PAIR_SUFFIXES = (("_DP", "_DM"), ("_P", "_N"), ("+", "-"))
|
_PAIR_SUFFIXES = (("_DP", "_DM"), ("_P", "_N"), ("+", "-"))
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ def check_si(
|
|||||||
status="ERROR",
|
status="ERROR",
|
||||||
recommendation="Length-match the differential pair.",
|
recommendation="Length-match the differential pair.",
|
||||||
source="si_check",
|
source="si_check",
|
||||||
rule_id="PS-SI-001",
|
rule_id="PE-SI-001",
|
||||||
net=net,
|
net=net,
|
||||||
pins=[],
|
pins=[],
|
||||||
source_page=page,
|
source_page=page,
|
||||||
@@ -8,14 +8,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from backend.pinscopex.led_current_check import (
|
from backend.periscopex.led_current_check import (
|
||||||
_leg_color,
|
_leg_color,
|
||||||
_net_voltage,
|
_net_voltage,
|
||||||
_parse_resistance,
|
_parse_resistance,
|
||||||
_series_resistor,
|
_series_resistor,
|
||||||
_vf,
|
_vf,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.models import (
|
from backend.periscopex.models import (
|
||||||
Component,
|
Component,
|
||||||
ComponentConstraints,
|
ComponentConstraints,
|
||||||
ComponentType,
|
ComponentType,
|
||||||
@@ -23,9 +23,9 @@ from backend.pinscopex.models import (
|
|||||||
Finding,
|
Finding,
|
||||||
ResistorSpecs,
|
ResistorSpecs,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.passive_rail_check import _pin_name_tokens
|
from backend.periscopex.passive_rail_check import _pin_name_tokens
|
||||||
from backend.pinscopex.resolve_passives import _parse_spice_value
|
from backend.periscopex.resolve_passives import _parse_spice_value
|
||||||
from backend.pinscopex.validate import _match_constraints
|
from backend.periscopex.validate import _match_constraints
|
||||||
|
|
||||||
_TA_C = 25.0
|
_TA_C = 25.0
|
||||||
_TJ_WARN_C = 125.0
|
_TJ_WARN_C = 125.0
|
||||||
@@ -175,12 +175,12 @@ def _ldo_thermal(
|
|||||||
reference="thermal estimate",
|
reference="thermal estimate",
|
||||||
net=net,
|
net=net,
|
||||||
pins=[ref],
|
pins=[ref],
|
||||||
rule_id="PS-TH-001",
|
rule_id="PE-TH-001",
|
||||||
))
|
))
|
||||||
continue
|
continue
|
||||||
tj = _TA_C + p * theta
|
tj = _TA_C + p * theta
|
||||||
status = "WARNING" if tj >= _TJ_WARN_C else "INFO"
|
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(
|
out.append(Finding(
|
||||||
designator=ref,
|
designator=ref,
|
||||||
mpn=comp.mpn or "",
|
mpn=comp.mpn or "",
|
||||||
@@ -251,7 +251,7 @@ def _resistor_thermal(graph: DesignGraph) -> list[Finding]:
|
|||||||
reference="resistor power rating",
|
reference="resistor power rating",
|
||||||
net=net,
|
net=net,
|
||||||
pins=[rref],
|
pins=[rref],
|
||||||
rule_id="PS-TH-003",
|
rule_id="PE-TH-003",
|
||||||
))
|
))
|
||||||
|
|
||||||
for ref, comp in sorted(graph.components.items()):
|
for ref, comp in sorted(graph.components.items()):
|
||||||
@@ -293,6 +293,6 @@ def _resistor_thermal(graph: DesignGraph) -> list[Finding]:
|
|||||||
reference="resistor power rating",
|
reference="resistor power rating",
|
||||||
net=nets[0],
|
net=nets[0],
|
||||||
pins=[ref],
|
pins=[ref],
|
||||||
rule_id="PS-TH-003",
|
rule_id="PE-TH-003",
|
||||||
))
|
))
|
||||||
return out
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ from dotenv import load_dotenv
|
|||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
from backend.pinscopex.models import (
|
from backend.periscopex.models import (
|
||||||
ComponentConstraints,
|
ComponentConstraints,
|
||||||
ComponentType,
|
ComponentType,
|
||||||
DesignGraph,
|
DesignGraph,
|
||||||
@@ -28,9 +28,9 @@ from backend.pinscopex.models import (
|
|||||||
NetType,
|
NetType,
|
||||||
ValidationReport,
|
ValidationReport,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.pin_function_tokens import parse_net_token
|
from backend.periscopex.pin_function_tokens import parse_net_token
|
||||||
from backend.pinscopex.quote_verify import verify_finding_citations
|
from backend.periscopex.quote_verify import verify_finding_citations
|
||||||
from backend.pinscopex.validation_tools import (
|
from backend.periscopex.validation_tools import (
|
||||||
ALL_TOOLS,
|
ALL_TOOLS,
|
||||||
SUBMIT_REVIEW_SCHEMA,
|
SUBMIT_REVIEW_SCHEMA,
|
||||||
ConstraintsMap,
|
ConstraintsMap,
|
||||||
@@ -136,7 +136,7 @@ INFO (worth noting but unlikely to cause problems).
|
|||||||
- **source_quote**: The exact verbatim sentence or clause from the datasheet \
|
- **source_quote**: The exact verbatim sentence or clause from the datasheet \
|
||||||
that states the requirement. Copy it precisely, character-for-character (a \
|
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. \
|
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 \
|
quote against the extracted text of the cited page (±1); invented or \
|
||||||
paraphrased quotes are demoted to Unverified WARNING. Omit the field only \
|
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 \
|
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",
|
model: str = "claude-sonnet-4-6",
|
||||||
) -> ValidationReport:
|
) -> ValidationReport:
|
||||||
"""Load graph, review every IC against its datasheet, write report."""
|
"""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())
|
raw = json.loads(Path(graph_path).read_text())
|
||||||
graph = DesignGraph.model_validate(raw)
|
graph = DesignGraph.model_validate(raw)
|
||||||
@@ -13,11 +13,11 @@ from dataclasses import dataclass, field
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from backend.pinscopex.models import (
|
from backend.periscopex.models import (
|
||||||
ComponentConstraints,
|
ComponentConstraints,
|
||||||
DesignGraph,
|
DesignGraph,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.utils import safe_mpn
|
from backend.periscopex.utils import safe_mpn
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -509,7 +509,7 @@ def get_datasheet_excerpt(
|
|||||||
return ("get_datasheet_excerpt called without per-review state — "
|
return ("get_datasheet_excerpt called without per-review state — "
|
||||||
"this is a bug, no excerpt returned.", None)
|
"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
|
from backend.services.llm import PdfBlock
|
||||||
|
|
||||||
designator = (designator or "").strip()
|
designator = (designator or "").strip()
|
||||||
@@ -776,7 +776,7 @@ SUBMIT_REVIEW_SCHEMA = {
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": (
|
"description": (
|
||||||
"Required for ERROR and WARNING. Exact verbatim "
|
"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 "
|
"checks it against the PDF page. Omit only if "
|
||||||
"the evidence is a figure/scan with no text."
|
"the evidence is a figure/scan with no text."
|
||||||
),
|
),
|
||||||
@@ -15,7 +15,7 @@ from fastapi.responses import JSONResponse
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from backend.config import settings
|
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.routers.deps import get_storage
|
||||||
from backend.services import admin_settings as settings_svc
|
from backend.services import admin_settings as settings_svc
|
||||||
from backend.services.billing_hook import get_billing
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|||||||
@@ -41,10 +41,10 @@ class ContactResponse(BaseModel):
|
|||||||
def _build_contact_message(data: ContactRequest) -> MIMEMultipart:
|
def _build_contact_message(data: ContactRequest) -> MIMEMultipart:
|
||||||
"""Build the contact form email."""
|
"""Build the contact form email."""
|
||||||
msg = MIMEMultipart("alternative")
|
msg = MIMEMultipart("alternative")
|
||||||
msg["From"] = f"Pinscope <{settings.email_sender}>"
|
msg["From"] = f"Periscope <{settings.email_sender}>"
|
||||||
msg["To"] = settings.contact_recipient
|
msg["To"] = settings.contact_recipient
|
||||||
msg["Reply-To"] = data.email
|
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
|
# Plain text
|
||||||
lines = [
|
lines = [
|
||||||
@@ -55,7 +55,7 @@ def _build_contact_message(data: ContactRequest) -> MIMEMultipart:
|
|||||||
lines.append(f"Company: {data.company}")
|
lines.append(f"Company: {data.company}")
|
||||||
if data.subject:
|
if data.subject:
|
||||||
lines.append(f"Subject: {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"))
|
msg.attach(MIMEText("\n".join(lines), "plain"))
|
||||||
|
|
||||||
# HTML
|
# HTML
|
||||||
@@ -94,7 +94,7 @@ def _build_contact_message(data: ContactRequest) -> MIMEMultipart:
|
|||||||
{rows}
|
{rows}
|
||||||
</table>
|
</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>
|
<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>"""
|
</div>"""
|
||||||
msg.attach(MIMEText(html_body, "html"))
|
msg.attach(MIMEText(html_body, "html"))
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from typing import Literal
|
|||||||
from fastapi import APIRouter, HTTPException, Request
|
from fastapi import APIRouter, HTTPException, Request
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from backend.pinscopex.impedance import (
|
from backend.periscopex.impedance import (
|
||||||
GeometryError,
|
GeometryError,
|
||||||
TraceGeometry,
|
TraceGeometry,
|
||||||
coupled_diff_z,
|
coupled_diff_z,
|
||||||
@@ -21,13 +21,13 @@ from backend.pinscopex.impedance import (
|
|||||||
stackup_targets,
|
stackup_targets,
|
||||||
stripline_z0,
|
stripline_z0,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.impedance_traces import (
|
from backend.periscopex.impedance_traces import (
|
||||||
NET_WALK_PITCH_MM,
|
NET_WALK_PITCH_MM,
|
||||||
analyze_specified_nets,
|
analyze_specified_nets,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.antenna_rf import build_antenna_report, build_design_recipe
|
from backend.periscopex.antenna_rf import build_antenna_report, build_design_recipe
|
||||||
from backend.pinscopex.models import DesignGraph, LayoutGraph
|
from backend.periscopex.models import DesignGraph, LayoutGraph
|
||||||
from backend.pinscopex.parsers_kicad_pcb import parse_kicad_pcb
|
from backend.periscopex.parsers_kicad_pcb import parse_kicad_pcb
|
||||||
from backend.routers.deps import get_storage, resolve_or_404
|
from backend.routers.deps import get_storage, resolve_or_404
|
||||||
from backend.services import projects as proj_svc
|
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`` ->
|
# Idempotent enqueue: only one ``draft|complete|error|cancelled`` ->
|
||||||
# ``queued`` transition can win. Concurrent /start clicks => 409.
|
# ``queued`` transition can win. Concurrent /start clicks => 409.
|
||||||
from backend._version import PINSCOPE_VERSION
|
from backend._version import PERISCOPE_VERSION
|
||||||
try:
|
try:
|
||||||
proj_svc.transition_status(
|
proj_svc.transition_status(
|
||||||
storage, owner_user_id, project_id,
|
storage, owner_user_id, project_id,
|
||||||
@@ -92,7 +92,7 @@ async def start(project_id: str, request: Request):
|
|||||||
to_status=proj_svc.STATUS_QUEUED,
|
to_status=proj_svc.STATUS_QUEUED,
|
||||||
cancel_requested=False,
|
cancel_requested=False,
|
||||||
execution_name=None,
|
execution_name=None,
|
||||||
pinscope_version=PINSCOPE_VERSION,
|
periscope_version=PERISCOPE_VERSION,
|
||||||
)
|
)
|
||||||
except proj_svc.StatusConflict:
|
except proj_svc.StatusConflict:
|
||||||
raise HTTPException(409, "Pipeline already running or queued")
|
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
|
(default) skips ICs that already produced a review; ``all`` re-reviews
|
||||||
every IC.
|
every IC.
|
||||||
"""
|
"""
|
||||||
from backend._version import PINSCOPE_VERSION
|
from backend._version import PERISCOPE_VERSION
|
||||||
|
|
||||||
storage = get_storage(request)
|
storage = get_storage(request)
|
||||||
owner_user_id, meta = await resolve_or_404(request, project_id)
|
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_checkpoint=None,
|
||||||
pause_reason=None,
|
pause_reason=None,
|
||||||
completed_review_refs=keep_refs,
|
completed_review_refs=keep_refs,
|
||||||
pinscope_version=PINSCOPE_VERSION,
|
periscope_version=PERISCOPE_VERSION,
|
||||||
)
|
)
|
||||||
except proj_svc.StatusConflict:
|
except proj_svc.StatusConflict:
|
||||||
raise HTTPException(409, "Pipeline already running or queued")
|
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
|
MAX_UPLOAD_BYTES = 30 * 1024 * 1024 # 30 MB
|
||||||
|
|
||||||
from backend.config import settings
|
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.routers.deps import get_storage, get_user_id, resolve_or_404
|
||||||
from backend.services import projects as proj_svc
|
from backend.services import projects as proj_svc
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ async def check_library(req: LibraryCheckRequest, request: Request):
|
|||||||
|
|
||||||
passive_resolved: list[str] = []
|
passive_resolved: list[str] = []
|
||||||
if req.passive_mpns:
|
if req.passive_mpns:
|
||||||
from backend.pinscopex.resolve_passives import resolve_mpn
|
from backend.periscopex.resolve_passives import resolve_mpn
|
||||||
|
|
||||||
passive_resolved = [
|
passive_resolved = [
|
||||||
mpn for mpn in req.passive_mpns
|
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
|
``selected`` list (None = "include everything") so the wizard can render
|
||||||
the picker pre-populated.
|
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
|
import tempfile, os
|
||||||
|
|
||||||
storage = get_storage(request)
|
storage = get_storage(request)
|
||||||
@@ -360,7 +360,7 @@ async def upload_bom(
|
|||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from backend.pinscopex.parsers import parse_bom
|
from backend.periscopex.parsers import parse_bom
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv")
|
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv")
|
||||||
@@ -420,7 +420,7 @@ async def upload_bom(
|
|||||||
# simple → datasheet upload). Mirrors the bucket logic in
|
# simple → datasheet upload). Mirrors the bucket logic in
|
||||||
# services/pipeline.py:_stage_bom_parse so the field is correct after
|
# services/pipeline.py:_stage_bom_parse so the field is correct after
|
||||||
# either path runs.
|
# 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] = []
|
ic_mpns: list[str] = []
|
||||||
passive_mpns: list[str] = []
|
passive_mpns: list[str] = []
|
||||||
@@ -511,9 +511,9 @@ async def upload_netlist(
|
|||||||
raise HTTPException(404, "Project not found")
|
raise HTTPException(404, "Project not found")
|
||||||
user_id = result[0] # owner_user_id for storage paths
|
user_id = result[0] # owner_user_id for storage paths
|
||||||
|
|
||||||
from backend.pinscopex.netlist_bundle import materialize_netlist_upload
|
from backend.periscopex.netlist_bundle import materialize_netlist_upload
|
||||||
from backend.pinscopex.parsers import parse_netlist_any, validate_netlist
|
from backend.periscopex.parsers import parse_netlist_any, validate_netlist
|
||||||
from backend.pinscopex.parsers_edif import list_edif_subdesigns
|
from backend.periscopex.parsers_edif import list_edif_subdesigns
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
blobs: list[tuple[str, bytes]] = []
|
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:
|
if len(data) > MAX_UPLOAD_BYTES:
|
||||||
raise HTTPException(413, f"File too large (max {MAX_UPLOAD_BYTES // 1024 // 1024} MB)")
|
raise HTTPException(413, f"File too large (max {MAX_UPLOAD_BYTES // 1024 // 1024} MB)")
|
||||||
import tempfile, os
|
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")
|
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".kicad_pcb")
|
||||||
try:
|
try:
|
||||||
@@ -647,7 +647,7 @@ def _build_designator_pins(
|
|||||||
(natural sort on refs and on pin numbers) so the wizard's dropdowns
|
(natural sort on refs and on pin numbers) so the wizard's dropdowns
|
||||||
look identical regardless of netlist format.
|
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}
|
by_ref: dict[str, dict[str, str]] = {ref: {} for ref in parts}
|
||||||
for net_name, pins in nets.items():
|
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.
|
# 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.
|
# 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()
|
api_logger = ApiLogger()
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
tax_dir = Path(tmpdir) / "taxonomy"
|
tax_dir = Path(tmpdir) / "taxonomy"
|
||||||
|
|||||||
@@ -11,15 +11,15 @@ from fastapi import APIRouter, HTTPException, Request
|
|||||||
from fastapi.responses import JSONResponse, Response
|
from fastapi.responses import JSONResponse, Response
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from backend.pinscopex.models import Finding
|
from backend.periscopex.models import Finding
|
||||||
from backend.pinscopex.review_workflow import (
|
from backend.periscopex.review_workflow import (
|
||||||
ReviewError,
|
ReviewError,
|
||||||
apply_review_state,
|
apply_review_state,
|
||||||
build_eco,
|
build_eco,
|
||||||
eco_csv,
|
eco_csv,
|
||||||
sign_report,
|
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.routers.deps import get_storage, get_user_id, resolve_or_404
|
||||||
from backend.services import projects as proj_svc
|
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)
|
storage = get_storage(request)
|
||||||
owner_user_id, _ = await resolve_or_404(request, project_id)
|
owner_user_id, _ = await resolve_or_404(request, project_id)
|
||||||
prefix = proj_svc.project_prefix(owner_user_id, 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):
|
if not storage.exists(key):
|
||||||
raise HTTPException(404, "CAD bridge not found — run the pipeline first")
|
raise HTTPException(404, "CAD bridge not found — run the pipeline first")
|
||||||
return JSONResponse(storage.read_json(key))
|
return JSONResponse(storage.read_json(key))
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ from typing import Any, Literal
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from backend.config import settings
|
from backend.config import settings
|
||||||
from backend.pinscopex.parsers import parse_bom
|
from backend.periscopex.parsers import parse_bom
|
||||||
from backend.pinscopex.resolve_passives import resolve_mpn
|
from backend.periscopex.resolve_passives import resolve_mpn
|
||||||
from backend.pinscopex.taxonomy import SIMPLE_TYPES, type_for_ref
|
from backend.periscopex.taxonomy import SIMPLE_TYPES, type_for_ref
|
||||||
from backend.pinscopex.utils import safe_mpn
|
from backend.periscopex.utils import safe_mpn
|
||||||
from backend.services import projects as proj_svc
|
from backend.services import projects as proj_svc
|
||||||
from backend.services.billing_hook import get_billing
|
from backend.services.billing_hook import get_billing
|
||||||
from backend.services.llm.pricing import CACHE_RATES, PRICING
|
from backend.services.llm.pricing import CACHE_RATES, PRICING
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Automatic datasheet lookup — LCSC, manufacturer URLs, optional DigiKey.
|
"""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
|
only auto-fetched via DigiKey, which requires paid API keys and often
|
||||||
fails when the manufacturer CDN blocks the download.
|
fails when the manufacturer CDN blocks the download.
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ _PDF_MAGIC = b"%PDF-"
|
|||||||
_MIN_PDF_SIZE = 5_000
|
_MIN_PDF_SIZE = 5_000
|
||||||
_UA = (
|
_UA = (
|
||||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
|
"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"
|
_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
|
``ESP32-S31-WROOM-3`` matches ``ESP32-S31-WROOM-3-N16R16V.pdf`` and the
|
||||||
reverse — packing / flash-size suffixes, not sibling dies (CH340 vs CH340E).
|
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():
|
if not mpn or not pdf_dir.is_dir():
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from __future__ import annotations
|
|||||||
import hashlib
|
import hashlib
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from backend.pinscopex.utils import safe_mpn
|
from backend.periscopex.utils import safe_mpn
|
||||||
from backend.services.storage import StorageBackend
|
from backend.services.storage import StorageBackend
|
||||||
|
|
||||||
BLOB_PREFIX = "library/datasheets/blobs/"
|
BLOB_PREFIX = "library/datasheets/blobs/"
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import time
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Awaitable, Callable
|
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.api_logs import ApiLogger
|
||||||
from backend.services.llm import Message, TextBlock
|
from backend.services.llm import Message, TextBlock
|
||||||
from backend.services.llm.factory import call_with_fallback
|
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">
|
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||||
<tr>
|
<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;">
|
<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>
|
||||||
<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;">
|
<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
|
Report Ready
|
||||||
@@ -247,7 +247,7 @@ def _render_report_email(
|
|||||||
<tr><td style="background-color: #f9fafb; padding: 20px 32px; border-top: 1px solid #e5e7eb;">
|
<tr><td style="background-color: #f9fafb; padding: 20px 32px; border-top: 1px solid #e5e7eb;">
|
||||||
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
<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;">
|
<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>
|
</td></tr>
|
||||||
</table>
|
</table>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
@@ -292,7 +292,7 @@ def _render_pipeline_started_email(
|
|||||||
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||||
<tr>
|
<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;">
|
<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>
|
||||||
<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;">
|
<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
|
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;">
|
<tr><td style="background-color: #f9fafb; padding: 20px 32px; border-top: 1px solid #e5e7eb;">
|
||||||
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
<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;">
|
<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>
|
</td></tr>
|
||||||
</table>
|
</table>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
@@ -448,7 +448,7 @@ def _build_report_message(
|
|||||||
) -> MIMEMultipart:
|
) -> MIMEMultipart:
|
||||||
"""Build the report-ready email message."""
|
"""Build the report-ready email message."""
|
||||||
msg = MIMEMultipart("alternative")
|
msg = MIMEMultipart("alternative")
|
||||||
msg["From"] = f"Pinscope <{settings.email_sender}>"
|
msg["From"] = f"Periscope <{settings.email_sender}>"
|
||||||
msg["To"] = to_email
|
msg["To"] = to_email
|
||||||
msg["Subject"] = f"Report ready: {project_name}"
|
msg["Subject"] = f"Report ready: {project_name}"
|
||||||
|
|
||||||
@@ -460,7 +460,7 @@ def _build_report_message(
|
|||||||
infos = summary.get("INFO", 0)
|
infos = summary.get("INFO", 0)
|
||||||
text_body = (
|
text_body = (
|
||||||
f"Hi {recipient_name},\n\n"
|
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"Summary: {total} findings — {errors} errors, {warnings} warnings, {infos} info\n\n"
|
||||||
f"View the report: {report_url}\n"
|
f"View the report: {report_url}\n"
|
||||||
)
|
)
|
||||||
@@ -485,7 +485,7 @@ def _build_paused_message(
|
|||||||
credits_needed_low: float,
|
credits_needed_low: float,
|
||||||
) -> MIMEMultipart:
|
) -> MIMEMultipart:
|
||||||
msg = MIMEMultipart("alternative")
|
msg = MIMEMultipart("alternative")
|
||||||
msg["From"] = f"Pinscope <{settings.email_sender}>"
|
msg["From"] = f"Periscope <{settings.email_sender}>"
|
||||||
msg["To"] = to_email
|
msg["To"] = to_email
|
||||||
msg["Subject"] = f"Paused: {project_name} is waiting for credits"
|
msg["Subject"] = f"Paused: {project_name} is waiting for credits"
|
||||||
|
|
||||||
@@ -495,7 +495,7 @@ def _build_paused_message(
|
|||||||
|
|
||||||
text_body = (
|
text_body = (
|
||||||
f"Hi {recipient_name},\n\n"
|
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"{last_line}\n{stage_line}\n\n"
|
||||||
f"Current balance: {balance:.2f} credits\n"
|
f"Current balance: {balance:.2f} credits\n"
|
||||||
f"Credits needed to finish (est): {credits_needed_low:.2f}+\n\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,
|
amount_usd: float, reason: str,
|
||||||
) -> MIMEMultipart:
|
) -> MIMEMultipart:
|
||||||
msg = MIMEMultipart("alternative")
|
msg = MIMEMultipart("alternative")
|
||||||
msg["From"] = f"Pinscope <{settings.email_sender}>"
|
msg["From"] = f"Periscope <{settings.email_sender}>"
|
||||||
msg["To"] = to_email
|
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"
|
manage_url = f"{settings.email_frontend_url}/credits"
|
||||||
text_body = (
|
text_body = (
|
||||||
f"Hi {recipient_name},\n\n"
|
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"${amount_usd:.2f} but the charge failed.\n\n"
|
||||||
f"Reason: {reason}\n\n"
|
f"Reason: {reason}\n\n"
|
||||||
f"Auto top-up has been disabled until you update your payment method. "
|
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,
|
to_email: str, recipient_name: str, balance: float, threshold: float,
|
||||||
) -> MIMEMultipart:
|
) -> MIMEMultipart:
|
||||||
msg = MIMEMultipart("alternative")
|
msg = MIMEMultipart("alternative")
|
||||||
msg["From"] = f"Pinscope <{settings.email_sender}>"
|
msg["From"] = f"Periscope <{settings.email_sender}>"
|
||||||
msg["To"] = to_email
|
msg["To"] = to_email
|
||||||
msg["Subject"] = "Pinscope: low credit balance"
|
msg["Subject"] = "Periscope: low credit balance"
|
||||||
credits_url = f"{settings.email_frontend_url}/credits"
|
credits_url = f"{settings.email_frontend_url}/credits"
|
||||||
text_body = (
|
text_body = (
|
||||||
f"Hi {recipient_name},\n\n"
|
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"{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"
|
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"
|
result["step"] = "send"
|
||||||
msg = MIMEMultipart("alternative")
|
msg = MIMEMultipart("alternative")
|
||||||
msg["From"] = f"Pinscope <{settings.email_sender}>"
|
msg["From"] = f"Periscope <{settings.email_sender}>"
|
||||||
msg["To"] = to_email
|
msg["To"] = to_email
|
||||||
msg["Subject"] = "Pinscope email test"
|
msg["Subject"] = "Periscope email test"
|
||||||
msg.attach(MIMEText(f"Test email from Pinscope. Sender: {settings.email_sender}. Creds: {cred_type}", "plain"))
|
msg.attach(MIMEText(f"Test email from Periscope. Sender: {settings.email_sender}. Creds: {cred_type}", "plain"))
|
||||||
|
|
||||||
import asyncio as _asyncio
|
import asyncio as _asyncio
|
||||||
raw = base64.urlsafe_b64encode(msg.as_bytes()).decode("ascii")
|
raw = base64.urlsafe_b64encode(msg.as_bytes()).decode("ascii")
|
||||||
@@ -741,7 +741,7 @@ async def send_pipeline_started_email(
|
|||||||
|
|
||||||
# Build message
|
# Build message
|
||||||
msg = MIMEMultipart("alternative")
|
msg = MIMEMultipart("alternative")
|
||||||
msg["From"] = f"Pinscope <{settings.email_sender}>"
|
msg["From"] = f"Periscope <{settings.email_sender}>"
|
||||||
msg["To"] = to_email
|
msg["To"] = to_email
|
||||||
msg["Subject"] = f"Pipeline started: {project_name} ({num_components} components)"
|
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">
|
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||||
<tr>
|
<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;">
|
<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>
|
||||||
<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;">
|
<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
|
Feedback Received
|
||||||
@@ -953,7 +953,7 @@ def _render_feedback_email(
|
|||||||
<tr><td style="background-color: #f9fafb; padding: 20px 32px; border-top: 1px solid #e5e7eb;">
|
<tr><td style="background-color: #f9fafb; padding: 20px 32px; border-top: 1px solid #e5e7eb;">
|
||||||
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
<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;">
|
<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>
|
</td></tr>
|
||||||
</table>
|
</table>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
@@ -1006,7 +1006,7 @@ async def send_feedback_received_email(
|
|||||||
to_email = settings.email_admin_notify
|
to_email = settings.email_admin_notify
|
||||||
subject_ctx = project_name or "general"
|
subject_ctx = project_name or "general"
|
||||||
msg = MIMEMultipart("alternative")
|
msg = MIMEMultipart("alternative")
|
||||||
msg["From"] = f"Pinscope <{settings.email_sender}>"
|
msg["From"] = f"Periscope <{settings.email_sender}>"
|
||||||
msg["To"] = to_email
|
msg["To"] = to_email
|
||||||
msg["Subject"] = f"Feedback ({type_label}): {subject_ctx}"
|
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">
|
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||||
<tr>
|
<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;">
|
<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>
|
||||||
<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;">
|
<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
|
New Reply
|
||||||
@@ -1113,7 +1113,7 @@ def _render_feedback_reply_email(
|
|||||||
Hi {_esc(recipient_first_name)},
|
Hi {_esc(recipient_first_name)},
|
||||||
</td></tr>
|
</td></tr>
|
||||||
<tr><td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; color: #374151; padding-bottom: 18px;">
|
<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>
|
</td></tr>
|
||||||
|
|
||||||
{context_line}
|
{context_line}
|
||||||
@@ -1124,7 +1124,7 @@ def _render_feedback_reply_email(
|
|||||||
<tr><td style="padding: 18px 22px;">
|
<tr><td style="padding: 18px 22px;">
|
||||||
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
<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;">
|
<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>
|
</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;">
|
<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)}
|
{_esc(reply_text)}
|
||||||
@@ -1155,12 +1155,12 @@ def _render_feedback_reply_email(
|
|||||||
<!--[if mso]>
|
<!--[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">
|
<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/>
|
<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>
|
</v:roundrect>
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
<!--[if !mso]><!-->
|
<!--[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;">
|
<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>
|
</a>
|
||||||
<!--<![endif]-->
|
<!--<![endif]-->
|
||||||
</td></tr>
|
</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.
|
Thank you so much for taking the time to share your feedback — we truly value it.
|
||||||
</td></tr>
|
</td></tr>
|
||||||
<tr><td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; color: #374151; padding-top: 6px;">
|
<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>
|
</td></tr>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
@@ -1180,7 +1180,7 @@ def _render_feedback_reply_email(
|
|||||||
<tr><td style="background-color: #f9fafb; padding: 20px 32px; border-top: 1px solid #e5e7eb;">
|
<tr><td style="background-color: #f9fafb; padding: 20px 32px; border-top: 1px solid #e5e7eb;">
|
||||||
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
<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;">
|
<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>
|
</td></tr>
|
||||||
</table>
|
</table>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
@@ -1203,7 +1203,7 @@ async def send_feedback_reply_email(
|
|||||||
finding_designator: str | None = None,
|
finding_designator: str | None = None,
|
||||||
finding_mpn: str | None = None,
|
finding_mpn: str | None = 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:
|
if not settings.use_email:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1229,15 +1229,15 @@ async def send_feedback_reply_email(
|
|||||||
first_name = full_name.split()[0] if full_name else "there"
|
first_name = full_name.split()[0] if full_name else "there"
|
||||||
|
|
||||||
msg = MIMEMultipart("alternative")
|
msg = MIMEMultipart("alternative")
|
||||||
msg["From"] = f"Pinscope <{settings.email_sender}>"
|
msg["From"] = f"Periscope <{settings.email_sender}>"
|
||||||
msg["To"] = to_email
|
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
|
# Plain text fallback
|
||||||
text_lines = [
|
text_lines = [
|
||||||
f"Hi {first_name},",
|
f"Hi {first_name},",
|
||||||
"",
|
"",
|
||||||
"The Pinscope team just replied to your feedback.",
|
"The Periscope team just replied to your feedback.",
|
||||||
"",
|
"",
|
||||||
"— Reply —",
|
"— Reply —",
|
||||||
reply_text,
|
reply_text,
|
||||||
@@ -1245,10 +1245,10 @@ async def send_feedback_reply_email(
|
|||||||
"— Your original message —",
|
"— Your original message —",
|
||||||
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.",
|
"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"))
|
msg.attach(MIMEText("\n".join(text_lines), "plain"))
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import tempfile
|
|||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from backend.pinscopex.utils import safe_mpn
|
from backend.periscopex.utils import safe_mpn
|
||||||
from backend.pinscopex.models import (
|
from backend.periscopex.models import (
|
||||||
CapacitorSpecs,
|
CapacitorSpecs,
|
||||||
ComponentConstraints,
|
ComponentConstraints,
|
||||||
ComponentModel,
|
ComponentModel,
|
||||||
@@ -27,7 +27,7 @@ from backend.pinscopex.models import (
|
|||||||
NetType,
|
NetType,
|
||||||
SimpleComponentSpecs,
|
SimpleComponentSpecs,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.taxonomy import (
|
from backend.periscopex.taxonomy import (
|
||||||
TAXONOMY_DIR,
|
TAXONOMY_DIR,
|
||||||
add_subtype,
|
add_subtype,
|
||||||
format_for_prompt,
|
format_for_prompt,
|
||||||
@@ -399,13 +399,13 @@ def _coerce_abs_max(raw: object) -> list[dict]:
|
|||||||
|
|
||||||
|
|
||||||
def _coerce_layout_rules(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 [])
|
rows, _errors = validate_layout_rules(raw if isinstance(raw, list) else [])
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
def _coerce_internal_features(raw: object):
|
def _coerce_internal_features(raw: object):
|
||||||
from backend.pinscopex.models import InternalFeatures
|
from backend.periscopex.models import InternalFeatures
|
||||||
if not isinstance(raw, dict):
|
if not isinstance(raw, dict):
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
@@ -1088,7 +1088,7 @@ async def auto_resolve_specs(
|
|||||||
|
|
||||||
# Convert passive SimpleComponentSpecs to typed models
|
# Convert passive SimpleComponentSpecs to typed models
|
||||||
if component_type == "passive":
|
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)
|
typed = simple_to_typed_passive_specs(specs)
|
||||||
return ComponentModel(mpn=mpn, specs=typed)
|
return ComponentModel(mpn=mpn, specs=typed)
|
||||||
|
|
||||||
@@ -1260,7 +1260,7 @@ async def resolve_from_value(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if component_type == "passive":
|
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)
|
typed = simple_to_typed_passive_specs(specs)
|
||||||
return ComponentModel(mpn=mpn, specs=typed)
|
return ComponentModel(mpn=mpn, specs=typed)
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ def load_skill_validator(skill_name: str):
|
|||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
return None
|
return None
|
||||||
spec = importlib.util.spec_from_file_location(
|
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:
|
if spec is None or spec.loader is None:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from backend.pinscopex.pdf_text import (
|
from backend.periscopex.pdf_text import (
|
||||||
extract_pdf_document_text,
|
extract_pdf_document_text,
|
||||||
fitz_page_text,
|
fitz_page_text,
|
||||||
page_is_sparse,
|
page_is_sparse,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Pinscope local JWT helpers (HS256)."""
|
"""Periscope local JWT helpers (HS256)."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ def issue_token(user_id: str, email: str) -> str:
|
|||||||
payload = {
|
payload = {
|
||||||
"sub": user_id,
|
"sub": user_id,
|
||||||
"email": email,
|
"email": email,
|
||||||
"iss": "pinscope-local",
|
"iss": "periscope-local",
|
||||||
"iat": now,
|
"iat": now,
|
||||||
"exp": now + timedelta(days=TOKEN_TTL_DAYS),
|
"exp": now + timedelta(days=TOKEN_TTL_DAYS),
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ def decode_token(token: str) -> dict[str, Any] | None:
|
|||||||
token,
|
token,
|
||||||
secret,
|
secret,
|
||||||
algorithms=[ALGORITHM],
|
algorithms=[ALGORITHM],
|
||||||
issuer="pinscope-local",
|
issuer="periscope-local",
|
||||||
options={"verify_aud": False},
|
options={"verify_aud": False},
|
||||||
leeway=10,
|
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.
|
Users live under ``data/auth/users/{user_id}.json`` with an email index.
|
||||||
Passwords use stdlib ``hashlib.scrypt``.
|
Passwords use stdlib ``hashlib.scrypt``.
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from datetime import datetime, timezone
|
|||||||
from typing import Awaitable, Callable
|
from typing import Awaitable, Callable
|
||||||
|
|
||||||
from backend.config import settings
|
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.api_logs import ApiLogger
|
||||||
from backend.services.llm import Message, TextBlock
|
from backend.services.llm import Message, TextBlock
|
||||||
from backend.services.llm.factory import call_with_fallback
|
from backend.services.llm.factory import call_with_fallback
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from backend.pinscopex.models import ComponentModel, SimpleComponentSpecs
|
from backend.periscopex.models import ComponentModel, SimpleComponentSpecs
|
||||||
from backend.pinscopex.resolve_passives import simple_to_typed_passive_specs
|
from backend.periscopex.resolve_passives import simple_to_typed_passive_specs
|
||||||
|
|
||||||
_CAP = re.compile(
|
_CAP = re.compile(
|
||||||
r"(?P<num>\d+(?:\.\d+)?)\s*(?P<mul>[pnuμµmk])?\s*[fF]\b",
|
r"(?P<num>\d+(?:\.\d+)?)\s*(?P<mul>[pnuμµmk])?\s*[fF]\b",
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from backend.pinscopex.models import ComponentModel, SimpleComponentSpecs
|
from backend.periscopex.models import ComponentModel, SimpleComponentSpecs
|
||||||
from backend.pinscopex.resolve_passives import simple_to_typed_passive_specs
|
from backend.periscopex.resolve_passives import simple_to_typed_passive_specs
|
||||||
from backend.services.passive_from_distributor import _spice
|
from backend.services.passive_from_distributor import _spice
|
||||||
|
|
||||||
_SIZE = r"(?:0201|0402|0603|0805|1206|1210|1812|2010|2512)"
|
_SIZE = r"(?:0201|0402|0603|0805|1206|1210|1812|2010|2512)"
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from backend.pinscopex.models import ComponentModel, SimpleComponentSpecs
|
from backend.periscopex.models import ComponentModel, SimpleComponentSpecs
|
||||||
from backend.pinscopex.resolve_passives import simple_to_typed_passive_specs
|
from backend.periscopex.resolve_passives import simple_to_typed_passive_specs
|
||||||
from backend.services.passive_from_distributor import _spice
|
from backend.services.passive_from_distributor import _spice
|
||||||
|
|
||||||
_PLACEHOLDER = re.compile(
|
_PLACEHOLDER = re.compile(
|
||||||
|
|||||||
@@ -33,15 +33,15 @@ from pathlib import Path
|
|||||||
from typing import Any, Awaitable, Callable
|
from typing import Any, Awaitable, Callable
|
||||||
|
|
||||||
|
|
||||||
from backend.pinscopex.models import ComponentType
|
from backend.periscopex.models import ComponentType
|
||||||
from backend.pinscopex.utils import natural_sort_key, safe_mpn
|
from backend.periscopex.utils import natural_sort_key, safe_mpn
|
||||||
from backend.pinscopex.bom_summary import build_bom_summary
|
from backend.periscopex.bom_summary import build_bom_summary
|
||||||
from backend.pinscopex.derating import build_derating_table
|
from backend.periscopex.derating import build_derating_table
|
||||||
from backend.pinscopex.validate import _load_datasheets
|
from backend.periscopex.validate import _load_datasheets
|
||||||
from backend.pinscopex.graph import build_graph
|
from backend.periscopex.graph import build_graph
|
||||||
from backend.pinscopex.parsers import parse_bom, parse_netlist_any
|
from backend.periscopex.parsers import parse_bom, parse_netlist_any
|
||||||
from backend.pinscopex.resolve_passives import SkippedItem, load_patterns, resolve_mpn
|
from backend.periscopex.resolve_passives import SkippedItem, load_patterns, resolve_mpn
|
||||||
from backend.pinscopex.taxonomy import SIMPLE_TYPES, type_for_ref
|
from backend.periscopex.taxonomy import SIMPLE_TYPES, type_for_ref
|
||||||
|
|
||||||
from backend.config import settings
|
from backend.config import settings
|
||||||
from backend.services import admin_settings as settings_svc
|
from backend.services import admin_settings as settings_svc
|
||||||
@@ -196,7 +196,7 @@ def _cancel_gate_check(ctx: PipelineContext) -> None:
|
|||||||
class PipelineWorkspace:
|
class PipelineWorkspace:
|
||||||
"""Downloads project files from storage to a temp dir for pipeline execution.
|
"""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.
|
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("bom_summary.json")
|
||||||
self._upload_file("derating.json")
|
self._upload_file("derating.json")
|
||||||
self._upload_file("report.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("review_fingerprints.json")
|
||||||
self._upload_file("api_logs.jsonl")
|
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
|
# Pre-categorize: workspace cache, library cache, or needs extraction
|
||||||
from backend.config import settings as app_settings
|
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()
|
layout_scan_ver = app_settings.get_default_model_version()
|
||||||
_ic_cache: dict[str, tuple] = {}
|
_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"
|
extracted_key = f"{ctx.ws.prefix}/extracted/{safe}.json"
|
||||||
ctx.storage.upload_from_local(json_path, extracted_key)
|
ctx.storage.upload_from_local(json_path, extracted_key)
|
||||||
try:
|
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"))
|
payload = json.loads(json_path.read_text(encoding="utf-8"))
|
||||||
ok, reason = should_promote_extraction(payload)
|
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():
|
if not pcb.is_file():
|
||||||
return
|
return
|
||||||
try:
|
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)
|
layout = parse_kicad_pcb(pcb)
|
||||||
out = ws.local_path("layout_graph.json")
|
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:
|
def _write_functional_groups(ws: PipelineWorkspace, graph) -> None:
|
||||||
"""Layout F1: topology domains/groups (no mm). Fail-soft."""
|
"""Layout F1: topology domains/groups (no mm). Fail-soft."""
|
||||||
try:
|
try:
|
||||||
from backend.pinscopex.functional_groups import build_functional_groups
|
from backend.periscopex.functional_groups import build_functional_groups
|
||||||
from backend.pinscopex.validate import _build_constraints_map, _load_datasheets
|
from backend.periscopex.validate import _build_constraints_map, _load_datasheets
|
||||||
|
|
||||||
extracted_dir = ws.local_path("extracted")
|
extracted_dir = ws.local_path("extracted")
|
||||||
cmap = {}
|
cmap = {}
|
||||||
@@ -1584,8 +1584,8 @@ def _write_impedance_nets(ws: PipelineWorkspace, graph) -> None:
|
|||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
from backend.pinscopex.impedance_traces import analyze_where_needed
|
from backend.periscopex.impedance_traces import analyze_where_needed
|
||||||
from backend.pinscopex.models import LayoutGraph
|
from backend.periscopex.models import LayoutGraph
|
||||||
|
|
||||||
layout = LayoutGraph.model_validate_json(path.read_text())
|
layout = LayoutGraph.model_validate_json(path.read_text())
|
||||||
report = analyze_where_needed(layout, graph)
|
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")
|
fp_path = ctx.ws.local_path("review_fingerprints.json")
|
||||||
current_fp: dict[str, str] = {}
|
current_fp: dict[str, str] = {}
|
||||||
try:
|
try:
|
||||||
from backend.pinscopex.review_fingerprint import (
|
from backend.periscopex.review_fingerprint import (
|
||||||
graph_ic_fingerprints,
|
graph_ic_fingerprints,
|
||||||
skip_unchanged_ics,
|
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))
|
cmap = _build_constraints_map(_load_datasheets(extracted_dir))
|
||||||
current_fp = graph_ic_fingerprints(ctx.graph, cmap)
|
current_fp = graph_ic_fingerprints(ctx.graph, cmap)
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from backend.pinscopex.functional_groups import build_placement_plan
|
from backend.periscopex.functional_groups import build_placement_plan
|
||||||
from backend.pinscopex.graph import build_graph
|
from backend.periscopex.graph import build_graph
|
||||||
from backend.pinscopex.models import ComponentConstraints, DesignGraph, LayoutGraph
|
from backend.periscopex.models import ComponentConstraints, DesignGraph, LayoutGraph
|
||||||
from backend.pinscopex.placement_pack import build_placement_pack
|
from backend.periscopex.placement_pack import build_placement_pack
|
||||||
from backend.services import projects as proj_svc
|
from backend.services import projects as proj_svc
|
||||||
from backend.services.pipeline import PipelineWorkspace, broker
|
from backend.services.pipeline import PipelineWorkspace, broker
|
||||||
from backend.services.storage import StorageBackend
|
from backend.services.storage import StorageBackend
|
||||||
@@ -218,7 +218,7 @@ def _load_layout(ws: PipelineWorkspace) -> LayoutGraph | None:
|
|||||||
if not pcb.is_file():
|
if not pcb.is_file():
|
||||||
return None
|
return None
|
||||||
try:
|
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)
|
layout = parse_kicad_pcb(pcb)
|
||||||
cached.write_text(layout.model_dump_json(indent=2) + "\n")
|
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
|
models/ — cached component specs
|
||||||
design_graph.json — graph output
|
design_graph.json — graph output
|
||||||
report.json — validation report
|
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 (global, shared across users):
|
||||||
library/extracted/{mpn}.json
|
library/extracted/{mpn}.json
|
||||||
@@ -31,9 +31,9 @@ from typing import Any
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
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
|
from backend.services.storage import StaleGeneration, StorageBackend
|
||||||
|
|
||||||
|
|
||||||
@@ -120,9 +120,13 @@ class ProjectMeta(BaseModel):
|
|||||||
pause_reason: str | None = None
|
pause_reason: str | None = None
|
||||||
completed_review_refs: list[str] = [] # IC refs already reviewed (persists across pauses)
|
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.
|
# 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
|
# Worker bookkeeping (set by the API on enqueue, read by /events SSE
|
||||||
# and by the stale-running sweeper).
|
# and by the stale-running sweeper).
|
||||||
@@ -166,7 +170,7 @@ def completed_review_refs_for_retry(
|
|||||||
for ref in (report.get("review_errors") or {}):
|
for ref in (report.get("review_errors") or {}):
|
||||||
if ref:
|
if ref:
|
||||||
failed.add(str(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]
|
kept = [r for r in (meta.completed_review_refs or []) if r and r not in failed]
|
||||||
return sorted(kept, key=natural_sort_key)
|
return sorted(kept, key=natural_sort_key)
|
||||||
|
|
||||||
@@ -484,7 +488,7 @@ def clear_project_extractions(
|
|||||||
"bom_summary.json",
|
"bom_summary.json",
|
||||||
"derating.json",
|
"derating.json",
|
||||||
"report.json",
|
"report.json",
|
||||||
"pinscope-findings.json",
|
"periscope-findings.json",
|
||||||
"review_fingerprints.json",
|
"review_fingerprints.json",
|
||||||
"api_logs.jsonl",
|
"api_logs.jsonl",
|
||||||
"graph_voltage_updates.json",
|
"graph_voltage_updates.json",
|
||||||
@@ -519,7 +523,7 @@ def reopen_project(
|
|||||||
"bom_summary.json",
|
"bom_summary.json",
|
||||||
"derating.json",
|
"derating.json",
|
||||||
"report.json",
|
"report.json",
|
||||||
"pinscope-findings.json",
|
"periscope-findings.json",
|
||||||
"review_fingerprints.json",
|
"review_fingerprints.json",
|
||||||
"api_logs.jsonl",
|
"api_logs.jsonl",
|
||||||
"graph_voltage_updates.json",
|
"graph_voltage_updates.json",
|
||||||
@@ -936,7 +940,7 @@ def library_has_datasheet(
|
|||||||
return key
|
return key
|
||||||
# 3. Pattern-based fallback for passives
|
# 3. Pattern-based fallback for passives
|
||||||
if patterns:
|
if patterns:
|
||||||
from backend.pinscopex.resolve_passives import resolve_mpn
|
from backend.periscopex.resolve_passives import resolve_mpn
|
||||||
|
|
||||||
match = resolve_mpn(mpn, patterns)
|
match = resolve_mpn(mpn, patterns)
|
||||||
if match is not None:
|
if match is not None:
|
||||||
@@ -1121,11 +1125,11 @@ def list_library_patterns(storage: StorageBackend) -> list[str]:
|
|||||||
def load_library_patterns(storage: StorageBackend):
|
def load_library_patterns(storage: StorageBackend):
|
||||||
"""Load and parse all passive patterns from the library.
|
"""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
|
This function is only used by the library/check endpoint — during pipeline
|
||||||
execution, patterns are loaded from the workspace temp directory.
|
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
|
from backend.services.storage import LocalStorageBackend
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from typing import Awaitable, Callable
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
from backend.pinscopex.models import (
|
from backend.periscopex.models import (
|
||||||
ComponentConstraints,
|
ComponentConstraints,
|
||||||
ComponentType,
|
ComponentType,
|
||||||
DesignGraph,
|
DesignGraph,
|
||||||
@@ -30,7 +30,7 @@ from backend.pinscopex.models import (
|
|||||||
NetType,
|
NetType,
|
||||||
ValidationReport,
|
ValidationReport,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.validate import (
|
from backend.periscopex.validate import (
|
||||||
SYSTEM_PROMPT,
|
SYSTEM_PROMPT,
|
||||||
_MAX_REVIEW_TURNS,
|
_MAX_REVIEW_TURNS,
|
||||||
ReviewResult,
|
ReviewResult,
|
||||||
@@ -41,30 +41,30 @@ from backend.pinscopex.validate import (
|
|||||||
build_component_context,
|
build_component_context,
|
||||||
_parse_review,
|
_parse_review,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.quote_verify import verify_finding_citations
|
from backend.periscopex.quote_verify import verify_finding_citations
|
||||||
from backend.pinscopex.utils import safe_mpn
|
from backend.periscopex.utils import safe_mpn
|
||||||
from backend.pinscopex.pin_mux_check import check_pin_mux_feasibility
|
from backend.periscopex.pin_mux_check import check_pin_mux_feasibility
|
||||||
from backend.pinscopex.led_current_check import check_led_current
|
from backend.periscopex.led_current_check import check_led_current
|
||||||
from backend.pinscopex.passive_rail_check import (
|
from backend.periscopex.passive_rail_check import (
|
||||||
check_i2c_pullups,
|
check_i2c_pullups,
|
||||||
check_reset_pullups,
|
check_reset_pullups,
|
||||||
check_supply_decoupling,
|
check_supply_decoupling,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.bom_match_check import check_bom_schematic_match
|
from backend.periscopex.bom_match_check import check_bom_schematic_match
|
||||||
from backend.pinscopex.hf_coverage_check import check_hf_decoupling_coverage
|
from backend.periscopex.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.periscopex.cad_bridge import annotate_findings_cad, build_cad_bridge, write_cad_bridge
|
||||||
from backend.pinscopex.filter_check import check_filters
|
from backend.periscopex.filter_check import check_filters
|
||||||
from backend.pinscopex.thermal_check import check_thermal
|
from backend.periscopex.thermal_check import check_thermal
|
||||||
from backend.pinscopex.power_margin_check import check_power_margin
|
from backend.periscopex.power_margin_check import check_power_margin
|
||||||
from backend.pinscopex.sequencing_check import check_power_sequencing
|
from backend.periscopex.sequencing_check import check_power_sequencing
|
||||||
from backend.pinscopex.dnp_check import check_dnp_enables
|
from backend.periscopex.dnp_check import check_dnp_enables
|
||||||
from backend.pinscopex.lifecycle import check_lifecycle, load_lifecycle_dir
|
from backend.periscopex.lifecycle import check_lifecycle, load_lifecycle_dir
|
||||||
from backend.pinscopex.errata_check import check_errata
|
from backend.periscopex.errata_check import check_errata
|
||||||
from backend.pinscopex.internal_features_check import check_internal_features
|
from backend.periscopex.internal_features_check import check_internal_features
|
||||||
from backend.pinscopex.placement_check import check_placement
|
from backend.periscopex.placement_check import check_placement
|
||||||
from backend.pinscopex.si_check import check_si
|
from backend.periscopex.si_check import check_si
|
||||||
from backend.pinscopex.crystal_cl_check import check_crystal_cl
|
from backend.periscopex.crystal_cl_check import check_crystal_cl
|
||||||
from backend.pinscopex.nc_pin_check import check_nc_pins
|
from backend.periscopex.nc_pin_check import check_nc_pins
|
||||||
|
|
||||||
TRACE_VERSION = 1
|
TRACE_VERSION = 1
|
||||||
|
|
||||||
@@ -139,14 +139,14 @@ def _assistant_text(blocks) -> str:
|
|||||||
except Exception:
|
except Exception:
|
||||||
log.exception("trace: assistant_text extraction failed")
|
log.exception("trace: assistant_text extraction failed")
|
||||||
return "\n".join(parts)
|
return "\n".join(parts)
|
||||||
from backend.pinscopex.validation_tools import (
|
from backend.periscopex.validation_tools import (
|
||||||
ALL_TOOLS,
|
ALL_TOOLS,
|
||||||
SUBMIT_REVIEW_SCHEMA,
|
SUBMIT_REVIEW_SCHEMA,
|
||||||
ConstraintsMap,
|
ConstraintsMap,
|
||||||
ExcerptState,
|
ExcerptState,
|
||||||
execute_tool,
|
execute_tool,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.utils import safe_mpn
|
from backend.periscopex.utils import safe_mpn
|
||||||
|
|
||||||
from backend.config import settings
|
from backend.config import settings
|
||||||
from backend.services.api_logs import ApiLogger
|
from backend.services.api_logs import ApiLogger
|
||||||
@@ -639,7 +639,7 @@ def _find_pdf(
|
|||||||
then tries to download from the library.
|
then tries to download from the library.
|
||||||
"""
|
"""
|
||||||
from backend.services.datasheet_finder import find_local_pdf
|
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()
|
mpn = (mpn or "").strip()
|
||||||
if not mpn:
|
if not mpn:
|
||||||
@@ -836,7 +836,7 @@ async def validate_design_async(
|
|||||||
try:
|
try:
|
||||||
prefix_id = (project_prefix or "").rstrip("/").rsplit("/", 1)[-1]
|
prefix_id = (project_prefix or "").rstrip("/").rsplit("/", 1)[-1]
|
||||||
bridge = build_cad_bridge(report, prefix_id or report.project)
|
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:
|
except Exception:
|
||||||
log.exception("cad bridge write failed")
|
log.exception("cad bridge write failed")
|
||||||
return report
|
return report
|
||||||
|
|||||||
+5
-5
@@ -5,7 +5,7 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
dockerfile: backend/Dockerfile
|
dockerfile: backend/Dockerfile
|
||||||
|
|
||||||
container_name: pinscope-backend
|
container_name: periscope-backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
env_file:
|
env_file:
|
||||||
@@ -24,7 +24,7 @@ services:
|
|||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
- pinscope
|
- periscope
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
@@ -34,7 +34,7 @@ services:
|
|||||||
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:8080}
|
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:8080}
|
||||||
NEXT_PUBLIC_AUTH_MODE: ${NEXT_PUBLIC_AUTH_MODE:-}
|
NEXT_PUBLIC_AUTH_MODE: ${NEXT_PUBLIC_AUTH_MODE:-}
|
||||||
|
|
||||||
container_name: pinscope-frontend
|
container_name: periscope-frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -44,9 +44,9 @@ services:
|
|||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
- pinscope
|
- periscope
|
||||||
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
pinscope:
|
periscope:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Piano di implementazione — Pinscope
|
# Piano di implementazione — Periscope
|
||||||
|
|
||||||
Documento di lavoro **prima dello sviluppo**. La lista dell’utente è il minimo; sotto c’è anche ciò che serve perché quella lista non resti un insieme di moduli scollegati.
|
Documento di lavoro **prima dello sviluppo**. La lista dell’utente è il minimo; sotto c’è anche ciò che serve perché quella lista non resti un insieme di moduli scollegati.
|
||||||
|
|
||||||
**Questo piano copre due prodotti.** Pinscope originale resta il primo. Layout/plugin/placement mm sono il secondo. Non mescolare i changelog né vendere il secondo come “un po’ di Pinscope in più”.
|
**Questo piano copre due prodotti.** Periscope originale resta il primo. Layout/plugin/placement mm sono il secondo. Non mescolare i changelog né vendere il secondo come “un po’ di Periscope in più”.
|
||||||
|
|
||||||
Stato del codice di riferimento: branch `cursor/deepseek-71c5` (post DeepSeek V4.1, costi USD, replace BOM/netlist, parser KiCad, fingerprint review, auth multi-utente, PCB pad nets).
|
Stato del codice di riferimento: branch `cursor/deepseek-71c5` (post DeepSeek V4.1, costi USD, replace BOM/netlist, parser KiCad, fingerprint review, auth multi-utente, PCB pad nets).
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ Stato del codice di riferimento: branch `cursor/deepseek-71c5` (post DeepSeek V4
|
|||||||
|
|
||||||
## 0b. Roadmap DeepSeek / crescita (integrata)
|
## 0b. Roadmap DeepSeek / crescita (integrata)
|
||||||
|
|
||||||
Fonte originale: canvas *Pinscope: crescita e DeepSeek*. Qui lo stato operativo.
|
Fonte originale: canvas *Periscope: crescita e DeepSeek*. Qui lo stato operativo.
|
||||||
|
|
||||||
| Fase | Voce | Stato |
|
| Fase | Voce | Stato |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
@@ -48,27 +48,27 @@ Obiettivo unico: **routing migliore** (loop corti, meno crossing, canali liberi)
|
|||||||
|
|
||||||
1. **F1 polish (ora)** — satelliti classificatì; `other` nascosti; Domains/Rails = primary rail.
|
1. **F1 polish (ora)** — satelliti classificatì; `other` nascosti; Domains/Rails = primary rail.
|
||||||
2. **C4 fill** — riestrazione IC → `layout_rules` con `max_distance_mm` dove il PDF lo dice (skill 1.10.0).
|
2. **C4 fill** — riestrazione IC → `layout_rules` con `max_distance_mm` dove il PDF lo dice (skill 1.10.0).
|
||||||
3. **PCB gate** — upload `.kicad_pcb` → `layout_graph.json` (footprint xy già usati da PS-PLC*).
|
3. **PCB gate** — upload `.kicad_pcb` → `layout_graph.json` (footprint xy già usati da PE-PLC*).
|
||||||
4. **F2 pack v1** — per ogni `decoupling_proximity` numerica: proporre xy satellite entro `max_distance_mm` dal pad (già skeleton); UI Pack lista proposte.
|
4. **F2 pack v1** — per ogni `decoupling_proximity` numerica: proporre xy satellite entro `max_distance_mm` dal pad (già skeleton); UI Pack lista proposte.
|
||||||
5. **F2 pack v2** — collisioni courtyard, stesso layer, ordine `assemble_order` per dominio, ancore IC fissi se già piazzati.
|
5. **F2 pack v2** — collisioni courtyard, stesso layer, ordine `assemble_order` per dominio, ancore IC fissi se già piazzati.
|
||||||
6. **F2 export** — scrivere posizioni proposte in file/plugin (pcbnew) senza muovere rame; `placement_check` resta verifica.
|
6. **F2 export** — scrivere posizioni proposte in file/plugin (pcbnew) senza muovere rame; `placement_check` resta verifica.
|
||||||
|
|
||||||
Output F1: `functional_groups.json` scritto in `graph_build`. Pipeline parallela Placement (`POST …/placement/start`) riscrive anche `placement_plan.json` senza toccare lo `status` di analisi, poi tenta F2 pack. Verifica PCB esistente resta `placement_check` (PS-PLC*) — non confondere con packing.
|
Output F1: `functional_groups.json` scritto in `graph_build`. Pipeline parallela Placement (`POST …/placement/start`) riscrive anche `placement_plan.json` senza toccare lo `status` di analisi, poi tenta F2 pack. Verifica PCB esistente resta `placement_check` (PE-PLC*) — non confondere con packing.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 0. Due prodotti (stesso repo, due promesse)
|
## 0. Due prodotti (stesso repo, due promesse)
|
||||||
|
|
||||||
| | **Pinscope** (oggi + wave A–B, C schema, F/H leggere) | **Pinscope Layout** (wave D parziale, G, C4+G2, plugin pcbnew) |
|
| | **Periscope** (oggi + wave A–B, C schema, F/H leggere) | **Periscope Layout** (wave D parziale, G, C4+G2, plugin pcbnew) |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Promessa | Lo schema rispetta il datasheet | Il rame rispetta datasheet + geometria |
|
| Promessa | Lo schema rispetta il datasheet | Il rame rispetta datasheet + geometria |
|
||||||
| File | BOM, netlist, `.kicad_sch` gerarchico | + `.kicad_pcb` |
|
| File | BOM, netlist, `.kicad_sch` gerarchico | + `.kicad_pcb` |
|
||||||
| Output | Finding su pin/net, derating, power tree | Distanze mm, 3W, creepage, skew, via EP |
|
| Output | Finding su pin/net, derating, power tree | Distanze mm, 3W, creepage, skew, via EP |
|
||||||
| Utente | Chi chiude lo schema | Chi sbroglia |
|
| Utente | Chi chiude lo schema | Chi sbroglia |
|
||||||
|
|
||||||
Farli nello stesso codebase (`pinscopex` + `LayoutGraph`) è ragionevole. Farli **nella stessa run obbligatoria** no: senza PCB il progetto deve restare un Pinscope completo, non “incompleto perché manca il gerber”.
|
Farli nello stesso codebase (`periscopex` + `LayoutGraph`) è ragionevole. Farli **nella stessa run obbligatoria** no: senza PCB il progetto deve restare un Periscope completo, non “incompleto perché manca il gerber”.
|
||||||
|
|
||||||
Nome in UI: tab **Layout** o prodotto “Layout checks” gated dal file `.kicad_pcb`. Il report schema non deve riempirsi di `PS-PLC` se il PCB non c’è.
|
Nome in UI: tab **Layout** o prodotto “Layout checks” gated dal file `.kicad_pcb`. Il report schema non deve riempirsi di `PE-PLC` se il PCB non c’è.
|
||||||
|
|
||||||
Non serve un fork oggi. Serve disciplina: ogni PR dichiara se è Core o Layout.
|
Non serve un fork oggi. Serve disciplina: ogni PR dichiara se è Core o Layout.
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ Non serve un fork oggi. Serve disciplina: ogni PR dichiara se è Core o Layout.
|
|||||||
|
|
||||||
## 0. Contratto di prodotto (non negoziabile)
|
## 0. Contratto di prodotto (non negoziabile)
|
||||||
|
|
||||||
Pinscope oggi è un **validatore di schema**: BOM + netlist → grafo bipartito → check deterministici + review LLM con citazione datasheet. Non legge il PCB.
|
Periscope oggi è un **validatore di schema**: BOM + netlist → grafo bipartito → check deterministici + review LLM con citazione datasheet. Non legge il PCB.
|
||||||
|
|
||||||
Molti punti della lista (larghezza traccia, 3W, creepage, CPW clearance, length matching) **non esistono senza geometria**. Il piano li tiene, ma li mette **dopo** un ingest layout. Se li si forza sullo schema si producono finding inventati.
|
Molti punti della lista (larghezza traccia, 3W, creepage, CPW clearance, length matching) **non esistono senza geometria**. Il piano li tiene, ma li mette **dopo** un ingest layout. Se li si forza sullo schema si producono finding inventati.
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ Da fare (Wave A1), in ordine:
|
|||||||
|
|
||||||
Regole:
|
Regole:
|
||||||
|
|
||||||
1. Ogni nuovo check è una funzione pura in `backend/pinscopex/` che legge `DesignGraph` (+ opzionale layout). Niente SDK LLM dentro `pinscopex/`.
|
1. Ogni nuovo check è una funzione pura in `backend/periscopex/` che legge `DesignGraph` (+ opzionale layout). Niente SDK LLM dentro `periscopex/`.
|
||||||
2. I finding usano lo stesso schema (`Finding` in `models.py` / `frontend/src/lib/types.ts`). Campo `source` già distingue check automatici vs review.
|
2. I finding usano lo stesso schema (`Finding` in `models.py` / `frontend/src/lib/types.ts`). Campo `source` già distingue check automatici vs review.
|
||||||
3. Finding normalization resta **downgrade-only**.
|
3. Finding normalization resta **downgrade-only**.
|
||||||
4. Libreria condivisa: MPN exact-match. Niente fuzzy sul die.
|
4. Libreria condivisa: MPN exact-match. Niente fuzzy sul die.
|
||||||
@@ -134,14 +134,14 @@ Aggiungere (backward compatible):
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `net` | Telemetry CAD, filtri, SI |
|
| `net` | Telemetry CAD, filtri, SI |
|
||||||
| `pins[]` | Pan-and-zoom su U1.4 |
|
| `pins[]` | Pan-and-zoom su U1.4 |
|
||||||
| `rule_id` | Plugin DRC (`PS-DEC-001`) |
|
| `rule_id` | Plugin DRC (`PE-DEC-001`) |
|
||||||
| `cad_sheet` / `cad_uuid` | Sync plugin KiCad |
|
| `cad_sheet` / `cad_uuid` | Sync plugin KiCad |
|
||||||
| `variant` | DNP / ECO |
|
| `variant` | DNP / ECO |
|
||||||
| `severity_calibrated` | già implicito; non alzare in post |
|
| `severity_calibrated` | già implicito; non alzare in post |
|
||||||
|
|
||||||
Passi:
|
Passi:
|
||||||
|
|
||||||
1. Estendere `Finding` in `backend/pinscopex/models.py` e `frontend/src/lib/types.ts`.
|
1. Estendere `Finding` in `backend/periscopex/models.py` e `frontend/src/lib/types.ts`.
|
||||||
2. Aggiornare `assign_finding_ids`, export Excel, report UI (campi opzionali nascosti se null).
|
2. Aggiornare `assign_finding_ids`, export Excel, report UI (campi opzionali nascosti se null).
|
||||||
3. Test su `simple_project/` che i check esistenti ancora serializzano.
|
3. Test su `simple_project/` che i check esistenti ancora serializzano.
|
||||||
|
|
||||||
@@ -157,12 +157,12 @@ Passi:
|
|||||||
| 2 Datasheet / errata / OCR blocchi | Pintable, excerpt, quote_verify, errata, `internal_features` | Nessun RAG vendor | **OK** |
|
| 2 Datasheet / errata / OCR blocchi | Pintable, excerpt, quote_verify, errata, `internal_features` | Nessun RAG vendor | **OK** |
|
||||||
| 3 Impedenze / stackup | ImpedenceFinder: calcolatrice + **Z0 sulle tracce** dei net signal (stackup PCB) | CPWG non nel vendor | **OK** |
|
| 3 Impedenze / stackup | ImpedenceFinder: calcolatrice + **Z0 sulle tracce** dei net signal (stackup PCB) | CPWG non nel vendor | **OK** |
|
||||||
| 4 Filtri | `check_filters` (solo con poli/numeri in specs) | Niente \(f_c\) inventata | **OK** |
|
| 4 Filtri | `check_filters` (solo con poli/numeri in specs) | Niente \(f_c\) inventata | **OK** |
|
||||||
| 5 Capacità PI | Decoupling sulla net; derating V; DC-bias/ESR se c’è il numero; mm sul PCB (`PS-PLC-001`) | — | **OK** |
|
| 5 Capacità PI | Decoupling sulla net; derating V; DC-bias/ESR se c’è il numero; mm sul PCB (`PE-PLC-001`) | — | **OK** |
|
||||||
| 6 Elettrico | Pin mux; I2C/reset pull-up; LED; sequencing/IR/power margin se c’è il parametro | Senza numero in specs → skip | **OK** |
|
| 6 Elettrico | Pin mux; I2C/reset pull-up; LED; sequencing/IR/power margin se c’è il parametro | Senza numero in specs → skip | **OK** |
|
||||||
| 7 RF | Tab **RF / Impedance**: verify matching + template geometry (IFA/meander/stub → SVG + `.kicad_mod`); Z0 feed se PCB | CPWG / auto-place into `.kicad_pcb` dopo | **Improved** |
|
| 7 RF | Tab **RF / Impedance**: verify matching + template geometry (IFA/meander/stub → SVG + `.kicad_mod`); Z0 feed se PCB | CPWG / auto-place into `.kicad_pcb` dopo | **Improved** |
|
||||||
| 8 HV / isolation | — | Serve `layout_rules` + V/mm dal datasheet, non IEC inventato | — |
|
| 8 HV / isolation | — | Serve `layout_rules` + V/mm dal datasheet, non IEC inventato | — |
|
||||||
| 9 Termico | `check_thermal` se θJA/I sono in specs; via courtyard vs `min_via_count` | Niente \(T_j\) senza parametro | **OK** |
|
| 9 Termico | `check_thermal` se θJA/I sono in specs; via courtyard vs `min_via_count` | Niente \(T_j\) senza parametro | **OK** |
|
||||||
| 10 SI / DNP | DNP enable; `PS-SI-001` solo con `length_match` mm | Niente 3W/crosstalk inventati | **OK** |
|
| 10 SI / DNP | DNP enable; `PE-SI-001` solo con `length_match` mm | Niente 3W/crosstalk inventati | **OK** |
|
||||||
| 11 Lifecycle | `lifecycle_check` su cache distributore (EOL/NRND/RoHS esplicito) | Niente equivalente LLM | **OK** |
|
| 11 Lifecycle | `lifecycle_check` su cache distributore (EOL/NRND/RoHS esplicito) | Niente equivalente LLM | **OK** |
|
||||||
| 13 Placement da datasheet | `layout_rules` + `.kicad_pcb`: PLC-001…004, same_layer, crystal, keepout, path | 3W/creepage/CPW/isolation senza numero | **OK** |
|
| 13 Placement da datasheet | `layout_rules` + `.kicad_pcb`: PLC-001…004, same_layer, crystal, keepout, path | 3W/creepage/CPW/isolation senza numero | **OK** |
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ Senza questi i moduli 1–12 non si misurano e i plugin mentono.
|
|||||||
|
|
||||||
**E1. KiCad gerarchico GA.** Obbligatorio. Vedi “Schema gerarchico” sopra. Senza flatten dei fogli il plugin e il `.kicad_pcb` non allineano i net.
|
**E1. KiCad gerarchico GA.** Obbligatorio. Vedi “Schema gerarchico” sopra. Senza flatten dei fogli il plugin e il `.kicad_pcb` non allineano i net.
|
||||||
|
|
||||||
**E2. Protocollo plugin KiCad** (`pinscope-cad-bridge` JSON). Un file per progetto:
|
**E2. Protocollo plugin KiCad** (`periscope-cad-bridge` JSON). Un file per progetto:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -184,14 +184,14 @@ Senza questi i moduli 1–12 non si misurano e i plugin mentono.
|
|||||||
"project_id": "...",
|
"project_id": "...",
|
||||||
"findings": [
|
"findings": [
|
||||||
{
|
{
|
||||||
"rule_id": "PS-MUX-001",
|
"rule_id": "PE-MUX-001",
|
||||||
"ref": "U3",
|
"ref": "U3",
|
||||||
"pins": ["12"],
|
"pins": ["12"],
|
||||||
"sheet": "...",
|
"sheet": "...",
|
||||||
"uuid": "...",
|
"uuid": "...",
|
||||||
"severity": "error",
|
"severity": "error",
|
||||||
"message": "...",
|
"message": "...",
|
||||||
"url": "https://pinscope.../report?finding=U3-001"
|
"url": "https://periscope.../report?finding=U3-001"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -219,7 +219,7 @@ Passi: quelli in “Schema gerarchico (più file)” + file-guide riscritta (nie
|
|||||||
Passi:
|
Passi:
|
||||||
|
|
||||||
1. Pacchetto `plugins/kicad/` (action plugin Python, KiCad 9/10).
|
1. Pacchetto `plugins/kicad/` (action plugin Python, KiCad 9/10).
|
||||||
2. `pinscope-findings.json` dal report (E2).
|
2. `periscope-findings.json` dal report (E2).
|
||||||
3. Marcatori / focus `uuid` su **eeschema** (foglio figlio corretto) e, per finding layout, su **pcbnew**.
|
3. Marcatori / focus `uuid` su **eeschema** (foglio figlio corretto) e, per finding layout, su **pcbnew**.
|
||||||
4. Pan-and-zoom: `FocusOnItem` / select symbol; se l’API 10 differisce, adapter sottile.
|
4. Pan-and-zoom: `FocusOnItem` / select symbol; se l’API 10 differisce, adapter sottile.
|
||||||
|
|
||||||
@@ -235,7 +235,7 @@ Passi:
|
|||||||
|
|
||||||
1. Da campi KiCad (`MPN`, `mpn`, `PN`, `lcsc`) già letti in `parsers_kicad.py` / `graph.py`.
|
1. Da campi KiCad (`MPN`, `mpn`, `PN`, `lcsc`) già letti in `parsers_kicad.py` / `graph.py`.
|
||||||
2. Tabella conflitti: ref in schema senza MPN, MPN in BOM senza ref, mismatch Value.
|
2. Tabella conflitti: ref in schema senza MPN, MPN in BOM senza ref, mismatch Value.
|
||||||
3. Finding `source=bom_match` con `rule_id=PS-BOM-001`.
|
3. Finding `source=bom_match` con `rule_id=PE-BOM-001`.
|
||||||
4. UI wizard: riga rossa nel matching, non solo colonne.
|
4. UI wizard: riga rossa nel matching, non solo colonne.
|
||||||
|
|
||||||
**Done when:** U1 in schema e U1 in BOM con MPN diversi → ERROR citabile.
|
**Done when:** U1 in schema e U1 in BOM con MPN diversi → ERROR citabile.
|
||||||
@@ -244,7 +244,7 @@ Passi:
|
|||||||
|
|
||||||
## Wave B — Check deterministici schema (sblocca 4, 5, 6, 9, 10 DNP)
|
## Wave B — Check deterministici schema (sblocca 4, 5, 6, 9, 10 DNP)
|
||||||
|
|
||||||
Obiettivo: meno LLM, più numeri. Ogni item = modulo `pinscopex` + test su grafo sintetico + riga in eval.
|
Obiettivo: meno LLM, più numeri. Ogni item = modulo `periscopex` + test su grafo sintetico + riga in eval.
|
||||||
|
|
||||||
### B1. Power tree & drop (6)
|
### B1. Power tree & drop (6)
|
||||||
|
|
||||||
@@ -253,7 +253,7 @@ Passi:
|
|||||||
1. Riuse UI power tree esistente.
|
1. Riuse UI power tree esistente.
|
||||||
2. Per ogni IC: somma IQ + load stimato da specs se c’è; confronta con `Iout_max` LDO/Buck se estratto.
|
2. Per ogni IC: somma IQ + load stimato da specs se c’è; confronta con `Iout_max` LDO/Buck se estratto.
|
||||||
3. IR drop **solo se** esiste Rseries esplicito (shunt/ferrite) — niente stima di pista.
|
3. IR drop **solo se** esiste Rseries esplicito (shunt/ferrite) — niente stima di pista.
|
||||||
4. Finding `PS-PWR-001` margin fail.
|
4. Finding `PE-PWR-001` margin fail.
|
||||||
|
|
||||||
### B2. Sequencing (6)
|
### B2. Sequencing (6)
|
||||||
|
|
||||||
@@ -343,7 +343,7 @@ Passi:
|
|||||||
1. Non scraping indiscriminato (TOS, HTML instabile).
|
1. Non scraping indiscriminato (TOS, HTML instabile).
|
||||||
2. Catalogo URL noti (TI `lit/er`, STM `errata`, Microchip).
|
2. Catalogo URL noti (TI `lit/er`, STM `errata`, Microchip).
|
||||||
3. DeepSeek `web_search` **opzionale** gated, citazione obbligatoria, stesso `quote_verify`.
|
3. DeepSeek `web_search` **opzionale** gated, citazione obbligatoria, stesso `quote_verify`.
|
||||||
4. Finding `PS-ERRATA-001` se il workaround (pull-up, bond-out) non è nello schema.
|
4. Finding `PE-ERRATA-001` se il workaround (pull-up, bond-out) non è nello schema.
|
||||||
|
|
||||||
**Done when:** un MPN con errata nota in fixture produce finding; vendor senza URL → skip silenzioso loggato.
|
**Done when:** un MPN con errata nota in fixture produce finding; vendor senza URL → skip silenzioso loggato.
|
||||||
|
|
||||||
@@ -407,7 +407,7 @@ Motore **standalone**, stile calcolatrice. I vincoli CAD sono export, non verit
|
|||||||
|
|
||||||
Passi:
|
Passi:
|
||||||
|
|
||||||
1. `pinscopex/impedance.py`: microstrip, stripline, coupled diff, CPW — formule documentate + test numerici vs 3 valori ImpedanceFinder.
|
1. `periscopex/impedance.py`: microstrip, stripline, coupled diff, CPW — formule documentate + test numerici vs 3 valori ImpedanceFinder.
|
||||||
2. Input: `h`, `er`, `t`, `w`, `s`, `target_z`.
|
2. Input: `h`, `er`, `t`, `w`, `s`, `target_z`.
|
||||||
3. UI tab progetto “Impedance” (non LLM).
|
3. UI tab progetto “Impedance” (non LLM).
|
||||||
|
|
||||||
@@ -461,7 +461,7 @@ Passi:
|
|||||||
|
|
||||||
Passi:
|
Passi:
|
||||||
|
|
||||||
1. Length matching / intra-pair skew vs limite datasheet (USB/HDMI/PCIe). **OK** (`PS-SI-001`, solo `length_match` mm).
|
1. Length matching / intra-pair skew vs limite datasheet (USB/HDMI/PCIe). **OK** (`PE-SI-001`, solo `length_match` mm).
|
||||||
2. 3W: distanza centro-centro vs W aggressore. — skip senza numero (non IEC/USB folklore).
|
2. 3W: distanza centro-centro vs W aggressore. — skip senza numero (non IEC/USB folklore).
|
||||||
3. Creepage/clearance: profilo IEC 62368 (pollution, RMS V dai net). — skip senza V/mm nel datasheet.
|
3. Creepage/clearance: profilo IEC 62368 (pollution, RMS V dai net). — skip senza V/mm nel datasheet.
|
||||||
4. Isolation barrier: bbox isolator + divieto piste LV nel courtyard HV. — skip senza regola.
|
4. Isolation barrier: bbox isolator + divieto piste LV nel courtyard HV. — skip senza regola.
|
||||||
@@ -477,17 +477,17 @@ Passi:
|
|||||||
|
|
||||||
1. Per ogni pin alimentazione del pintable: footprint pad xy sul `.kicad_pcb`; condensatori sul medesimo net (grafo); distanza euclidea pad-cap (pin cap verso GND/VDD). **OK**
|
1. Per ogni pin alimentazione del pintable: footprint pad xy sul `.kicad_pcb`; condensatori sul medesimo net (grafo); distanza euclidea pad-cap (pin cap verso GND/VDD). **OK**
|
||||||
2. Se `max_distance_mm` estratto: ERROR/WARNING se oltre. Se assente: skip (niente default 3 mm). **OK**
|
2. Se `max_distance_mm` estratto: ERROR/WARNING se oltre. Se assente: skip (niente default 3 mm). **OK**
|
||||||
3. Via in pad / via sotto EP: contare via nel courtyard del thermal pad vs `min_via_count`. **OK** (`PS-PLC-002`)
|
3. Via in pad / via sotto EP: contare via nel courtyard del thermal pad vs `min_via_count`. **OK** (`PE-PLC-002`)
|
||||||
4. Stesso layer: se `same_layer: true` e il cap è sull’altro lato senza via sotto il pin → WARNING. **OK** (`PS-PLC-003`)
|
4. Stesso layer: se `same_layer: true` e il cap è sull’altro lato senza via sotto il pin → WARNING. **OK** (`PE-PLC-003`)
|
||||||
5. Piste: lunghezza net dal pin al cap = shortest path sui segmenti vs `max_distance_mm`. **OK**
|
5. Piste: lunghezza net dal pin al cap = shortest path sui segmenti vs `max_distance_mm`. **OK**
|
||||||
6. Crystal: cap load vs pin XIN/XOUT (stessa metrica). **OK** (`X1`/`C9`/`C10`)
|
6. Crystal: cap load vs pin XIN/XOUT (stessa metrica). **OK** (`X1`/`C9`/`C10`)
|
||||||
7. Finding `PS-PLC-001`…`004` con `pins`, `net`; plugin focus pcbnew. **OK** (keepout = `PS-PLC-004`)
|
7. Finding `PE-PLC-001`…`004` con `pins`, `net`; plugin focus pcbnew. **OK** (keepout = `PE-PLC-004`)
|
||||||
|
|
||||||
Non confrontare una foto del layout TI con il board pixel-a-pixel. Solo vincoli numerici/topologici.
|
Non confrontare una foto del layout TI con il board pixel-a-pixel. Solo vincoli numerici/topologici.
|
||||||
|
|
||||||
**Done when:** fixture PCB con C di decoupling a 15 mm da VDD (regola 2 mm) → `PS-PLC-001`; cap a 1 mm → niente finding.
|
**Done when:** fixture PCB con C di decoupling a 15 mm da VDD (regola 2 mm) → `PE-PLC-001`; cap a 1 mm → niente finding.
|
||||||
|
|
||||||
**Done when (G1+G2):** un `.kicad_pcb` di test (USB diff pair volutamente sbagliata) produce `PS-SI-001`. I net coincidono con lo schema gerarchico della stessa repo.
|
**Done when (G1+G2):** un `.kicad_pcb` di test (USB diff pair volutamente sbagliata) produce `PE-SI-001`. I net coincidono con lo schema gerarchico della stessa repo.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -529,7 +529,7 @@ Stima onesta: Wave A–B (schema) sono il ritorno; G è un secondo prodotto. Non
|
|||||||
## Passi operativi per **ogni** check nuovo
|
## Passi operativi per **ogni** check nuovo
|
||||||
|
|
||||||
1. Fixture grafo minimo in `tests/test_<nome>.py` (non solo `simple_project`).
|
1. Fixture grafo minimo in `tests/test_<nome>.py` (non solo `simple_project`).
|
||||||
2. Funzione in `pinscopex/` senza I/O.
|
2. Funzione in `periscopex/` senza I/O.
|
||||||
3. Registrare in `services/validation.py` accanto a pin_mux/LED.
|
3. Registrare in `services/validation.py` accanto a pin_mux/LED.
|
||||||
4. `rule_id` + `source`.
|
4. `rule_id` + `source`.
|
||||||
5. Una riga changelog.
|
5. Una riga changelog.
|
||||||
@@ -557,6 +557,6 @@ Resta, senza inventare numeri:
|
|||||||
|
|
||||||
1. HV / isolation (blocco 8) se il datasheet dà V/mm o keepout HV.
|
1. HV / isolation (blocco 8) se il datasheet dà V/mm o keepout HV.
|
||||||
2. 3W / creepage / CPW solo con numero in `layout_rules` o dal calcolatore D2.
|
2. 3W / creepage / CPW solo con numero in `layout_rules` o dal calcolatore D2.
|
||||||
3. Un `.kicad_pcb` reale di progetto (non fixture USB inventata) per vedere `PS-PLC`/`PS-SI` sul board.
|
3. Un `.kicad_pcb` reale di progetto (non fixture USB inventata) per vedere `PE-PLC`/`PE-SI` sul board.
|
||||||
|
|
||||||
Il plugin KiCad aspetta ancora verifica uuid + sheet su un progetto multi-foglio vero.
|
Il plugin KiCad aspetta ancora verifica uuid + sheet su un progetto multi-foglio vero.
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
@AGENTS.md
|
@AGENTS.md
|
||||||
|
|
||||||
# Pinscope Frontend
|
# Periscope Frontend
|
||||||
|
|
||||||
Next.js 16 app (App Router, Turbopack) providing a web UI for Pinscope schematic validation. Talks to the FastAPI backend at `localhost:8000`.
|
Next.js 16 app (App Router, Turbopack) providing a web UI for Periscope schematic validation. Talks to the FastAPI backend at `localhost:8000`.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ credit state only through `useCredits()` — both are inert in this repo.
|
|||||||
|
|
||||||
| Path | Purpose |
|
| Path | Purpose |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `src/lib/types.ts` | TS types mirroring `pinscopex/models.py` |
|
| `src/lib/types.ts` | TS types mirroring `periscopex/models.py` |
|
||||||
| `src/lib/api.ts` | All data fetching — single integration point with backend |
|
| `src/lib/api.ts` | All data fetching — single integration point with backend |
|
||||||
| `src/lib/mock-data.ts` | Pipeline step definitions for progress UI |
|
| `src/lib/mock-data.ts` | Pipeline step definitions for progress UI |
|
||||||
| `src/components/report/` | Report viewer components + power tree React Flow graph + derating table + finding comments |
|
| `src/components/report/` | Report viewer components + power tree React Flow graph + derating table + finding comments |
|
||||||
@@ -97,6 +97,6 @@ Requires the backend running at `localhost:8000` (or set `NEXT_PUBLIC_API_URL`).
|
|||||||
|
|
||||||
- Keep all data fetching in `src/lib/api.ts` — don't scatter fetch calls across components
|
- Keep all data fetching in `src/lib/api.ts` — don't scatter fetch calls across components
|
||||||
- Report filters persist in URL search params (`?status=ERROR&component=U3&q=decoupling`)
|
- Report filters persist in URL search params (`?status=ERROR&component=U3&q=decoupling`)
|
||||||
- When modifying types, keep `src/lib/types.ts` in sync with `backend/pinscopex/models.py`
|
- When modifying types, keep `src/lib/types.ts` in sync with `backend/periscopex/models.py`
|
||||||
- Use `font-mono` for technical values: designators (U1), MPNs, pin names, component values
|
- Use `font-mono` for technical values: designators (U1), MPNs, pin names, component values
|
||||||
- Status colors: emerald = PASS, amber = WARNING, rose = ERROR, blue = accent/active
|
- Status colors: emerald = PASS, amber = WARNING, rose = ERROR, blue = accent/active
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
What's new in Pinscope.
|
What's new in Periscope.
|
||||||
|
|
||||||
|
## 2.29.0 — 2026-09-13 — Rebrand to Periscope
|
||||||
|
|
||||||
|
Product name, package (`periscopex`), Docker containers, deploy script, and default site URL are now **Periscope** (`https://periscope.michelebigi.it`). Deterministic finding IDs use the `PE-*` prefix. Existing project.json `pinscope_version` and browser storage keys are still read.
|
||||||
|
|
||||||
|
- [Changed] Brand Pinscope → Periscope across UI, docs, and APIs.
|
||||||
|
- [Changed] `backend/pinscopex` → `backend/periscopex`; rule IDs `PS-*` → `PE-*`.
|
||||||
|
- [Changed] Default host / update script → `periscope.michelebigi.it` / `update-periscope.sh`.
|
||||||
|
|
||||||
## 2.28.12 — 2026-09-13 — Antenna templates: IFA / meander / stub + .kicad_mod
|
## 2.28.12 — 2026-09-13 — Antenna templates: IFA / meander / stub + .kicad_mod
|
||||||
|
|
||||||
@@ -99,7 +107,7 @@ Dedicated Placement job builds the routing-first topology plan without touching
|
|||||||
Routing-first floorplan foundation without inventing millimetres: domains and satellite role hints after graph build, plus deterministic crystal CL and NC-pin checks.
|
Routing-first floorplan foundation without inventing millimetres: domains and satellite role hints after graph build, plus deterministic crystal CL and NC-pin checks.
|
||||||
|
|
||||||
- [New] `functional_groups.json` from `graph_build` (domains, IC groups, `role_hint`, attached `layout_rules`).
|
- [New] `functional_groups.json` from `graph_build` (domains, IC groups, `role_hint`, attached `layout_rules`).
|
||||||
- [New] `crystal_cl_check` (PS-XTAL-*) and `nc_pin_check` (PS-NC-001) in the deterministic suite.
|
- [New] `crystal_cl_check` (PS-XTAL-*) and `nc_pin_check` (PE-NC-001) in the deterministic suite.
|
||||||
- [Docs] Piano §0c Placement F1/F2; packing mm stays Layout F2.
|
- [Docs] Piano §0c Placement F1/F2; packing mm stays Layout F2.
|
||||||
|
|
||||||
## 2.27.1 — 2026-09-12 — DeepSeek roadmap integrations
|
## 2.27.1 — 2026-09-12 — DeepSeek roadmap integrations
|
||||||
@@ -113,7 +121,7 @@ Close the open P0/P2 items from the growth plan: offline smoke on `simple_projec
|
|||||||
|
|
||||||
## 2.27.0 — 2026-09-11 — Local multi-user auth
|
## 2.27.0 — 2026-09-11 — Local multi-user auth
|
||||||
|
|
||||||
Self-host Pinscope accounts (email + password) so several people can share a project. Invite collaborators by email from the project page — same flow as cloud, without Clerk.
|
Self-host Periscope accounts (email + password) so several people can share a project. Invite collaborators by email from the project page — same flow as cloud, without Clerk.
|
||||||
|
|
||||||
- [New] `AUTH_JWT_SECRET` enables register/login; first user is admin and inherits `users/local` projects.
|
- [New] `AUTH_JWT_SECRET` enables register/login; first user is admin and inherits `users/local` projects.
|
||||||
- [New] `/sign-in` and `/sign-up`; sidebar account menu. Set `NEXT_PUBLIC_AUTH_MODE=local` on the frontend build.
|
- [New] `/sign-in` and `/sign-up`; sidebar account menu. Set `NEXT_PUBLIC_AUTH_MODE=local` on the frontend build.
|
||||||
@@ -166,38 +174,38 @@ A pipeline run with `.kicad_pcb` + stackup samples routed **signal** nets (power
|
|||||||
|
|
||||||
`layout_rules` `keepout` flags a foreign net whose track endpoint is inside the KiCad courtyard. Own net and missing courtyard skip. No invented analog/digital classes.
|
`layout_rules` `keepout` flags a foreign net whose track endpoint is inside the KiCad courtyard. Own net and missing courtyard skip. No invented analog/digital classes.
|
||||||
|
|
||||||
- [New] `PS-PLC-004` WARNING. Tests use `X1` / `/HFXIN` / `GND` from `simple_project`.
|
- [New] `PE-PLC-004` WARNING. Tests use `X1` / `/HFXIN` / `GND` from `simple_project`.
|
||||||
|
|
||||||
## 2.24.0 — 2026-09-10 — Crystal load caps and track path
|
## 2.24.0 — 2026-09-10 — Crystal load caps and track path
|
||||||
|
|
||||||
Load caps on XIN/XOUT use the same `max_distance_mm` as decoupling. If the PCB has segments on the net, the limit is shortest-path length, not a guessed “loop is too big” ratio.
|
Load caps on XIN/XOUT use the same `max_distance_mm` as decoupling. If the PCB has segments on the net, the limit is shortest-path length, not a guessed “loop is too big” ratio.
|
||||||
|
|
||||||
- [New] Crystals (`X1` / C9 / C10 on `simple_project`) run `PS-PLC-001` when `layout_rules` has millimetres.
|
- [New] Crystals (`X1` / C9 / C10 on `simple_project`) run `PE-PLC-001` when `layout_rules` has millimetres.
|
||||||
- [New] Detour tracks: path along segments vs the same `max_distance_mm`. No segments → euclidean pad distance.
|
- [New] Detour tracks: path along segments vs the same `max_distance_mm`. No segments → euclidean pad distance.
|
||||||
|
|
||||||
## 2.23.0 — 2026-09-10 — same_layer decoupling
|
## 2.23.0 — 2026-09-10 — same_layer decoupling
|
||||||
|
|
||||||
If `layout_rules` sets `same_layer: true`, a decoupling cap on the opposite copper from the IC is a WARNING. A via inside the courtyard (calculated) is enough. Unset flag → skip.
|
If `layout_rules` sets `same_layer: true`, a decoupling cap on the opposite copper from the IC is a WARNING. A via inside the courtyard (calculated) is enough. Unset flag → skip.
|
||||||
|
|
||||||
- [New] `PS-PLC-003` WARNING when every placed cap on the net is on F vs B opposite the IC.
|
- [New] `PE-PLC-003` WARNING when every placed cap on the net is on F vs B opposite the IC.
|
||||||
|
|
||||||
## 2.22.0 — 2026-09-10 — Thermal vias vs min_via_count
|
## 2.22.0 — 2026-09-10 — Thermal vias vs min_via_count
|
||||||
|
|
||||||
Via count is calculated inside the KiCad courtyard. The limit is the `min_via_count` parameter from `layout_rules`. No courtyard or no count → skip. No pad radius default.
|
Via count is calculated inside the KiCad courtyard. The limit is the `min_via_count` parameter from `layout_rules`. No courtyard or no count → skip. No pad radius default.
|
||||||
|
|
||||||
- [New] `PS-PLC-002` when vias in courtyard < `min_via_count`. `simple_project` has no PCB so it stays silent.
|
- [New] `PE-PLC-002` when vias in courtyard < `min_via_count`. `simple_project` has no PCB so it stays silent.
|
||||||
|
|
||||||
## 2.21.0 — 2026-09-10 — Layout SI skew (datasheet mm only)
|
## 2.21.0 — 2026-09-10 — Layout SI skew (datasheet mm only)
|
||||||
|
|
||||||
Intra-pair skew is measured on the PCB only when `layout_rules` `length_match` has a number. 3W, creepage, and CPWG are not guessed.
|
Intra-pair skew is measured on the PCB only when `layout_rules` `length_match` has a number. 3W, creepage, and CPWG are not guessed.
|
||||||
|
|
||||||
- [New] `PS-SI-001` ERROR when a named pair (_DP/_DM, _P/_N) exceeds that millimetre. No mm in the datasheet → skip.
|
- [New] `PE-SI-001` ERROR when a named pair (_DP/_DM, _P/_N) exceeds that millimetre. No mm in the datasheet → skip.
|
||||||
|
|
||||||
## 2.20.0 — 2026-09-10 — Placement vs datasheet (PCB)
|
## 2.20.0 — 2026-09-10 — Placement vs datasheet (PCB)
|
||||||
|
|
||||||
Decoupling distance is measured on the `.kicad_pcb` against `layout_rules`. No board → no `PS-PLC-001`. A null millimetre skips — no 3 mm default.
|
Decoupling distance is measured on the `.kicad_pcb` against `layout_rules`. No board → no `PE-PLC-001`. A null millimetre skips — no 3 mm default.
|
||||||
|
|
||||||
- [New] `PS-PLC-001` when a decoupling cap is farther than datasheet `max_distance_mm`. Empty `layout_rules` and missing caps skip.
|
- [New] `PE-PLC-001` when a decoupling cap is farther than datasheet `max_distance_mm`. Empty `layout_rules` and missing caps skip.
|
||||||
|
|
||||||
## 2.19.0 — 2026-09-10 — Finding review and ECO
|
## 2.19.0 — 2026-09-10 — Finding review and ECO
|
||||||
|
|
||||||
@@ -219,14 +227,14 @@ The Impedance tab uses the closed-form engine from ImpedenceFinder (Hammerstad
|
|||||||
|
|
||||||
Distributor lifecycle is a cached check, not a review scrape. Errata and layout_rules stay structured and skip when the catalog or the PDF has no number.
|
Distributor lifecycle is a cached check, not a review scrape. Errata and layout_rules stay structured and skip when the catalog or the PDF has no number.
|
||||||
|
|
||||||
- [New] `PS-LF-001` EOL, `PS-LF-002` NRND, `PS-LF-003` explicit RoHS fail. Replacement only if the distributor lists it. Active / RoHS N/A / missing cache row are silent.
|
- [New] `PE-LF-001` EOL, `PE-LF-002` NRND, `PE-LF-003` explicit RoHS fail. Replacement only if the distributor lists it. Active / RoHS N/A / missing cache row are silent.
|
||||||
- [New] `PS-ERRATA-001` when a catalogued workaround pull-up is missing. No URL → skip.
|
- [New] `PE-ERRATA-001` when a catalogued workaround pull-up is missing. No URL → skip.
|
||||||
- [New] `PS-INT-001` when `internal_features.pullup_pins` has no rail resistor.
|
- [New] `PE-INT-001` when `internal_features.pullup_pins` has no rail resistor.
|
||||||
- [New] `layout_rules` closed kinds; non-numeric `max_distance_mm` is stored as null.
|
- [New] `layout_rules` closed kinds; non-numeric `max_distance_mm` is stored as null.
|
||||||
|
|
||||||
## 2.16.0 — 2026-09-10 — KiCad cad-bridge
|
## 2.16.0 — 2026-09-10 — KiCad cad-bridge
|
||||||
|
|
||||||
Pinscope writes `pinscope-findings.json` next to the report so a KiCad 9/10 action plugin can pan to the symbol uuid on the right sheet.
|
Periscope writes `periscope-findings.json` next to the report so a KiCad 9/10 action plugin can pan to the symbol uuid on the right sheet.
|
||||||
|
|
||||||
- [New] E2 cad-bridge JSON (`version`, `ref`, `pins`, `sheet`, `uuid`, `severity`). Layout rule ids target pcbnew; others target eeschema.
|
- [New] E2 cad-bridge JSON (`version`, `ref`, `pins`, `sheet`, `uuid`, `severity`). Layout rule ids target pcbnew; others target eeschema.
|
||||||
- [New] `.kicad_sch` symbols keep `cad_uuid` + `cad_sheet` in `cad_index` (child sheet, not the empty root).
|
- [New] `.kicad_sch` symbols keep `cad_uuid` + `cad_sheet` in `cad_index` (child sheet, not the empty root).
|
||||||
@@ -236,17 +244,17 @@ Pinscope writes `pinscope-findings.json` next to the report so a KiCad 9/10 acti
|
|||||||
|
|
||||||
Schema checks now compare regulator load to Iout_max, look at PG→EN when a sequence is declared, and treat DNP as a fitted-variant graph.
|
Schema checks now compare regulator load to Iout_max, look at PG→EN when a sequence is declared, and treat DNP as a fitted-variant graph.
|
||||||
|
|
||||||
- [New] `PS-PWR-001` when specified IQ+I_load exceeds Iout_max, or an explicit series R/ferrite DCR drops >5% of the rail. Missing IQ and PCB traces are not guessed.
|
- [New] `PE-PWR-001` when specified IQ+I_load exceeds Iout_max, or an explicit series R/ferrite DCR drops >5% of the rail. Missing IQ and PCB traces are not guessed.
|
||||||
- [New] `PS-SEQ-001` WARNING if `power_sequence` is in IC specs and upstream PG does not net to downstream EN.
|
- [New] `PE-SEQ-001` WARNING if `power_sequence` is in IC specs and upstream PG does not net to downstream EN.
|
||||||
- [New] BOM `DNP`/`Fitted`/`Variant` on `bom_fields`. Fitted enable with only a DNP pull is `PS-DNP-001` ERROR; no DNP column skips the check.
|
- [New] BOM `DNP`/`Fitted`/`Variant` on `bom_fields`. Fitted enable with only a DNP pull is `PE-DNP-001` ERROR; no DNP column skips the check.
|
||||||
|
|
||||||
## 2.14.0 — 2026-09-10 — Filtri e termico schema
|
## 2.14.0 — 2026-09-10 — Filtri e termico schema
|
||||||
|
|
||||||
Deterministic checks now match RC/LC/π/T filters and estimate LDO/resistor dissipation without inventing missing numbers.
|
Deterministic checks now match RC/LC/π/T filters and estimate LDO/resistor dissipation without inventing missing numbers.
|
||||||
|
|
||||||
- [New] Filter topology `PS-FLT-001` (fc INFO) / `PS-FLT-002` vs `adc_sample_rate` only when that spec exists. Ferrite DCR `PS-FLT-003` only with a datasheet limit.
|
- [New] Filter topology `PE-FLT-001` (fc INFO) / `PE-FLT-002` vs `adc_sample_rate` only when that spec exists. Ferrite DCR `PE-FLT-003` only with a datasheet limit.
|
||||||
- [New] LDO `P = I_load×(Vin−Vout)` and `Tj = 25 + P·θJA`. Missing θJA is `PS-TH-001` INFO. `Iout_max` is not treated as load.
|
- [New] LDO `P = I_load×(Vin−Vout)` and `Tj = 25 + P·θJA`. Missing θJA is `PE-TH-001` INFO. `Iout_max` is not treated as load.
|
||||||
- [New] Resistor `I²R` vs `power_rating_w` on LED paths and shunts with known ΔV (`PS-TH-003`).
|
- [New] Resistor `I²R` vs `power_rating_w` on LED paths and shunts with known ΔV (`PE-TH-003`).
|
||||||
|
|
||||||
## 2.13.0 — 2026-09-10 — DC-bias C_eff stima
|
## 2.13.0 — 2026-09-10 — DC-bias C_eff stima
|
||||||
|
|
||||||
@@ -254,20 +262,20 @@ The derating table now shows an effective capacitance under DC bias for C0G/X7R/
|
|||||||
|
|
||||||
- [New] `C_eff` column from an empirical V/Vrated table. Tantalum/electrolytic and unknown dielectrics are left blank.
|
- [New] `C_eff` column from an empirical V/Vrated table. Tantalum/electrolytic and unknown dielectrics are left blank.
|
||||||
- [Improved] C0G/NP0 stays at nominal C; X7R at 50% of rated V is about 70% of C.
|
- [Improved] C0G/NP0 stays at nominal C; X7R at 50% of rated V is about 70% of C.
|
||||||
- [New] Bulk C without a ~100 nF ceramic is `PS-ESR-001` INFO (no invented Z(f) target).
|
- [New] Bulk C without a ~100 nF ceramic is `PE-ESR-001` INFO (no invented Z(f) target).
|
||||||
|
|
||||||
## 2.12.0 — 2026-09-10 — Pull-up sizing and LDO Cout
|
## 2.12.0 — 2026-09-10 — Pull-up sizing and LDO Cout
|
||||||
|
|
||||||
Deterministic schema checks now size I2C pull-ups, flag NRST pull-downs, and look at LDO VOUT capacitance — still WARNING, never a invented datasheet µF ERROR.
|
Deterministic schema checks now size I2C pull-ups, flag NRST pull-downs, and look at LDO VOUT capacitance — still WARNING, never a invented datasheet µF ERROR.
|
||||||
|
|
||||||
- [New] I2C pull-up value vs a wide NXP UM10204 band (`PS-I2C-002`). 4.7 kΩ is in-band; missing values are not sized.
|
- [New] I2C pull-up value vs a wide NXP UM10204 band (`PE-I2C-002`). 4.7 kΩ is in-band; missing values are not sized.
|
||||||
- [New] Active-low reset with a resistor to ground is `PS-RST-002`.
|
- [New] Active-low reset with a resistor to ground is `PE-RST-002`.
|
||||||
- [New] Regulator VOUT needs Cout (`PS-DEC-001`); 100 nF-only on VOUT is `PS-DEC-002`. MCU VDD 100 nF is not flagged.
|
- [New] Regulator VOUT needs Cout (`PE-DEC-001`); 100 nF-only on VOUT is `PE-DEC-002`. MCU VDD 100 nF is not flagged.
|
||||||
- [Improved] Pin-mux UART0 on the `simple_project` MSPM0 nets is covered in tests (SPI PICO/POCI already was).
|
- [Improved] Pin-mux UART0 on the `simple_project` MSPM0 nets is covered in tests (SPI PICO/POCI already was).
|
||||||
|
|
||||||
## 2.11.0 — 2026-09-10 — DeepSeek V4.1 and re-analyze
|
## 2.11.0 — 2026-09-10 — DeepSeek V4.1 and re-analyze
|
||||||
|
|
||||||
Pinscope now defaults to DeepSeek-V4.1-Flash (`deepseek-flash`) for every LLM stage, shows API cost in dollars, and lets you replace the BOM and netlist on an existing project without deleting it.
|
Periscope now defaults to DeepSeek-V4.1-Flash (`deepseek-flash`) for every LLM stage, shows API cost in dollars, and lets you replace the BOM and netlist on an existing project without deleting it.
|
||||||
|
|
||||||
- [New] Default model is `deepseek-flash` (native vision). Legacy `deepseek-v4-flash` / `deepseek-v4-flash-vision-exp` names still work; they route to V4.1.
|
- [New] Default model is `deepseek-flash` (native vision). Legacy `deepseek-v4-flash` / `deepseek-v4-flash-vision-exp` names still work; they route to V4.1.
|
||||||
- [New] Replace BOM & netlist on a finished project and re-run the analysis. History, library cache, and prior spend stay on the same project.
|
- [New] Replace BOM & netlist on a finished project and re-run the analysis. History, library cache, and prior spend stay on the same project.
|
||||||
@@ -291,7 +299,7 @@ Chips and passives stay in a shared library after the first look, so the next bo
|
|||||||
|
|
||||||
## 2.8.0 — 2026-08-27 — Automatic datasheets
|
## 2.8.0 — 2026-08-27 — Automatic datasheets
|
||||||
|
|
||||||
Pinscope now finds datasheet PDFs on its own. You can still upload a file, but you no longer need DigiKey keys for the common case.
|
Periscope now finds datasheet PDFs on its own. You can still upload a file, but you no longer need DigiKey keys for the common case.
|
||||||
|
|
||||||
- [New] Auto-fetch from LCSC (no API key) by manufacturer part number or LCSC code, with exact-MPN matching so a CH340E never silently becomes a CH340G.
|
- [New] Auto-fetch from LCSC (no API key) by manufacturer part number or LCSC code, with exact-MPN matching so a CH340E never silently becomes a CH340G.
|
||||||
- [New] Direct Texas Instruments datasheet URLs (`ti.com/lit/ds/symlink/…`) as a second source for TI parts.
|
- [New] Direct Texas Instruments datasheet URLs (`ti.com/lit/ds/symlink/…`) as a second source for TI parts.
|
||||||
@@ -300,7 +308,7 @@ Pinscope now finds datasheet PDFs on its own. You can still upload a file, but y
|
|||||||
|
|
||||||
## 2.7.0 — 2026-08-27 — DeepSeek API
|
## 2.7.0 — 2026-08-27 — DeepSeek API
|
||||||
|
|
||||||
Pinscope now talks to DeepSeek by default. Extraction skills run locally; datasheet PDFs are converted to text (and page images on the vision model) because DeepSeek does not accept native PDF documents.
|
Periscope now talks to DeepSeek by default. Extraction skills run locally; datasheet PDFs are converted to text (and page images on the vision model) because DeepSeek does not accept native PDF documents.
|
||||||
|
|
||||||
- [New] DeepSeek provider (`deepseek-v4-flash`, `deepseek-v4-pro`, `deepseek-v4-flash-vision-exp`) via the OpenAI-compatible Chat Completions API.
|
- [New] DeepSeek provider (`deepseek-v4-flash`, `deepseek-v4-pro`, `deepseek-v4-flash-vision-exp`) via the OpenAI-compatible Chat Completions API.
|
||||||
- [New] Local skill runner: `skills/*/SKILL.md` is inlined and `validate.py` runs in-process — no Anthropic Console upload required.
|
- [New] Local skill runner: `skills/*/SKILL.md` is inlined and `validate.py` runs in-process — no Anthropic Console upload required.
|
||||||
@@ -309,7 +317,7 @@ Pinscope now talks to DeepSeek by default. Extraction skills run locally; datash
|
|||||||
|
|
||||||
## 2.6.0 — 2026-07-12 — Export Report to Excel
|
## 2.6.0 — 2026-07-12 — Export Report to Excel
|
||||||
|
|
||||||
Download a project's findings as an Excel spreadsheet straight from the report — one click, ready to share, filter, or archive outside Pinscope.
|
Download a project's findings as an Excel spreadsheet straight from the report — one click, ready to share, filter, or archive outside Periscope.
|
||||||
|
|
||||||
- [New] "Export Excel" button on the validation report. Every finding becomes a spreadsheet row — designator, part number, ID, severity, title, description, recommendation, and its datasheet source (page included) — sorted most-severe first.
|
- [New] "Export Excel" button on the validation report. Every finding becomes a spreadsheet row — designator, part number, ID, severity, title, description, recommendation, and its datasheet source (page included) — sorted most-severe first.
|
||||||
|
|
||||||
@@ -321,7 +329,7 @@ Schematic review now works through every functional area of a component before f
|
|||||||
|
|
||||||
## 2.5.0 — 2026-07-02 — Light Mode
|
## 2.5.0 — 2026-07-02 — Light Mode
|
||||||
|
|
||||||
Pinscope now has a light theme. Toggle between light and dark with the sun/moon button — in the sidebar next to your account menu, or in the header on the website.
|
Periscope now has a light theme. Toggle between light and dark with the sun/moon button — in the sidebar next to your account menu, or in the header on the website.
|
||||||
|
|
||||||
- [New] Theme toggle. Switch between light and dark mode anywhere in the app; your choice is remembered on this device. Everything defaults to dark, exactly as before, until you flip it.
|
- [New] Theme toggle. Switch between light and dark mode anywhere in the app; your choice is remembered on this device. Everything defaults to dark, exactly as before, until you flip it.
|
||||||
- [Improved] Every status color — error, warning, and pass badges, finding cards, the progress view, billing — is tuned for both themes, so reports stay legible either way.
|
- [Improved] Every status color — error, warning, and pass badges, finding cards, the progress view, billing — is tuned for both themes, so reports stay legible either way.
|
||||||
@@ -331,14 +339,14 @@ Pinscope now has a light theme. Toggle between light and dark with the sun/moon
|
|||||||
|
|
||||||
Two datasheet-grounded checks now run on every project, independent of the schematic review — catching a swapped-peripheral pin or an over-driven LED — plus a clear list of any components that had no datasheet to review against.
|
Two datasheet-grounded checks now run on every project, independent of the schematic review — catching a swapped-peripheral pin or an over-driven LED — plus a clear list of any components that had no datasheet to review against.
|
||||||
|
|
||||||
- [New] Pin-function feasibility check. Pinscope now flags when a net assigns an IC pin a peripheral function its silicon can't route — for example a `UART5_TX` net on a pin whose alternate-function table only offers `UART5_RX`. It's reported as an error straight from the datasheet's pin table and names the likely swap (TX↔RX, SDA↔SCL). It deliberately does not judge signal *direction* across an interface — a direct UART crosses TX↔RX while a transceiver runs straight through — so it only fires on physically impossible pin assignments, never on wiring style.
|
- [New] Pin-function feasibility check. Periscope now flags when a net assigns an IC pin a peripheral function its silicon can't route — for example a `UART5_TX` net on a pin whose alternate-function table only offers `UART5_RX`. It's reported as an error straight from the datasheet's pin table and names the likely swap (TX↔RX, SDA↔SCL). It deliberately does not judge signal *direction* across an interface — a direct UART crosses TX↔RX while a transceiver runs straight through — so it only fires on physically impossible pin assignments, never on wiring style.
|
||||||
- [New] LED forward-current check. For each LED, Pinscope computes the forward current from the supply rail, the series resistor, and the LED's rated forward voltage, and flags any channel whose current exceeds the LED's rated maximum. Each color of an RGB LED is checked separately, and a leg with no current-limiting resistor at all is called out as a caution.
|
- [New] LED forward-current check. For each LED, Periscope computes the forward current from the supply rail, the series resistor, and the LED's rated forward voltage, and flags any channel whose current exceeds the LED's rated maximum. Each color of an RGB LED is checked separately, and a leg with no current-limiting resistor at all is called out as a caution.
|
||||||
- [New] "Not reviewed" list on the report. Components with no datasheet on file — for instance a do-not-populate footprint that isn't in the BOM — are now called out explicitly, so a mis-wired pin on an unreviewed part shows up as a known gap instead of being silently absent.
|
- [New] "Not reviewed" list on the report. Components with no datasheet on file — for instance a do-not-populate footprint that isn't in the BOM — are now called out explicitly, so a mis-wired pin on an unreviewed part shows up as a known gap instead of being silently absent.
|
||||||
- [New] Findings from these automatic checks carry an "Automated check" badge, so they're easy to tell apart from datasheet-review findings.
|
- [New] Findings from these automatic checks carry an "Automated check" badge, so they're easy to tell apart from datasheet-review findings.
|
||||||
|
|
||||||
## 2.3.3 — 2026-06-08 — Faster Reviews
|
## 2.3.3 — 2026-06-08 — Faster Reviews
|
||||||
|
|
||||||
Multi-chip designs now review several times faster — Pinscope works through ICs in parallel instead of one at a time.
|
Multi-chip designs now review several times faster — Periscope works through ICs in parallel instead of one at a time.
|
||||||
|
|
||||||
- [Improved] Datasheet extraction and schematic review now process multiple ICs at once, so reports on multi-IC projects come back substantially faster. The findings are unchanged — only the wait is shorter.
|
- [Improved] Datasheet extraction and schematic review now process multiple ICs at once, so reports on multi-IC projects come back substantially faster. The findings are unchanged — only the wait is shorter.
|
||||||
|
|
||||||
@@ -364,7 +372,7 @@ EDIF 2.0.0 netlists upload alongside PADS-PCB, with a sub-design picker for file
|
|||||||
|
|
||||||
## 2.3.0 — 2026-05-24 — LCSC Part Number Support
|
## 2.3.0 — 2026-05-24 — LCSC Part Number Support
|
||||||
|
|
||||||
JLCPCB-style BOMs with LCSC part numbers (e.g. `C12044`) now work out of the box — Pinscope auto-detects the column, resolves each id to the real manufacturer part number, and shows you what it resolved to before the pipeline runs.
|
JLCPCB-style BOMs with LCSC part numbers (e.g. `C12044`) now work out of the box — Periscope auto-detects the column, resolves each id to the real manufacturer part number, and shows you what it resolved to before the pipeline runs.
|
||||||
|
|
||||||
- [New] LCSC part numbers in the manufacturer part number column are auto-detected at BOM upload and converted to real MPNs. Works with JLCPCB / EasyEDA exports without any column renaming.
|
- [New] LCSC part numbers in the manufacturer part number column are auto-detected at BOM upload and converted to real MPNs. Works with JLCPCB / EasyEDA exports without any column renaming.
|
||||||
- [New] Project setup now shows the LCSC → MPN mapping on each IC row in the datasheet step (e.g. `C12044 → TP4057-42-SOT26-R`), so you can see what each LCSC id became before the pipeline starts.
|
- [New] Project setup now shows the LCSC → MPN mapping on each IC row in the datasheet step (e.g. `C12044 → TP4057-42-SOT26-R`), so you can see what each LCSC id became before the pipeline starts.
|
||||||
@@ -378,7 +386,7 @@ Tabbed file upload guide with per-tool instructions, Xpedition coverage, and dir
|
|||||||
- [New] Documentation for exporting a PADS-PCB netlist from Siemens Xpedition Designer / DxDesigner (VX.2.x, including VX.2.14).
|
- [New] Documentation for exporting a PADS-PCB netlist from Siemens Xpedition Designer / DxDesigner (VX.2.x, including VX.2.14).
|
||||||
- [Improved] File upload guide reorganized into tabs — KiCad, Altium, OrCAD/Allegro, Xpedition, EasyEDA, and Eagle each get their own panel.
|
- [Improved] File upload guide reorganized into tabs — KiCad, Altium, OrCAD/Allegro, Xpedition, EasyEDA, and Eagle each get their own panel.
|
||||||
- [Improved] Netlist uploads now accept `.asc`, `.net`, `.NET`, and `.txt` directly — no more renaming required before upload.
|
- [Improved] Netlist uploads now accept `.asc`, `.net`, `.NET`, and `.txt` directly — no more renaming required before upload.
|
||||||
- [Improved] File guide now calls out the difference between the PADS-PCB schematic netlist Pinscope needs and the `!PADS-POWERPCB` PCB-layout dump that some EDA tools also save as `.asc`.
|
- [Improved] File guide now calls out the difference between the PADS-PCB schematic netlist Periscope needs and the `!PADS-POWERPCB` PCB-layout dump that some EDA tools also save as `.asc`.
|
||||||
|
|
||||||
## 2.2.0 — 2026-05-20 — Cross-chip Datasheet Review
|
## 2.2.0 — 2026-05-20 — Cross-chip Datasheet Review
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
# File Upload Guide
|
# File Upload Guide
|
||||||
|
|
||||||
Pinscope needs two files from your EDA tool to review a schematic, and an optional KiCad board for layout:
|
Periscope needs two files from your EDA tool to review a schematic, and an optional KiCad board for layout:
|
||||||
|
|
||||||
- A **PADS-PCB ASCII netlist** or an **EDIF 2.0.0 netlist** — the circuit's connectivity. Pinscope accepts `.asc`, `.net`, `.NET`, `.txt` (PADS-PCB) and `.edn`, `.edif`, `.edf` (EDIF); the format is auto-detected from the file's first bytes.
|
- A **PADS-PCB ASCII netlist** or an **EDIF 2.0.0 netlist** — the circuit's connectivity. Periscope accepts `.asc`, `.net`, `.NET`, `.txt` (PADS-PCB) and `.edn`, `.edif`, `.edf` (EDIF); the format is auto-detected from the file's first bytes.
|
||||||
- A **Bill of Materials** (CSV or XLSX) — mapping each reference designator to a manufacturer part number.
|
- A **Bill of Materials** (CSV or XLSX) — mapping each reference designator to a manufacturer part number.
|
||||||
- Optional: a **KiCad PCB** (`.kicad_pcb`) — placement, keepout, pair length, and net Z0. Schematic review still runs without it.
|
- Optional: a **KiCad PCB** (`.kicad_pcb`) — placement, keepout, pair length, and net Z0. Schematic review still runs without it.
|
||||||
|
|
||||||
## Example files
|
## Example files
|
||||||
|
|
||||||
New to Pinscope? Here's a complete set of files from [Phil's Lab](https://www.youtube.com/@PhilsLab)' KiCad 9 TI MSPM0 tutorial you can download and upload as a starter project:
|
New to Periscope? Here's a complete set of files from [Phil's Lab](https://www.youtube.com/@PhilsLab)' KiCad 9 TI MSPM0 tutorial you can download and upload as a starter project:
|
||||||
|
|
||||||
- [TI-MSP-KICAD9-TUTORIAL.asc](/examples/TI-MSP-KICAD9-TUTORIAL.asc) — netlist
|
- [TI-MSP-KICAD9-TUTORIAL.asc](/examples/TI-MSP-KICAD9-TUTORIAL.asc) — netlist
|
||||||
- [TI-MSP-KICAD9-TUTORIAL.csv](/examples/TI-MSP-KICAD9-TUTORIAL.csv) — BOM
|
- [TI-MSP-KICAD9-TUTORIAL.csv](/examples/TI-MSP-KICAD9-TUTORIAL.csv) — BOM
|
||||||
- [TI-MSP-KICAD9-TUTORIAL-SCHEMATIC.pdf](/examples/TI-MSP-KICAD9-TUTORIAL-SCHEMATIC.pdf) — schematic (for your reference; Pinscope doesn't need this)
|
- [TI-MSP-KICAD9-TUTORIAL-SCHEMATIC.pdf](/examples/TI-MSP-KICAD9-TUTORIAL-SCHEMATIC.pdf) — schematic (for your reference; Periscope doesn't need this)
|
||||||
|
|
||||||
The BOM below shows the shape Pinscope is looking for.
|
The BOM below shows the shape Periscope is looking for.
|
||||||
|
|
||||||
## The BOM
|
## The BOM
|
||||||
|
|
||||||
Pinscope auto-detects BOM columns by header name. After upload you'll confirm which column holds designators and which holds part numbers, so the exact header names don't matter — only that these columns exist.
|
Periscope auto-detects BOM columns by header name. After upload you'll confirm which column holds designators and which holds part numbers, so the exact header names don't matter — only that these columns exist.
|
||||||
|
|
||||||
**Required**
|
**Required**
|
||||||
|
|
||||||
- **Designator / Reference** — one row per part, or grouped references like `C1,C2,C5` in a single row (Pinscope expands these automatically).
|
- **Designator / Reference** — one row per part, or grouped references like `C1,C2,C5` in a single row (Periscope expands these automatically).
|
||||||
- **Manufacturer Part Number (MPN)** — the full orderable part number. Pinscope uses this to look up datasheets, so `10uF 0805` on its own is **not** enough — it needs e.g. `GRM21BR61C106KE15L`.
|
- **Manufacturer Part Number (MPN)** — the full orderable part number. Periscope uses this to look up datasheets, so `10uF 0805` on its own is **not** enough — it needs e.g. `GRM21BR61C106KE15L`.
|
||||||
|
|
||||||
**Recommended**
|
**Recommended**
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ CSV and XLSX both work. For XLSX, the first worksheet is used.
|
|||||||
|
|
||||||
## The Netlist
|
## The Netlist
|
||||||
|
|
||||||
Pinscope accepts either a **PADS-PCB ASCII netlist** or an **EDIF 2.0.0 netlist** — the upload form auto-detects which one you sent based on the file's first bytes, so you don't have to pick a format.
|
Periscope accepts either a **PADS-PCB ASCII netlist** or an **EDIF 2.0.0 netlist** — the upload form auto-detects which one you sent based on the file's first bytes, so you don't have to pick a format.
|
||||||
|
|
||||||
### PADS-PCB ASCII
|
### PADS-PCB ASCII
|
||||||
|
|
||||||
@@ -55,18 +55,18 @@ U1.24 C1.1
|
|||||||
*END*
|
*END*
|
||||||
```
|
```
|
||||||
|
|
||||||
If your file starts with `*PADS-PCB*` and ends with `*END*`, you're good. Reference designators may contain spaces (e.g. `CV GND`) — Pinscope resolves them against your BOM.
|
If your file starts with `*PADS-PCB*` and ends with `*END*`, you're good. Reference designators may contain spaces (e.g. `CV GND`) — Periscope resolves them against your BOM.
|
||||||
|
|
||||||
**Heads up — two different `.asc` files exist.** PADS (and tools that interop with PADS, like Xpedition) use the `.asc` extension for two unrelated things:
|
**Heads up — two different `.asc` files exist.** PADS (and tools that interop with PADS, like Xpedition) use the `.asc` extension for two unrelated things:
|
||||||
|
|
||||||
- The **schematic-exported netlist** starts with `*PADS-PCB*` and lists `*PART*` / `*NET*` sections. **This is what Pinscope wants.**
|
- The **schematic-exported netlist** starts with `*PADS-PCB*` and lists `*PART*` / `*NET*` sections. **This is what Periscope wants.**
|
||||||
- The **full PCB layout dump** starts with `!PADS-POWERPCB-V…` and contains routing/footprint geometry. Pinscope cannot parse this.
|
- The **full PCB layout dump** starts with `!PADS-POWERPCB-V…` and contains routing/footprint geometry. Periscope cannot parse this.
|
||||||
|
|
||||||
If your upload errors with "No components found", check the first line of the file.
|
If your upload errors with "No components found", check the first line of the file.
|
||||||
|
|
||||||
### EDIF 2.0.0
|
### EDIF 2.0.0
|
||||||
|
|
||||||
EDIF is a vendor-neutral s-expression format. The first form is `(edif …`, with an `(edifVersion 2 0 0)` declaration near the top, libraries that define each cell's pin list, and a design library that lists `(instance …)` and `(net …)` forms. Pinscope has been verified against **Siemens xDX Designer / DxDesigner** exports; other EDIF 2.0.0 exporters (OrCAD, Altium, KiCad, Eagle) follow the same grammar and should work, but haven't been broadly tested. If your EDIF file doesn't parse, [contact us](/contact) and send a snippet — most fixes are small.
|
EDIF is a vendor-neutral s-expression format. The first form is `(edif …`, with an `(edifVersion 2 0 0)` declaration near the top, libraries that define each cell's pin list, and a design library that lists `(instance …)` and `(net …)` forms. Periscope has been verified against **Siemens xDX Designer / DxDesigner** exports; other EDIF 2.0.0 exporters (OrCAD, Altium, KiCad, Eagle) follow the same grammar and should work, but haven't been broadly tested. If your EDIF file doesn't parse, [contact us](/contact) and send a snippet — most fixes are small.
|
||||||
|
|
||||||
If your tool exports both PADS-PCB and EDIF, PADS-PCB is the path more users have validated; use EDIF when it's the only option.
|
If your tool exports both PADS-PCB and EDIF, PADS-PCB is the path more users have validated; use EDIF when it's the only option.
|
||||||
|
|
||||||
@@ -127,17 +127,17 @@ Works in **Xpedition Designer / DxDesigner** (VX.2.x, including VX.2.14). Xpedit
|
|||||||
3. **Tools → PCB Interface** → choose the **PADS** template (`pads2007.cfg` or equivalent).
|
3. **Tools → PCB Interface** → choose the **PADS** template (`pads2007.cfg` or equivalent).
|
||||||
4. Run the export. The output's first line should be `*PADS-PCB*` with `*PART*` and `*NET*` sections below. The file extension (`.txt`, `.net`, or `.asc`) doesn't matter — upload it as-is.
|
4. Run the export. The output's first line should be `*PADS-PCB*` with `*PART*` and `*NET*` sections below. The file extension (`.txt`, `.net`, or `.asc`) doesn't matter — upload it as-is.
|
||||||
|
|
||||||
If your project is locked to the integrated Xpedition flow, **export EDIF instead** — DxDesigner's EDIF Exporter runs against any project type and Pinscope accepts the resulting `.edn` as an equivalent input. See **Netlist (EDIF alternative)** below.
|
If your project is locked to the integrated Xpedition flow, **export EDIF instead** — DxDesigner's EDIF Exporter runs against any project type and Periscope accepts the resulting `.edn` as an equivalent input. See **Netlist (EDIF alternative)** below.
|
||||||
|
|
||||||
**Netlist (EDIF alternative)**
|
**Netlist (EDIF alternative)**
|
||||||
|
|
||||||
1. Open the schematic in Xpedition Designer / DxDesigner.
|
1. Open the schematic in Xpedition Designer / DxDesigner.
|
||||||
2. **File → Export → EDIF…** (older builds: **Tools → Run Tool → Edif Exporter**).
|
2. **File → Export → EDIF…** (older builds: **Tools → Run Tool → Edif Exporter**).
|
||||||
3. In the export dialog:
|
3. In the export dialog:
|
||||||
- **EDIF version**: **2.0.0** (Pinscope only supports 2.0.0)
|
- **EDIF version**: **2.0.0** (Periscope only supports 2.0.0)
|
||||||
- **EDIF level**: **0** (the default)
|
- **EDIF level**: **0** (the default)
|
||||||
- **Output format**: **Netlist view** — make sure cells, instances, nets, and the `viewMap` back-annotation block are all included
|
- **Output format**: **Netlist view** — make sure cells, instances, nets, and the `viewMap` back-annotation block are all included
|
||||||
- **Designator source**: include back-annotated designators (otherwise instances export as templates like `U?` / `R?` and Pinscope drops them)
|
- **Designator source**: include back-annotated designators (otherwise instances export as templates like `U?` / `R?` and Periscope drops them)
|
||||||
4. Save as `<design>.edn` and upload it as the netlist. The file should start with `(edif …)` and contain `(edifVersion 2 0 0)` near the top.
|
4. Save as `<design>.edn` and upload it as the netlist. The file should start with `(edif …)` and contain `(edifVersion 2 0 0)` near the top.
|
||||||
|
|
||||||
If neither PADS nor EDIF works for your project setup, send us the BOM and schematic PDF via [Contact](/contact) — we can usually help unblock the export.
|
If neither PADS nor EDIF works for your project setup, send us the BOM and schematic PDF via [Contact](/contact) — we can usually help unblock the export.
|
||||||
@@ -178,10 +178,10 @@ For both EasyEDA Standard and EasyEDA Pro.
|
|||||||
|
|
||||||
- **"No components found"** — your netlist is missing the `*PART*` section. Re-export specifically in PADS-PCB format (not Spice, Protel, or a generic text netlist). If the first line is `!PADS-POWERPCB-V…`, you uploaded the PCB layout dump instead of the schematic netlist — re-export from the schematic side.
|
- **"No components found"** — your netlist is missing the `*PART*` section. Re-export specifically in PADS-PCB format (not Spice, Protel, or a generic text netlist). If the first line is `!PADS-POWERPCB-V…`, you uploaded the PCB layout dump instead of the schematic netlist — re-export from the schematic side.
|
||||||
- **"No ground net found"** — your netlist has no net named `GND`, `VSS`, `AGND`, `DGND`, or similar. If you exported a sub-sheet, re-export the top sheet instead.
|
- **"No ground net found"** — your netlist has no net named `GND`, `VSS`, `AGND`, `DGND`, or similar. If you exported a sub-sheet, re-export the top sheet instead.
|
||||||
- **Unresolved parts after the pipeline runs** — a BOM row has no MPN, or the MPN wasn't found on DigiKey. Add the MPN, or rely on Pinscope's value fallback (fills in from the `Value` / `Comment` column).
|
- **Unresolved parts after the pipeline runs** — a BOM row has no MPN, or the MPN wasn't found on DigiKey. Add the MPN, or rely on Periscope's value fallback (fills in from the `Value` / `Comment` column).
|
||||||
|
|
||||||
## The KiCad PCB (optional)
|
## The KiCad PCB (optional)
|
||||||
|
|
||||||
Drop the KiCad **project folder** (or a zip of it) on Schematic — Pinscope takes the sheets and the `.kicad_pcb` if it is there. You can also add the board later.
|
Drop the KiCad **project folder** (or a zip of it) on Schematic — Periscope takes the sheets and the `.kicad_pcb` if it is there. You can also add the board later.
|
||||||
|
|
||||||
Still stuck? [Contact us](/contact) with your netlist and BOM attached and we'll take a look.
|
Still stuck? [Contact us](/contact) with your netlist and BOM attached and we'll take a look.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Pinscope Privacy Policy
|
# Periscope Privacy Policy
|
||||||
|
|
||||||
**Last updated: April 5, 2026**
|
**Last updated: April 5, 2026**
|
||||||
|
|
||||||
This Privacy Policy explains how Faradworks, Inc. ("Faradworks," "we," "us," and "our") collects, uses, and discloses information in connection with the Pinscope website (pinscope.ai), platform, and related services (the "Service").
|
This Privacy Policy explains how Faradworks, Inc. ("Faradworks," "we," "us," and "our") collects, uses, and discloses information in connection with the Periscope website (periscope.michelebigi.it), platform, and related services (the "Service").
|
||||||
|
|
||||||
This Privacy Policy is intended for free users and self-serve paid users. Enterprise customers typically use the Service under a separate agreement and (if applicable) a data processing agreement ("DPA"), which may include additional privacy and security terms.
|
This Privacy Policy is intended for free users and self-serve paid users. Enterprise customers typically use the Service under a separate agreement and (if applicable) a data processing agreement ("DPA"), which may include additional privacy and security terms.
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Pinscope Terms of Service
|
# Periscope Terms of Service
|
||||||
|
|
||||||
**Last updated: April 5, 2026**
|
**Last updated: April 5, 2026**
|
||||||
|
|
||||||
These Terms of Service ("Terms") govern access to and use of the Pinscope platform (pinscope.ai) and related services (the "Service"). These Terms apply to free users and self-serve paid users. If you have a separate written agreement signed by Faradworks, Inc. (for example, an enterprise agreement), that agreement governs your use of the Service to the extent it conflicts with these Terms.
|
These Terms of Service ("Terms") govern access to and use of the Periscope platform (periscope.michelebigi.it) and related services (the "Service"). These Terms apply to free users and self-serve paid users. If you have a separate written agreement signed by Faradworks, Inc. (for example, an enterprise agreement), that agreement governs your use of the Service to the extent it conflicts with these Terms.
|
||||||
|
|
||||||
These Terms incorporate Faradworks' [Privacy Policy](/privacy) and any policies referenced in the Service.
|
These Terms incorporate Faradworks' [Privacy Policy](/privacy) and any policies referenced in the Service.
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ By creating an account, clicking to accept these Terms (for example, by clicking
|
|||||||
|
|
||||||
**"Derived Data"** means technical artifacts generated solely to operate the Service, such as parsed text, indexes, embeddings, summaries, or extracted metadata.
|
**"Derived Data"** means technical artifacts generated solely to operate the Service, such as parsed text, indexes, embeddings, summaries, or extracted metadata.
|
||||||
|
|
||||||
**"Service"** means the Pinscope platform (pinscope.ai), including all features, APIs, and related services.
|
**"Service"** means the Periscope platform (periscope.michelebigi.it), including all features, APIs, and related services.
|
||||||
|
|
||||||
**"Content"** refers collectively to Customer Content, Outputs, and Derived Data.
|
**"Content"** refers collectively to Customer Content, Outputs, and Derived Data.
|
||||||
|
|
||||||
@@ -170,15 +170,15 @@ Plans may include limits on reviews, tokens, files, API spend, usage allocations
|
|||||||
|
|
||||||
### 11.2 Prepaid service credits.
|
### 11.2 Prepaid service credits.
|
||||||
|
|
||||||
Certain Services, plans, or features may allow or require you to prepay for future eligible Pinscope review services for professional, business, or organizational use by purchasing prepaid service credits ("Usage Credits"). Usage Credits represent a prepaid, limited, revocable, non-transferable license to access eligible Pinscope review services up to the applicable credited amount and may be used only for eligible Pinscope review charges as described in the Service. Faradworks may also, in its sole discretion, provide free or promotional credits ("Promotional Credits"), which may be subject to additional restrictions or expiration dates stated when issued.
|
Certain Services, plans, or features may allow or require you to prepay for future eligible Periscope review services for professional, business, or organizational use by purchasing prepaid service credits ("Usage Credits"). Usage Credits represent a prepaid, limited, revocable, non-transferable license to access eligible Periscope review services up to the applicable credited amount and may be used only for eligible Periscope review charges as described in the Service. Faradworks may also, in its sole discretion, provide free or promotional credits ("Promotional Credits"), which may be subject to additional restrictions or expiration dates stated when issued.
|
||||||
|
|
||||||
### 11.3 Credit characteristics and workspace scope.
|
### 11.3 Credit characteristics and workspace scope.
|
||||||
|
|
||||||
Credits may be used only for eligible Pinscope review charges and may not be used for any other product or service unless Faradworks expressly states otherwise in the Service. Credits are not legal tender, are not currency, are not redeemable for cash, are not refundable except as required by law or expressly stated by Faradworks, do not constitute or confer any personal property right, and do not constitute a bank account, deposit account, stored-value account, digital wallet, payment instrument, or other monetary account. Credits are an internal service accounting mechanism that measures the amount of eligible Pinscope review services you have prepaid and are licensed to use. Any credit balance or similar amount displayed in the Service reflects only our record of remaining prepaid eligibility for future eligible review charges and does not represent money held on your behalf. Credits are non-transferable, may not be sold, assigned, gifted, or sublicensed, and may be used only by the workspace or account to which they are issued. If credits are issued to an organization or workspace, they belong to that workspace and may be consumed by authorized users acting within that workspace.
|
Credits may be used only for eligible Periscope review charges and may not be used for any other product or service unless Faradworks expressly states otherwise in the Service. Credits are not legal tender, are not currency, are not redeemable for cash, are not refundable except as required by law or expressly stated by Faradworks, do not constitute or confer any personal property right, and do not constitute a bank account, deposit account, stored-value account, digital wallet, payment instrument, or other monetary account. Credits are an internal service accounting mechanism that measures the amount of eligible Periscope review services you have prepaid and are licensed to use. Any credit balance or similar amount displayed in the Service reflects only our record of remaining prepaid eligibility for future eligible review charges and does not represent money held on your behalf. Credits are non-transferable, may not be sold, assigned, gifted, or sublicensed, and may be used only by the workspace or account to which they are issued. If credits are issued to an organization or workspace, they belong to that workspace and may be consumed by authorized users acting within that workspace.
|
||||||
|
|
||||||
### 11.4 Credit purchases and application to charges.
|
### 11.4 Credit purchases and application to charges.
|
||||||
|
|
||||||
Your order for Usage Credits constitutes an offer to purchase those Usage Credits. Faradworks may accept or reject any purchase request in its discretion. Credits are issued when Faradworks confirms the purchase or otherwise makes the credits available in your account or workspace. Credits are applied to eligible Pinscope review charges in the manner described in the Service. Faradworks may reserve, deduct, reverse, release, or adjust credits to reflect quoted charges, completed usage, failed runs, duplicate requests, fraud checks, refunds, chargebacks, or billing corrections. Credit pricing, minimum purchase amounts, maximum purchase amounts, and applicable taxes will be shown in the Service or at checkout. Fees are exclusive of taxes unless stated otherwise.
|
Your order for Usage Credits constitutes an offer to purchase those Usage Credits. Faradworks may accept or reject any purchase request in its discretion. Credits are issued when Faradworks confirms the purchase or otherwise makes the credits available in your account or workspace. Credits are applied to eligible Periscope review charges in the manner described in the Service. Faradworks may reserve, deduct, reverse, release, or adjust credits to reflect quoted charges, completed usage, failed runs, duplicate requests, fraud checks, refunds, chargebacks, or billing corrections. Credit pricing, minimum purchase amounts, maximum purchase amounts, and applicable taxes will be shown in the Service or at checkout. Fees are exclusive of taxes unless stated otherwise.
|
||||||
|
|
||||||
### 11.5 Credit expiration, forfeiture, and promotional credits.
|
### 11.5 Credit expiration, forfeiture, and promotional credits.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Pinscope",
|
"name": "Periscope",
|
||||||
"short_name": "Pinscope",
|
"short_name": "Periscope",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/favicon_io/android-chrome-192x192.png",
|
"src": "/favicon_io/android-chrome-192x192.png",
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ function ProjectsPanel() {
|
|||||||
// Stash the source project id and hand off to the dashboard, which
|
// Stash the source project id and hand off to the dashboard, which
|
||||||
// mounts the create-project dialog. The dialog fetches the full
|
// mounts the create-project dialog. The dialog fetches the full
|
||||||
// Project on its end so we don't have to pass the entire object here.
|
// Project on its end so we don't have to pass the entire object here.
|
||||||
window.sessionStorage.setItem("pinscopex:cloneAsNewProjectId", projectId);
|
window.sessionStorage.setItem("periscopex:cloneAsNewProjectId", projectId);
|
||||||
router.push("/dashboard");
|
router.push("/dashboard");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { useCredits } from "@/components/billing/credits-context";
|
|||||||
import { OnboardingSurvey } from "@/components/dashboard/onboarding-survey";
|
import { OnboardingSurvey } from "@/components/dashboard/onboarding-survey";
|
||||||
|
|
||||||
type ViewMode = "cards" | "table";
|
type ViewMode = "cards" | "table";
|
||||||
const VIEW_STORAGE_KEY = "pinscopex:dashboard:view";
|
const VIEW_STORAGE_KEY = "periscopex:dashboard:view";
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
return (
|
return (
|
||||||
@@ -79,10 +79,10 @@ function DashboardContent() {
|
|||||||
// Admin handoff: "Rerun as new project" stashes a project ID here.
|
// Admin handoff: "Rerun as new project" stashes a project ID here.
|
||||||
const cloneId =
|
const cloneId =
|
||||||
typeof window !== "undefined"
|
typeof window !== "undefined"
|
||||||
? window.sessionStorage.getItem("pinscopex:cloneAsNewProjectId")
|
? window.sessionStorage.getItem("periscopex:cloneAsNewProjectId")
|
||||||
: null;
|
: null;
|
||||||
if (cloneId) {
|
if (cloneId) {
|
||||||
window.sessionStorage.removeItem("pinscopex:cloneAsNewProjectId");
|
window.sessionStorage.removeItem("periscopex:cloneAsNewProjectId");
|
||||||
fetchProject(cloneId)
|
fetchProject(cloneId)
|
||||||
.then(setCloneAsNewProject)
|
.then(setCloneAsNewProject)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export default function FeedbackPage() {
|
|||||||
)}
|
)}
|
||||||
{expanded === t.ticket_id && t.admin_notes && (
|
{expanded === t.ticket_id && t.admin_notes && (
|
||||||
<div className="mt-3 rounded border border-emerald-500/20 bg-emerald-500/5 p-3">
|
<div className="mt-3 rounded border border-emerald-500/20 bg-emerald-500/5 p-3">
|
||||||
<p className="text-xs text-emerald-600 dark:text-emerald-400 mb-1">Pinscope Team:</p>
|
<p className="text-xs text-emerald-600 dark:text-emerald-400 mb-1">Periscope Team:</p>
|
||||||
<p className="text-sm text-muted-foreground">{t.admin_notes}</p>
|
<p className="text-sm text-muted-foreground">{t.admin_notes}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -46,6 +46,13 @@ import { ImpedancePanel } from "@/components/project/impedance-panel";
|
|||||||
import { TopologyPanel } from "@/components/project/topology-panel";
|
import { TopologyPanel } from "@/components/project/topology-panel";
|
||||||
import { PcbUploadButton } from "@/components/project/pcb-upload";
|
import { PcbUploadButton } from "@/components/project/pcb-upload";
|
||||||
import { PdfViewerSheet } from "@/components/pdf/pdf-viewer-sheet";
|
import { PdfViewerSheet } from "@/components/pdf/pdf-viewer-sheet";
|
||||||
|
import {
|
||||||
|
deratingOverridesKey,
|
||||||
|
deratingSettingsKey,
|
||||||
|
legacyDeratingOverridesKey,
|
||||||
|
legacyDeratingSettingsKey,
|
||||||
|
migrateLocalKey,
|
||||||
|
} from "@/lib/storage-keys";
|
||||||
|
|
||||||
export default function ProjectDetailPage({
|
export default function ProjectDetailPage({
|
||||||
params,
|
params,
|
||||||
@@ -61,7 +68,10 @@ export default function ProjectDetailPage({
|
|||||||
const [deratingSettings, setDeratingSettings] = useState<DeratingSettings>(() => {
|
const [deratingSettings, setDeratingSettings] = useState<DeratingSettings>(() => {
|
||||||
if (typeof window === "undefined") return { ceramic: 50, tantalum: 50, electrolytic: 50 };
|
if (typeof window === "undefined") return { ceramic: 50, tantalum: 50, electrolytic: 50 };
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem(`pinscopex:derating-settings:${id}`);
|
const stored = migrateLocalKey(
|
||||||
|
deratingSettingsKey(id),
|
||||||
|
legacyDeratingSettingsKey(id),
|
||||||
|
);
|
||||||
return stored ? JSON.parse(stored) : { ceramic: 50, tantalum: 50, electrolytic: 50 };
|
return stored ? JSON.parse(stored) : { ceramic: 50, tantalum: 50, electrolytic: 50 };
|
||||||
} catch {
|
} catch {
|
||||||
return { ceramic: 50, tantalum: 50, electrolytic: 50 };
|
return { ceramic: 50, tantalum: 50, electrolytic: 50 };
|
||||||
@@ -70,7 +80,10 @@ export default function ProjectDetailPage({
|
|||||||
const [manualVoltages, setManualVoltages] = useState<Record<string, number>>(() => {
|
const [manualVoltages, setManualVoltages] = useState<Record<string, number>>(() => {
|
||||||
if (typeof window === "undefined") return {};
|
if (typeof window === "undefined") return {};
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem(`pinscopex:derating-overrides:${id}`);
|
const stored = migrateLocalKey(
|
||||||
|
deratingOverridesKey(id),
|
||||||
|
legacyDeratingOverridesKey(id),
|
||||||
|
);
|
||||||
return stored ? JSON.parse(stored) : {};
|
return stored ? JSON.parse(stored) : {};
|
||||||
} catch {
|
} catch {
|
||||||
return {};
|
return {};
|
||||||
@@ -83,12 +96,12 @@ export default function ProjectDetailPage({
|
|||||||
|
|
||||||
// Persist derating settings to localStorage
|
// Persist derating settings to localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem(`pinscopex:derating-settings:${id}`, JSON.stringify(deratingSettings));
|
localStorage.setItem(deratingSettingsKey(id), JSON.stringify(deratingSettings));
|
||||||
}, [deratingSettings, id]);
|
}, [deratingSettings, id]);
|
||||||
|
|
||||||
// Persist manual voltage overrides to localStorage
|
// Persist manual voltage overrides to localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem(`pinscopex:derating-overrides:${id}`, JSON.stringify(manualVoltages));
|
localStorage.setItem(deratingOverridesKey(id), JSON.stringify(manualVoltages));
|
||||||
}, [manualVoltages, id]);
|
}, [manualVoltages, id]);
|
||||||
|
|
||||||
const reload = useCallback(() => {
|
const reload = useCallback(() => {
|
||||||
@@ -458,7 +471,7 @@ export default function ProjectDetailPage({
|
|||||||
</Card>
|
</Card>
|
||||||
<CollaboratorsSection projectId={id} />
|
<CollaboratorsSection projectId={id} />
|
||||||
<SkippedComponentsSection skipped={project.skippedComponents} />
|
<SkippedComponentsSection skipped={project.skippedComponents} />
|
||||||
<ReportVersionSection pinscopeVersion={project.pinscopeVersion} />
|
<ReportVersionSection periscopeVersion={project.periscopeVersion} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<PdfViewerSheet
|
<PdfViewerSheet
|
||||||
@@ -1268,11 +1281,11 @@ function SkippedComponentsSection({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ReportVersionSection({
|
function ReportVersionSection({
|
||||||
pinscopeVersion,
|
periscopeVersion,
|
||||||
}: {
|
}: {
|
||||||
pinscopeVersion?: string | null;
|
periscopeVersion?: string | null;
|
||||||
}) {
|
}) {
|
||||||
if (!pinscopeVersion) return null;
|
if (!periscopeVersion) return null;
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -1281,9 +1294,9 @@ function ReportVersionSection({
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Generated with Pinscope
|
Generated with Periscope
|
||||||
</p>
|
</p>
|
||||||
<span className="font-mono text-sm">v{pinscopeVersion}</span>
|
<span className="font-mono text-sm">v{periscopeVersion}</span>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -1299,7 +1312,7 @@ function PipelineErrorBanner({
|
|||||||
}) {
|
}) {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const detail = message ?? "Unknown error — no details were recorded.";
|
const detail = message ?? "Unknown error — no details were recorded.";
|
||||||
const shareText = `PinscopeX project ${projectId} failed: ${detail}`;
|
const shareText = `PeriscopeX project ${projectId} failed: ${detail}`;
|
||||||
|
|
||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { pageMetadata } from "@/lib/site";
|
|||||||
export const metadata = pageMetadata({
|
export const metadata = pageMetadata({
|
||||||
title: "Changelog",
|
title: "Changelog",
|
||||||
description:
|
description:
|
||||||
"Recent updates and improvements to Pinscope — new EDA tool support, review accuracy improvements, and platform changes.",
|
"Recent updates and improvements to Periscope — new EDA tool support, review accuracy improvements, and platform changes.",
|
||||||
path: "/changelog",
|
path: "/changelog",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function Nav() {
|
|||||||
<div className="mx-auto flex h-14 max-w-6xl items-center justify-between px-6">
|
<div className="mx-auto flex h-14 max-w-6xl items-center justify-between px-6">
|
||||||
<Link href="/" className="flex items-center gap-2">
|
<Link href="/" className="flex items-center gap-2">
|
||||||
<Cpu className="h-5 w-5 text-blue-500" />
|
<Cpu className="h-5 w-5 text-blue-500" />
|
||||||
<span className="text-sm font-semibold tracking-tight">Pinscope</span>
|
<span className="text-sm font-semibold tracking-tight">Periscope</span>
|
||||||
</Link>
|
</Link>
|
||||||
<nav className="hidden sm:flex items-center gap-6 text-sm text-muted-foreground">
|
<nav className="hidden sm:flex items-center gap-6 text-sm text-muted-foreground">
|
||||||
<Link href="/#features" className="hover:text-foreground transition-colors">
|
<Link href="/#features" className="hover:text-foreground transition-colors">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { pageMetadata } from "@/lib/site";
|
|||||||
export const metadata = pageMetadata({
|
export const metadata = pageMetadata({
|
||||||
title: "Contact",
|
title: "Contact",
|
||||||
description:
|
description:
|
||||||
"Talk to the Pinscope team — questions, account help, or enterprise deployment.",
|
"Talk to the Periscope team — questions, account help, or enterprise deployment.",
|
||||||
path: "/contact",
|
path: "/contact",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ export default function ContactPage() {
|
|||||||
Get in touch
|
Get in touch
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-4 text-muted-foreground max-w-lg leading-relaxed animate-fade-up [animation-delay:100ms]">
|
<p className="mt-4 text-muted-foreground max-w-lg leading-relaxed animate-fade-up [animation-delay:100ms]">
|
||||||
Have a question about Pinscope, need help with your account, or want to
|
Have a question about Periscope, need help with your account, or want to
|
||||||
discuss enterprise deployment? We’d love to hear from you.
|
discuss enterprise deployment? We’d love to hear from you.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
@@ -54,7 +54,7 @@ export default function ContactPage() {
|
|||||||
<div className="mx-auto max-w-6xl px-6 py-8 flex items-center justify-between text-xs text-muted-foreground">
|
<div className="mx-auto max-w-6xl px-6 py-8 flex items-center justify-between text-xs text-muted-foreground">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Cpu className="h-4 w-4 text-blue-500" />
|
<Cpu className="h-4 w-4 text-blue-500" />
|
||||||
<span>Pinscope</span>
|
<span>Periscope</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { pageMetadata } from "@/lib/site";
|
|||||||
export const metadata = pageMetadata({
|
export const metadata = pageMetadata({
|
||||||
title: "File Upload Guide",
|
title: "File Upload Guide",
|
||||||
description:
|
description:
|
||||||
"Step-by-step export instructions for KiCad, Altium, OrCAD, Cadence Allegro, Siemens Xpedition, EasyEDA, and Autodesk EAGLE — netlists, BOMs, and datasheets ready for Pinscope.",
|
"Step-by-step export instructions for KiCad, Altium, OrCAD, Cadence Allegro, Siemens Xpedition, EasyEDA, and Autodesk EAGLE — netlists, BOMs, and datasheets ready for Periscope.",
|
||||||
path: "/file-guide",
|
path: "/file-guide",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export default function LandingPage() {
|
|||||||
<Link href="/" className="flex items-center gap-2">
|
<Link href="/" className="flex items-center gap-2">
|
||||||
<Cpu className="h-5 w-5 text-blue-500" />
|
<Cpu className="h-5 w-5 text-blue-500" />
|
||||||
<span className="text-sm font-semibold tracking-tight">
|
<span className="text-sm font-semibold tracking-tight">
|
||||||
Pinscope
|
Periscope
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
<nav className="hidden sm:flex items-center gap-6 text-sm text-muted-foreground">
|
<nav className="hidden sm:flex items-center gap-6 text-sm text-muted-foreground">
|
||||||
@@ -169,7 +169,7 @@ export default function LandingPage() {
|
|||||||
<br className="hidden lg:block" /> first time
|
<br className="hidden lg:block" /> first time
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-6 text-lg sm:text-xl text-muted-foreground max-w-xl leading-relaxed animate-fade-up [animation-delay:100ms]">
|
<p className="mt-6 text-lg sm:text-xl text-muted-foreground max-w-xl leading-relaxed animate-fade-up [animation-delay:100ms]">
|
||||||
Pinscope reviews your schematic against every datasheet and
|
Periscope reviews your schematic against every datasheet and
|
||||||
catches the errors that would otherwise surface at bring-up.
|
catches the errors that would otherwise surface at bring-up.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-8 flex flex-col items-start gap-4 animate-fade-up [animation-delay:200ms]">
|
<div className="mt-8 flex flex-col items-start gap-4 animate-fade-up [animation-delay:200ms]">
|
||||||
@@ -191,7 +191,7 @@ export default function LandingPage() {
|
|||||||
<div className="rounded-xl border border-border overflow-hidden bg-card/40">
|
<div className="rounded-xl border border-border overflow-hidden bg-card/40">
|
||||||
<Image
|
<Image
|
||||||
src="/report.png"
|
src="/report.png"
|
||||||
alt="Pinscope validation report"
|
alt="Periscope validation report"
|
||||||
width={2400}
|
width={2400}
|
||||||
height={1500}
|
height={1500}
|
||||||
className="w-full h-auto"
|
className="w-full h-auto"
|
||||||
@@ -328,7 +328,7 @@ export default function LandingPage() {
|
|||||||
Your designs stay yours
|
Your designs stay yours
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-3 text-muted-foreground text-center max-w-lg mx-auto">
|
<p className="mt-3 text-muted-foreground text-center max-w-lg mx-auto">
|
||||||
Hardware IP is sensitive. Pinscope is built to keep it that way.
|
Hardware IP is sensitive. Periscope is built to keep it that way.
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mt-12">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mt-12">
|
||||||
{[
|
{[
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { pageMetadata } from "@/lib/site";
|
|||||||
export const metadata = pageMetadata({
|
export const metadata = pageMetadata({
|
||||||
title: "Privacy Policy",
|
title: "Privacy Policy",
|
||||||
description:
|
description:
|
||||||
"How Pinscope collects, stores, and protects the schematics, datasheets, and BOMs you upload.",
|
"How Periscope collects, stores, and protects the schematics, datasheets, and BOMs you upload.",
|
||||||
path: "/privacy",
|
path: "/privacy",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export default function SignInPage() {
|
|||||||
<div className="mx-auto flex min-h-[70vh] max-w-md flex-col justify-center px-4 py-16">
|
<div className="mx-auto flex min-h-[70vh] max-w-md flex-col justify-center px-4 py-16">
|
||||||
<h1 className="font-display text-3xl tracking-tight">Sign in</h1>
|
<h1 className="font-display text-3xl tracking-tight">Sign in</h1>
|
||||||
<p className="mt-2 text-sm text-muted-foreground">
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
Pinscope account for this server. Invite collaborators from a project once they have an account.
|
Periscope account for this server. Invite collaborators from a project once they have an account.
|
||||||
</p>
|
</p>
|
||||||
<form onSubmit={onSubmit} className="mt-8 flex flex-col gap-4">
|
<form onSubmit={onSubmit} className="mt-8 flex flex-col gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user