diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7b6857c..bc0f728 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,20 +14,20 @@ jobs:
with:
python-version: "3.12"
cache: pip
- - run: pip install -r backend/requirements.txt pytest pytest-asyncio
+ - run: pip install -r periscope/dependency/backend/requirements.txt pytest pytest-asyncio
- run: pytest tests/ -q
frontend:
runs-on: ubuntu-latest
- defaults:
- run:
- working-directory: frontend
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- cache-dependency-path: frontend/package-lock.json
+ cache-dependency-path: periscope/dependency/frontend/package-lock.json
+ - run: chmod +x scripts/materialize-frontend.sh && scripts/materialize-frontend.sh
- run: npm ci
+ working-directory: .merge/frontend
- run: npm run build
+ working-directory: .merge/frontend
diff --git a/.gitignore b/.gitignore
index d1163a2..17f8913 100644
--- a/.gitignore
+++ b/.gitignore
@@ -49,10 +49,10 @@ skills-lock.json
data/
backend/data/
-# Frontend
-frontend/node_modules/
-frontend/.next/
-frontend/out/
+.merge/
+periscope/dependency/frontend/node_modules/
+periscope/dependency/frontend/.next/
+periscope/src/frontend/.next/
.next/
# retrospective
diff --git a/CLAUDE.md b/CLAUDE.md
index 7015a21..eac43a5 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,149 +1,15 @@
# Periscope — Agentic Schematic Validation
-Periscope validates hardware schematics against component datasheets. It extracts constraints from PDFs, parses netlists and BOMs into a queryable graph, and runs an agentic validation loop to flag design violations.
-
-> **Open-core note.** This is the open-source core. A small set of files are
-> "gateway-owned seams" — pass-through stubs here (`frontend/src/proxy.ts`,
-> `use-optional-auth.ts`, `clerk-theme-provider.tsx`,
-> `components/billing/*`, `sidebar-auth.tsx`, `pricing-section.tsx`,
-> `analytics/*`, `lib/csp-hosts.ts`) that the hosted-cloud repo replaces
-> with auth/billing implementations. Keep their export signatures stable,
-> and never import auth/billing SDKs anywhere else in the frontend. On the
-> backend, everything reaches billing only through
-> `backend/services/billing_hook.py:get_billing()` (a no-op here).
-
-## System Overview
-
-Three layers:
-
-| Layer | Location | Purpose |
-|-------|----------|---------|
-| **Core library** | `backend/periscopex/` | Models, parsers, graph builder, agentic validator, passive resolver, taxonomy, BOM summary, derating |
-| **Backend** | `backend/` | FastAPI app — async pipeline orchestration, SSE progress, project/file storage |
-| **Frontend** | `frontend/` | Next.js 16 app — project dashboard, pipeline progress, report viewer, derating, admin dashboard |
-
-Plus `skills/` — extraction prompts (pintable, patterns, specs) inlined locally for DeepSeek. Do not upload to Anthropic Console.
-
-The pipeline stages: Parse BOM → Extract IC Pintables → Extract Simple Components → Extract Passives → DigiKey Auto-Resolve + Value Fallback → Build Graph → Direct Datasheet Review. Pipeline runs can be cancelled mid-execution via `POST /api/pipeline/{id}/cancel`.
-
-## Example Project
-
-`simple_project/` is the reference design for development and testing:
-
-- **MCU**: TI MSPM0G3507SPTR (U3) — 48-pin LQFP
-- **USB-UART Bridge**: CH340E (U2)
-- **LDO Regulator**: SPX3819M5-L-3-3 (U1) — 5V to 3.3V
-- **ESD Protection**: USBLC6-2SC6 (D1)
-- **Crystal**: 8 MHz (X1) with 18pF load caps (C9, C10)
-
-Files: `.asc` (PADS-PCB netlist; `.edn` EDIF 2.0.0 also accepted), `.csv`/`.xlsx` (BOM), `design_graph.json` (committed reference fixture used by tests).
-
-## Architecture Principles
-
-- **Modular extractors** — Domain-specific extraction per component type, unified constraint schema
-- **Netlist as graph** — Queryable bipartite graph (components + nets) with traversal helpers
-- **LLM API for PDF extraction** — Forced tool calls for structured output (pintable, passive patterns, specs). Default provider is DeepSeek.
-- **Prompt caching** — Anthropic stamps `cache_control`; Gemini uses CachedContent; DeepSeek uses automatic prefix cache (cache-hit tokens in usage).
-- **Local extraction skills** — `skills/*/SKILL.md` is inlined and `validate.py` runs in-process. Never call `scripts/upload_skills.py` (Anthropic Console).
-- **Direct datasheet review** — The model reads the IC datasheet plus circuit neighborhood, compares to the reference application circuit, and flags issues via graph query tools (`find_connected_components`, `get_net_for_pin`, `get_pintable`). DeepSeek converts PDFs to text (and page images on the vision model).
-- **Datasheet page trimming** — Large PDFs are keyword-trimmed to relevant pages before sending to Claude, reducing token cost (`pypdf`)
-- **DigiKey fallback (exact MPN only)** — When pattern-based and direct extraction fail, DigiKey API fetches product parameters for auto-resolve. DigiKey matches only on exact MPN; fuzzy hits are rejected to avoid polluting the shared library with wrong-dielectric / wrong-voltage parts.
-- **Value-string fallback** — When DigiKey misses an R/C/L/FB passive, a value-string resolver maps the BOM `Value` string to typed passive specs. Value-derived specs are persisted per-project only — never to the shared library.
-- **Per-IC review error isolation** — Direct datasheet review runs each IC independently; one malformed payload or bad response cannot kill the whole run. Failed ICs surface as skipped components with the error.
-- **Cross-IC excerpt budget (per-neighbor)** — To verify an interface finding the reviewer can pull a *connected* IC's datasheet pages (`get_datasheet_excerpt`). The budget is a global per-review page ceiling **plus a per-neighbor sub-budget**, so verifying one interface is never starved by pages already spent on other neighbors.
-- **Finding normalization is downgrade-only** — A post-review per-IC normalize pass (`services/normalize_findings.py`) drops self-cancelling findings, merges same-root-cause findings, and re-grades severity — but only ever *downward*. A deterministic clamp caps each finding at the reviewer's calibrated severity (and any `Unverified:` finding at WARNING, preserving the prefix).
-- **Cross-IC finding dedup** — After all per-IC reviews complete, a single pass (`services/dedupe_findings.py`) collapses one physical interface defect reported from both endpoints into a single finding. Gated by `cross_ic_dedup_enabled`; fail-soft.
-- **Capacitor voltage derating** — Deterministic derating table computed from graph (ceramic/tantalum/electrolytic percentages, pass/fail per capacitor)
-- **Deterministic checks over heuristics** — Exact checks where possible
-- **Zero coupling between layers** — Backend calls periscopex functions with paths; frontend talks to backend via REST + SSE
-- **Library deduplication** — Shared library (`library/extracted/`, `library/patterns/`, `library/models/`, `library/passives/`, `library/datasheets/`) caches extractions across projects
-- **Content-addressed datasheets** — `library/datasheets/blobs/{md5}.pdf` stores unique PDFs once; `library/datasheets/refs/{safe_mpn}.json` maps MPNs to blobs (dedupe + multi-MPN sharing)
-- **Taxonomy-driven extraction** — Living component taxonomy (`taxonomy/`) with per-subtype classification and specs schemas
-- **Per-stage model config** — Each pipeline stage can use a different Claude model (e.g., Sonnet for review, Haiku for auto-resolve)
-- **API call logging** — Every Claude API call is logged with token counts, cost, and timing per pipeline run
-- **Report versioning** — Each project run is stamped with the current app version on the first `/start` transition (`ProjectMeta.periscope_version`). The version comes from `frontend/content/changelog.md`'s latest `##` heading — single source of truth — read at backend startup via `backend/_version.py`.
-
-## Datasheet Extraction
-
-Extracted data lives in `library/extracted/` (shared) or per-project under the storage backend. One JSON per MPN, schema in `backend/periscopex/models.py`.
-
-Per-MPN IC extraction captures:
-1. **Pintable** — Pin number + name (required), description + alt functions (optional)
-2. **Package info** — Base family, package, pin count, description
-3. **Component subtype** — Dotted taxonomy path (e.g., `ic.mcu`, `ic.power.ldo`)
-
-For discrete/simple components:
-4. **Specs** — Component specs (value, tolerance, package, voltage rating, etc.); parameters are filtered against taxonomy specs schemas
-
-Extraction inlines **local skills** (`skills/*/SKILL.md` + `validate.py`) against DeepSeek. Do not use Anthropic Console Skills.
-
-## Claude Console Skills
-
-```
-skills/
-├── extract-pintable/ # Pin table + package info + taxonomy
-│ ├── SKILL.md # System prompt (YAML frontmatter + markdown)
-│ ├── schema.json # Tool output schema
-│ └── validate.py # Validation script
-├── extract-pattern/ # Passive MPN pattern
-└── extract-specs/ # Component specs (discrete, connectors, crystals, etc.)
-```
-
-## Taxonomy
-
-Living component taxonomy in `taxonomy/` — one JSON file per top-level type (ic, passive, connector, crystal, discrete, fuse, switch, test_point, transformer). Each subtype entry includes `description` and `example_mpn`.
-
-Key taxonomy features:
-- **Ref prefix mapping** — `U→ic`, `R/C/L→passive`, `D/Q→discrete`, `X→crystal`, etc.
-- **Dotted subtype paths** — e.g., `ic.mcu`, `passive.capacitor.ceramic`, `ic.protection.esd`
-- **Dynamic growth** — `add_subtype()` adds new entries; concurrent-safe JSON writes
-- **Specs schema auto-generation** — Type-level and subtype-level parameter specs schemas are auto-generated via Claude when a taxonomy entry has none; extraction discards parameters not in the schema (`extra_specs` field)
-
-## Scripts
-
-- `scripts/upload_skills.py` — leftover Claude Console uploader. **Do not run.** Skills are local + DeepSeek only.
-- `scripts/migrate_datasheets_to_library.py` — One-time migration: copy per-project datasheets to `library/datasheets/` (dry-run by default, `--apply` to execute)
-- `scripts/migrate_datasheets_to_blobs.py` — Migrate named-PDF datasheets into the content-addressed blobs/refs layout (dry-run by default, `--apply` to execute)
-- `scripts/dedup_library_datasheets.py` — Remove redundant per-MPN datasheet PDFs when a passive pattern already has a `datasheet_key` (dry-run by default, `--apply` to execute)
-- `scripts/gc_orphan_blobs.py` — Garbage-collect `library/datasheets/blobs/*.pdf` not referenced by any ref file
-- `scripts/clear_rules_from_extractions.py` — Strip deprecated `rules`/`absolute_maximum_ratings` from existing library extractions
-
-## Tech Stack
-
-- **Core**: Python 3.12+, Pydantic 2.x, OpenAI SDK (DeepSeek), Anthropic SDK (optional), google-genai (optional), openpyxl, pypdf, PyMuPDF
-- **Backend**: FastAPI, uvicorn, sse-starlette, pydantic-settings
-- **Frontend**: Next.js 16 (App Router, Turbopack), React 19, Tailwind CSS v4, shadcn/ui (Base UI), react-pdf
-- **AI**: DeepSeek Chat Completions (OpenAI-compatible) with forced tool calls for extraction and agentic review. Do not route stages to Anthropic.
-- **Model**: `deepseek-flash` for extraction, review, auto-resolve, and normalize (per-stage overrides via `.env`)
-- **Skills**: Local SKILL.md + validate.py on DeepSeek
-- **External APIs**: DigiKey API v4 (OAuth2) — optional datasheet auto-fetch and parameter-based auto-resolve (`DIGIKEY_CLIENT_ID`, `DIGIKEY_CLIENT_SECRET`)
-
-## Extracted Model Versioning
-
-All `ComponentConstraints` extracted JSON files carry a `model_version` semver field:
-
-- **Initial value** — set from `default_model_version` in `backend/skills_manifest.json` (starts at `1.0.0`)
-- **Minor bump** — increment `default_model_version` in `skills_manifest.json` when extraction prompts change (do **not** run `upload_skills.py`).
-
-**Rule**: When committing changes under `skills/`, bump `default_model_version` locally. Never call Anthropic.
-
-## Development Guidelines
-
-- Write tests against `simple_project/` — it's the ground truth
-- Netlist parser and BOM parser are pure functions with no side effects
-- All data structures use Pydantic models in `backend/periscopex/models.py`
-- Frontend types in `frontend/src/lib/types.ts` must stay in sync with `backend/periscopex/models.py`
-- Extraction prompts live in `skills/` (SKILL.md + schema.json + validate.py) and run locally against DeepSeek
-- **Never swallow exceptions silently** — prefer logging or re-raising over bare `except: continue`. Silent failures hide real bugs.
+Open-core checkout. **Native code** lives in `periscope/src`. **Inherited PinScope** lives in `periscope/dependency` (in-tree AGPL dependency — do not delete). Root `LICENSE` is AGPL-3.0.
## Running
```bash
-# Backend (copy backend/.env.example to .env at repo root first)
-python3 -m uvicorn backend.main:app --reload # localhost:8000
+# Backend (copy periscope/dependency/backend/.env.example to .env at repo root first)
+python3 -m uvicorn backend.main:app --reload # localhost:8000
# Frontend
-cd frontend && npm run dev # localhost:3000
+cd periscope/dependency/frontend && npm run dev # localhost:3000
```
-Local mode needs no cloud services and no auth — projects are stored in `data/` and you are `user_id="local"` with admin access.
+See `periscope/README.md` and root `README.md`.
diff --git a/README.md b/README.md
index 2a3f1f6..1e7a73b 100644
--- a/README.md
+++ b/README.md
@@ -23,12 +23,21 @@ Default routing:
| Per-IC datasheet review | `deepseek-flash` |
| Auto-resolve / normalize | `deepseek-flash` |
-Override with `PROVIDER_*` and `MODEL_*_DEEPSEEK` in `backend/.env`. See `backend/.env.example`.
+Override with `PROVIDER_*` and `MODEL_*_DEEPSEEK` in `.env`. See `periscope/dependency/backend/.env.example`.
+
+## Layout
+
+- `periscope/src/` — native Periscope
+- `periscope/dependency/` — inherited PinScope (AGPL in-tree dependency; do not delete)
+- `LICENSE` — GNU AGPL v3 (visible at repo root)
+- `vendor/impedancefinder/` — third party (license UNKNOWN)
+
+See `periscope/README.md` and `periscope/src/docs/development/PINSCOPE_INDEPENDENCE_PLAN.md`.
## How it works
-
+
1. **Parse** the BOM (CSV/XLSX) and netlist (PADS-PCB `.asc` or EDIF 2.0.0 `.edn`) into a queryable bipartite graph of components and nets.
@@ -38,29 +47,29 @@ Override with `PROVIDER_*` and `MODEL_*_DEEPSEEK` in `backend/.env`. See `backen
## Try it on the bundled design
-`simple_project/` is a small MSPM0G3507 board with a CH340E USB-UART bridge and an SPX3819 LDO.
+`periscope/dependency/simple_project/` is a small MSPM0G3507 board with a CH340E USB-UART bridge and an SPX3819 LDO.
You need Python 3.12+, Node 20+, and a [DeepSeek API key](https://platform.deepseek.com/):
```bash
python3 -m venv .venv
source .venv/bin/activate
-pip install -r backend/requirements.txt
-cp backend/.env.example backend/.env # set DEEPSEEK_API_KEY
+pip install -r periscope/dependency/backend/requirements.txt
+cp periscope/dependency/backend/.env.example .env # set DEEPSEEK_API_KEY
python3 -m uvicorn backend.main:app --reload --host 127.0.0.1 --port 18741
# in another terminal
-cd frontend && npm install
+cd periscope/dependency/frontend && npm install
NEXT_PUBLIC_API_URL=http://127.0.0.1:18741 npm run dev -- --port 18742 --hostname 127.0.0.1
```
-Open the frontend URL, create a project, and feed it the netlist and BOM from `simple_project/`. Datasheets are fetched automatically (LCSC, TI, optional DigiKey); you can still drop in PDFs by hand. Fetched PDFs and extracted pin tables land in the **Library** (sidebar) and are reused on later projects. Everything runs locally against your own DeepSeek key; projects and the extraction library live in `data/`. Skills are local `skills/*/SKILL.md` — do not run `scripts/upload_skills.py`.
+Open the frontend URL, create a project, and feed it the netlist and BOM from `periscope/dependency/simple_project/`. Datasheets are fetched automatically (LCSC, TI, optional DigiKey); you can still drop in PDFs by hand. Fetched PDFs and extracted pin tables land in the **Library** (sidebar) and are reused on later projects. Everything runs locally against your own DeepSeek key; projects and the extraction library live in `data/`. Skills are local `periscope/dependency/skills/*/SKILL.md` — do not run `periscope/dependency/scripts/upload_skills.py`.
## Docker
```bash
-cp backend/.env.example .env # set DEEPSEEK_API_KEY
+cp periscope/dependency/backend/.env.example .env # set DEEPSEEK_API_KEY
docker compose up --build
```
diff --git a/backend/Dockerfile b/backend/Dockerfile
deleted file mode 100644
index 3b379b1..0000000
--- a/backend/Dockerfile
+++ /dev/null
@@ -1,33 +0,0 @@
-FROM python:3.12-slim
-RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
-
-WORKDIR /app
-
-# Install dependencies first (layer caching).
-# Open-core: the private gateway repo adds backend/requirements-gateway.txt
-# (Stripe, etc.); a plain core checkout has no such file and skips that step.
-COPY backend/requirements*.txt /app/backend/
-RUN pip install --no-cache-dir -r /app/backend/requirements.txt \
- && if [ -f /app/backend/requirements-gateway.txt ]; then \
- pip install --no-cache-dir -r /app/backend/requirements-gateway.txt; \
- fi
-
-# Copy application code
-COPY backend/ /app/backend/
-
-# Copy runtime assets needed by the backend
-# Taxonomy: fallback for local mode; GCS mode downloads from bucket
-COPY taxonomy/ /app/taxonomy/
-
-# Extraction skills (SKILL.md + validate.py) — required for DeepSeek/Gemini
-COPY skills/ /app/skills/
-
-# Changelog: single source of truth for the user-facing Periscope version.
-COPY frontend/content/changelog.md /app/changelog.md
-
-# ImpedenceFinder closed-form engine (no OpenEMS / pcbnew).
-COPY vendor/ /app/vendor/
-
-EXPOSE 8080
-
-CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8080"]
diff --git a/backend/__init__.py b/backend/__init__.py
index e69de29..18c8f1d 100644
--- a/backend/__init__.py
+++ b/backend/__init__.py
@@ -0,0 +1,18 @@
+"""Backend package facade: native Periscope + inherited PinScope.
+
+``periscope/src/backend`` and ``periscope/dependency/backend`` are merged via
+pkgutil path extension. Do not put application modules in this directory.
+"""
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+from pkgutil import extend_path
+
+_REPO = Path(__file__).resolve().parents[1]
+for _p in (_REPO / "periscope" / "src", _REPO / "periscope" / "dependency"):
+ _s = str(_p)
+ if _p.is_dir() and _s not in sys.path:
+ sys.path.insert(0, _s)
+
+__path__ = list(extend_path(__path__, __name__))
diff --git a/backend/periscopex/__init__.py b/backend/periscopex/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/backend/routers/__init__.py b/backend/routers/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/backend/services/__init__.py b/backend/services/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/docker-compose.yml b/docker-compose.yml
index 81cc02e..20b76b1 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -10,7 +10,7 @@ services:
backend:
build:
context: .
- dockerfile: backend/Dockerfile
+ dockerfile: periscope/src/backend/Dockerfile
container_name: periscope-backend
restart: unless-stopped
@@ -25,7 +25,7 @@ services:
volumes:
- ./data:/app/data
- - ./taxonomy:/app/taxonomy
+ - ./periscope/dependency/taxonomy:/app/taxonomy
ports:
- "8080:8080"
@@ -38,8 +38,8 @@ services:
frontend:
build:
- context: ./frontend
- dockerfile: dockerfile
+ context: .
+ dockerfile: periscope/src/frontend/dockerfile
args:
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:8080}
NEXT_PUBLIC_AUTH_MODE: ${NEXT_PUBLIC_AUTH_MODE:-}
diff --git a/docs/development/PINSCOPE_FENCE.md b/docs/development/PINSCOPE_FENCE.md
deleted file mode 100644
index 853288d..0000000
--- a/docs/development/PINSCOPE_FENCE.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# Recinto PinScope (Fase A)
-
-Documented fence. **No file moves.** `LICENSE` AGPL-3.0 is unchanged. The GitHub fork `manvalan/periscope` → `Faradworks/Pinscope` is **not** detached.
-
-## Branch topology
-
-| Ref | Role |
-| --- | --- |
-| `origin/main` | Archival: upstream Pinscope + Docker/OSS (+3). Not the live product. |
-| `cursor/pcb-review-plan-44dd` | Live product (schema, PCB exam, finding engine). **PCB workers stay here.** |
-| `cursor/pinscope-fase-a-identity` | Fase A identity + compat fence only. Do not land engine/PCB rewrites on this branch. |
-
-Merging product onto `main` is a later process step, still without detaching the fork.
-
-## Inherited tree (KEEP behind the fence)
-
-Treat as in-tree PinScope, not as “native Periscope”:
-
-- `backend/periscopex/{graph,models,parsers*,validate,validation_tools,resolve_passives,derating,bom_summary,taxonomy,pin_mux_check,led_current_check,pin_function_tokens}`
-- `backend/services/pipeline.py`, `pipeline_worker.py`, `validation.py`, `extraction.py` (DIRECT)
-- `skills/`, `scripts/upload_skills.py`
-- Open-core Clerk/billing stubs
-- `simple_project/` fixtures, `docs/how-it-works.svg`
-
-Native Periscope (do not fold into the fence): `finding_engine.py`, PCB/placement/antenna jobs, DeepSeek providers, local JWT, this identity layer.
-
-## Write vs read (identifiers)
-
-| Write (native) | Read-only / migrate |
-| --- | --- |
-| `periscope_token` | `pinscope_token` |
-| `periscopex:*` localStorage | `pinscopex:*` |
-| JWT `iss=periscope-local` | also accept `pinscope-local` |
-| `periscope_version` in `project.json` | also accept `pinscope_version` |
-
-Code: `frontend/src/lib/pinscope-compat.ts`, `backend/pinscope_compat.py`.
-
-**Do not rename** live Docker networks `pinscope_pinscope` / `pinscope_periscope` in Fase A (VPS Caddy).
-
-## Identity
-
-Operator of https://periscope.michelebigi.it is **Michele Bigi**. UI, TOS, privacy, metadata, and contact must not say the operator is Faradworks, Inc. Attribution to Faradworks/Pinscope remains for AGPL lineage only.
-
-## Out of scope here (later phases)
-
-Clerk/Anthropic runtime isolation (B). Rewrite of `validate.py` / finding engine (C). UI FindingCard (D). Parsers/graph (E). Fork detach (legal, not this plan).
diff --git a/periscope/README.md b/periscope/README.md
new file mode 100644
index 0000000..af81ef9
--- /dev/null
+++ b/periscope/README.md
@@ -0,0 +1,14 @@
+# Periscope tree layout
+
+Physical split (not a rewrite). AGPL `LICENSE` stays at the git root. The GitHub fork is not detached.
+
+| Tree | Path | Role |
+| --- | --- | --- |
+| Native Periscope | `periscope/src/` | Finding engine clamp, PCB/placement/antenna, DeepSeek, local auth, KiCad plugin, deploy Dockerfiles |
+| Inherited PinScope | `periscope/dependency/` | Graph/parsers/`validate.py`, pipeline/extraction, OSS Next.js shell, skills, taxonomy, `simple_project` |
+| Third party | `vendor/impedancefinder/` | ImpedenceFinder (license UNKNOWN) |
+| Glue | `backend/__init__.py` | Merges the two `backend` packages for local imports |
+
+**Do not empty-delete `periscope/dependency/`.** Later phases replace PinScope modules incrementally in `periscope/src` (Fase C). `validate.py` stays until a native reviewer exists.
+
+Docker overlays `dependency` then `src` into `/app`. Local frontend: `cd periscope/dependency/frontend && npm run dev` (native files are symlinked from `periscope/src/frontend`).
diff --git a/periscope/dependency/CLAUDE.md b/periscope/dependency/CLAUDE.md
new file mode 100644
index 0000000..7015a21
--- /dev/null
+++ b/periscope/dependency/CLAUDE.md
@@ -0,0 +1,149 @@
+# Periscope — Agentic Schematic Validation
+
+Periscope validates hardware schematics against component datasheets. It extracts constraints from PDFs, parses netlists and BOMs into a queryable graph, and runs an agentic validation loop to flag design violations.
+
+> **Open-core note.** This is the open-source core. A small set of files are
+> "gateway-owned seams" — pass-through stubs here (`frontend/src/proxy.ts`,
+> `use-optional-auth.ts`, `clerk-theme-provider.tsx`,
+> `components/billing/*`, `sidebar-auth.tsx`, `pricing-section.tsx`,
+> `analytics/*`, `lib/csp-hosts.ts`) that the hosted-cloud repo replaces
+> with auth/billing implementations. Keep their export signatures stable,
+> and never import auth/billing SDKs anywhere else in the frontend. On the
+> backend, everything reaches billing only through
+> `backend/services/billing_hook.py:get_billing()` (a no-op here).
+
+## System Overview
+
+Three layers:
+
+| Layer | Location | Purpose |
+|-------|----------|---------|
+| **Core library** | `backend/periscopex/` | Models, parsers, graph builder, agentic validator, passive resolver, taxonomy, BOM summary, derating |
+| **Backend** | `backend/` | FastAPI app — async pipeline orchestration, SSE progress, project/file storage |
+| **Frontend** | `frontend/` | Next.js 16 app — project dashboard, pipeline progress, report viewer, derating, admin dashboard |
+
+Plus `skills/` — extraction prompts (pintable, patterns, specs) inlined locally for DeepSeek. Do not upload to Anthropic Console.
+
+The pipeline stages: Parse BOM → Extract IC Pintables → Extract Simple Components → Extract Passives → DigiKey Auto-Resolve + Value Fallback → Build Graph → Direct Datasheet Review. Pipeline runs can be cancelled mid-execution via `POST /api/pipeline/{id}/cancel`.
+
+## Example Project
+
+`simple_project/` is the reference design for development and testing:
+
+- **MCU**: TI MSPM0G3507SPTR (U3) — 48-pin LQFP
+- **USB-UART Bridge**: CH340E (U2)
+- **LDO Regulator**: SPX3819M5-L-3-3 (U1) — 5V to 3.3V
+- **ESD Protection**: USBLC6-2SC6 (D1)
+- **Crystal**: 8 MHz (X1) with 18pF load caps (C9, C10)
+
+Files: `.asc` (PADS-PCB netlist; `.edn` EDIF 2.0.0 also accepted), `.csv`/`.xlsx` (BOM), `design_graph.json` (committed reference fixture used by tests).
+
+## Architecture Principles
+
+- **Modular extractors** — Domain-specific extraction per component type, unified constraint schema
+- **Netlist as graph** — Queryable bipartite graph (components + nets) with traversal helpers
+- **LLM API for PDF extraction** — Forced tool calls for structured output (pintable, passive patterns, specs). Default provider is DeepSeek.
+- **Prompt caching** — Anthropic stamps `cache_control`; Gemini uses CachedContent; DeepSeek uses automatic prefix cache (cache-hit tokens in usage).
+- **Local extraction skills** — `skills/*/SKILL.md` is inlined and `validate.py` runs in-process. Never call `scripts/upload_skills.py` (Anthropic Console).
+- **Direct datasheet review** — The model reads the IC datasheet plus circuit neighborhood, compares to the reference application circuit, and flags issues via graph query tools (`find_connected_components`, `get_net_for_pin`, `get_pintable`). DeepSeek converts PDFs to text (and page images on the vision model).
+- **Datasheet page trimming** — Large PDFs are keyword-trimmed to relevant pages before sending to Claude, reducing token cost (`pypdf`)
+- **DigiKey fallback (exact MPN only)** — When pattern-based and direct extraction fail, DigiKey API fetches product parameters for auto-resolve. DigiKey matches only on exact MPN; fuzzy hits are rejected to avoid polluting the shared library with wrong-dielectric / wrong-voltage parts.
+- **Value-string fallback** — When DigiKey misses an R/C/L/FB passive, a value-string resolver maps the BOM `Value` string to typed passive specs. Value-derived specs are persisted per-project only — never to the shared library.
+- **Per-IC review error isolation** — Direct datasheet review runs each IC independently; one malformed payload or bad response cannot kill the whole run. Failed ICs surface as skipped components with the error.
+- **Cross-IC excerpt budget (per-neighbor)** — To verify an interface finding the reviewer can pull a *connected* IC's datasheet pages (`get_datasheet_excerpt`). The budget is a global per-review page ceiling **plus a per-neighbor sub-budget**, so verifying one interface is never starved by pages already spent on other neighbors.
+- **Finding normalization is downgrade-only** — A post-review per-IC normalize pass (`services/normalize_findings.py`) drops self-cancelling findings, merges same-root-cause findings, and re-grades severity — but only ever *downward*. A deterministic clamp caps each finding at the reviewer's calibrated severity (and any `Unverified:` finding at WARNING, preserving the prefix).
+- **Cross-IC finding dedup** — After all per-IC reviews complete, a single pass (`services/dedupe_findings.py`) collapses one physical interface defect reported from both endpoints into a single finding. Gated by `cross_ic_dedup_enabled`; fail-soft.
+- **Capacitor voltage derating** — Deterministic derating table computed from graph (ceramic/tantalum/electrolytic percentages, pass/fail per capacitor)
+- **Deterministic checks over heuristics** — Exact checks where possible
+- **Zero coupling between layers** — Backend calls periscopex functions with paths; frontend talks to backend via REST + SSE
+- **Library deduplication** — Shared library (`library/extracted/`, `library/patterns/`, `library/models/`, `library/passives/`, `library/datasheets/`) caches extractions across projects
+- **Content-addressed datasheets** — `library/datasheets/blobs/{md5}.pdf` stores unique PDFs once; `library/datasheets/refs/{safe_mpn}.json` maps MPNs to blobs (dedupe + multi-MPN sharing)
+- **Taxonomy-driven extraction** — Living component taxonomy (`taxonomy/`) with per-subtype classification and specs schemas
+- **Per-stage model config** — Each pipeline stage can use a different Claude model (e.g., Sonnet for review, Haiku for auto-resolve)
+- **API call logging** — Every Claude API call is logged with token counts, cost, and timing per pipeline run
+- **Report versioning** — Each project run is stamped with the current app version on the first `/start` transition (`ProjectMeta.periscope_version`). The version comes from `frontend/content/changelog.md`'s latest `##` heading — single source of truth — read at backend startup via `backend/_version.py`.
+
+## Datasheet Extraction
+
+Extracted data lives in `library/extracted/` (shared) or per-project under the storage backend. One JSON per MPN, schema in `backend/periscopex/models.py`.
+
+Per-MPN IC extraction captures:
+1. **Pintable** — Pin number + name (required), description + alt functions (optional)
+2. **Package info** — Base family, package, pin count, description
+3. **Component subtype** — Dotted taxonomy path (e.g., `ic.mcu`, `ic.power.ldo`)
+
+For discrete/simple components:
+4. **Specs** — Component specs (value, tolerance, package, voltage rating, etc.); parameters are filtered against taxonomy specs schemas
+
+Extraction inlines **local skills** (`skills/*/SKILL.md` + `validate.py`) against DeepSeek. Do not use Anthropic Console Skills.
+
+## Claude Console Skills
+
+```
+skills/
+├── extract-pintable/ # Pin table + package info + taxonomy
+│ ├── SKILL.md # System prompt (YAML frontmatter + markdown)
+│ ├── schema.json # Tool output schema
+│ └── validate.py # Validation script
+├── extract-pattern/ # Passive MPN pattern
+└── extract-specs/ # Component specs (discrete, connectors, crystals, etc.)
+```
+
+## Taxonomy
+
+Living component taxonomy in `taxonomy/` — one JSON file per top-level type (ic, passive, connector, crystal, discrete, fuse, switch, test_point, transformer). Each subtype entry includes `description` and `example_mpn`.
+
+Key taxonomy features:
+- **Ref prefix mapping** — `U→ic`, `R/C/L→passive`, `D/Q→discrete`, `X→crystal`, etc.
+- **Dotted subtype paths** — e.g., `ic.mcu`, `passive.capacitor.ceramic`, `ic.protection.esd`
+- **Dynamic growth** — `add_subtype()` adds new entries; concurrent-safe JSON writes
+- **Specs schema auto-generation** — Type-level and subtype-level parameter specs schemas are auto-generated via Claude when a taxonomy entry has none; extraction discards parameters not in the schema (`extra_specs` field)
+
+## Scripts
+
+- `scripts/upload_skills.py` — leftover Claude Console uploader. **Do not run.** Skills are local + DeepSeek only.
+- `scripts/migrate_datasheets_to_library.py` — One-time migration: copy per-project datasheets to `library/datasheets/` (dry-run by default, `--apply` to execute)
+- `scripts/migrate_datasheets_to_blobs.py` — Migrate named-PDF datasheets into the content-addressed blobs/refs layout (dry-run by default, `--apply` to execute)
+- `scripts/dedup_library_datasheets.py` — Remove redundant per-MPN datasheet PDFs when a passive pattern already has a `datasheet_key` (dry-run by default, `--apply` to execute)
+- `scripts/gc_orphan_blobs.py` — Garbage-collect `library/datasheets/blobs/*.pdf` not referenced by any ref file
+- `scripts/clear_rules_from_extractions.py` — Strip deprecated `rules`/`absolute_maximum_ratings` from existing library extractions
+
+## Tech Stack
+
+- **Core**: Python 3.12+, Pydantic 2.x, OpenAI SDK (DeepSeek), Anthropic SDK (optional), google-genai (optional), openpyxl, pypdf, PyMuPDF
+- **Backend**: FastAPI, uvicorn, sse-starlette, pydantic-settings
+- **Frontend**: Next.js 16 (App Router, Turbopack), React 19, Tailwind CSS v4, shadcn/ui (Base UI), react-pdf
+- **AI**: DeepSeek Chat Completions (OpenAI-compatible) with forced tool calls for extraction and agentic review. Do not route stages to Anthropic.
+- **Model**: `deepseek-flash` for extraction, review, auto-resolve, and normalize (per-stage overrides via `.env`)
+- **Skills**: Local SKILL.md + validate.py on DeepSeek
+- **External APIs**: DigiKey API v4 (OAuth2) — optional datasheet auto-fetch and parameter-based auto-resolve (`DIGIKEY_CLIENT_ID`, `DIGIKEY_CLIENT_SECRET`)
+
+## Extracted Model Versioning
+
+All `ComponentConstraints` extracted JSON files carry a `model_version` semver field:
+
+- **Initial value** — set from `default_model_version` in `backend/skills_manifest.json` (starts at `1.0.0`)
+- **Minor bump** — increment `default_model_version` in `skills_manifest.json` when extraction prompts change (do **not** run `upload_skills.py`).
+
+**Rule**: When committing changes under `skills/`, bump `default_model_version` locally. Never call Anthropic.
+
+## Development Guidelines
+
+- Write tests against `simple_project/` — it's the ground truth
+- Netlist parser and BOM parser are pure functions with no side effects
+- All data structures use Pydantic models in `backend/periscopex/models.py`
+- Frontend types in `frontend/src/lib/types.ts` must stay in sync with `backend/periscopex/models.py`
+- Extraction prompts live in `skills/` (SKILL.md + schema.json + validate.py) and run locally against DeepSeek
+- **Never swallow exceptions silently** — prefer logging or re-raising over bare `except: continue`. Silent failures hide real bugs.
+
+## Running
+
+```bash
+# Backend (copy backend/.env.example to .env at repo root first)
+python3 -m uvicorn backend.main:app --reload # localhost:8000
+
+# Frontend
+cd frontend && npm run dev # localhost:3000
+```
+
+Local mode needs no cloud services and no auth — projects are stored in `data/` and you are `user_id="local"` with admin access.
diff --git a/backend/.env.example b/periscope/dependency/backend/.env.example
similarity index 100%
rename from backend/.env.example
rename to periscope/dependency/backend/.env.example
diff --git a/backend/CLAUDE.md b/periscope/dependency/backend/CLAUDE.md
similarity index 100%
rename from backend/CLAUDE.md
rename to periscope/dependency/backend/CLAUDE.md
diff --git a/periscope/dependency/backend/__init__.py b/periscope/dependency/backend/__init__.py
new file mode 100644
index 0000000..0b54cc6
--- /dev/null
+++ b/periscope/dependency/backend/__init__.py
@@ -0,0 +1,4 @@
+"""PinScope-inherited backend package (in-tree dependency)."""
+from pkgutil import extend_path
+
+__path__ = extend_path(__path__, __name__)
diff --git a/backend/config.py b/periscope/dependency/backend/config.py
similarity index 96%
rename from backend/config.py
rename to periscope/dependency/backend/config.py
index cfd918d..36ff9e9 100644
--- a/backend/config.py
+++ b/periscope/dependency/backend/config.py
@@ -7,6 +7,11 @@ from pathlib import Path
from pydantic import Field
from pydantic_settings import BaseSettings
+from backend.repo_paths import data_dir as _data_dir
+from backend.repo_paths import env_file as _env_file
+from backend.repo_paths import skills_dir as _skills_dir
+from backend.repo_paths import taxonomy_dir as _taxonomy_dir
+
# Resolve paths relative to the project root (one level up from backend/)
_BACKEND_DIR = Path(__file__).resolve().parent
_PROJECT_ROOT = _BACKEND_DIR.parent
@@ -111,10 +116,10 @@ class Settings(BaseSettings):
# single finding. Runs once after all per-IC reviews complete.
cross_ic_dedup_enabled: bool = True
- # Paths (relative to project root, used by LocalStorageBackend)
- data_dir: Path = _PROJECT_ROOT / "data"
- taxonomy_dir: Path = _PROJECT_ROOT / "taxonomy"
- skills_dir: Path = _PROJECT_ROOT / "skills"
+ # Paths (git split: data at repo root; taxonomy/skills under periscope/dependency)
+ data_dir: Path = _data_dir()
+ taxonomy_dir: Path = _taxonomy_dir()
+ skills_dir: Path = _skills_dir()
# GCS (if set, use GCSStorageBackend; otherwise LocalStorageBackend)
gcs_bucket: str = ""
@@ -193,7 +198,7 @@ class Settings(BaseSettings):
pipeline_sweeper_stale_seconds: int = 60
model_config = {
- "env_file": str(_BACKEND_DIR / ".env"),
+ "env_file": str(_env_file()),
"env_file_encoding": "utf-8",
"extra": "ignore",
}
diff --git a/backend/main.py b/periscope/dependency/backend/main.py
similarity index 100%
rename from backend/main.py
rename to periscope/dependency/backend/main.py
diff --git a/backend/middleware/__init__.py b/periscope/dependency/backend/middleware/__init__.py
similarity index 100%
rename from backend/middleware/__init__.py
rename to periscope/dependency/backend/middleware/__init__.py
diff --git a/backend/middleware/auth.py b/periscope/dependency/backend/middleware/auth.py
similarity index 100%
rename from backend/middleware/auth.py
rename to periscope/dependency/backend/middleware/auth.py
diff --git a/periscope/dependency/backend/periscopex/__init__.py b/periscope/dependency/backend/periscopex/__init__.py
new file mode 100644
index 0000000..b36383a
--- /dev/null
+++ b/periscope/dependency/backend/periscopex/__init__.py
@@ -0,0 +1,3 @@
+from pkgutil import extend_path
+
+__path__ = extend_path(__path__, __name__)
diff --git a/backend/periscopex/bom_summary.py b/periscope/dependency/backend/periscopex/bom_summary.py
similarity index 100%
rename from backend/periscopex/bom_summary.py
rename to periscope/dependency/backend/periscopex/bom_summary.py
diff --git a/backend/periscopex/derating.py b/periscope/dependency/backend/periscopex/derating.py
similarity index 100%
rename from backend/periscopex/derating.py
rename to periscope/dependency/backend/periscopex/derating.py
diff --git a/backend/periscopex/graph.py b/periscope/dependency/backend/periscopex/graph.py
similarity index 100%
rename from backend/periscopex/graph.py
rename to periscope/dependency/backend/periscopex/graph.py
diff --git a/backend/periscopex/led_current_check.py b/periscope/dependency/backend/periscopex/led_current_check.py
similarity index 100%
rename from backend/periscopex/led_current_check.py
rename to periscope/dependency/backend/periscopex/led_current_check.py
diff --git a/backend/periscopex/models.py b/periscope/dependency/backend/periscopex/models.py
similarity index 100%
rename from backend/periscopex/models.py
rename to periscope/dependency/backend/periscopex/models.py
diff --git a/backend/periscopex/parsers.py b/periscope/dependency/backend/periscopex/parsers.py
similarity index 100%
rename from backend/periscopex/parsers.py
rename to periscope/dependency/backend/periscopex/parsers.py
diff --git a/backend/periscopex/parsers_edif.py b/periscope/dependency/backend/periscopex/parsers_edif.py
similarity index 100%
rename from backend/periscopex/parsers_edif.py
rename to periscope/dependency/backend/periscopex/parsers_edif.py
diff --git a/backend/periscopex/pin_function_tokens.py b/periscope/dependency/backend/periscopex/pin_function_tokens.py
similarity index 100%
rename from backend/periscopex/pin_function_tokens.py
rename to periscope/dependency/backend/periscopex/pin_function_tokens.py
diff --git a/backend/periscopex/pin_mux_check.py b/periscope/dependency/backend/periscopex/pin_mux_check.py
similarity index 100%
rename from backend/periscopex/pin_mux_check.py
rename to periscope/dependency/backend/periscopex/pin_mux_check.py
diff --git a/backend/periscopex/resolve_passives.py b/periscope/dependency/backend/periscopex/resolve_passives.py
similarity index 100%
rename from backend/periscopex/resolve_passives.py
rename to periscope/dependency/backend/periscopex/resolve_passives.py
diff --git a/backend/periscopex/taxonomy.py b/periscope/dependency/backend/periscopex/taxonomy.py
similarity index 100%
rename from backend/periscopex/taxonomy.py
rename to periscope/dependency/backend/periscopex/taxonomy.py
diff --git a/backend/periscopex/utils.py b/periscope/dependency/backend/periscopex/utils.py
similarity index 100%
rename from backend/periscopex/utils.py
rename to periscope/dependency/backend/periscopex/utils.py
diff --git a/backend/periscopex/validate.py b/periscope/dependency/backend/periscopex/validate.py
similarity index 100%
rename from backend/periscopex/validate.py
rename to periscope/dependency/backend/periscopex/validate.py
diff --git a/backend/periscopex/validation_tools.py b/periscope/dependency/backend/periscopex/validation_tools.py
similarity index 100%
rename from backend/periscopex/validation_tools.py
rename to periscope/dependency/backend/periscopex/validation_tools.py
diff --git a/backend/pipeline_worker.py b/periscope/dependency/backend/pipeline_worker.py
similarity index 100%
rename from backend/pipeline_worker.py
rename to periscope/dependency/backend/pipeline_worker.py
diff --git a/backend/requirements.txt b/periscope/dependency/backend/requirements.txt
similarity index 100%
rename from backend/requirements.txt
rename to periscope/dependency/backend/requirements.txt
diff --git a/periscope/dependency/backend/routers/__init__.py b/periscope/dependency/backend/routers/__init__.py
new file mode 100644
index 0000000..b36383a
--- /dev/null
+++ b/periscope/dependency/backend/routers/__init__.py
@@ -0,0 +1,3 @@
+from pkgutil import extend_path
+
+__path__ = extend_path(__path__, __name__)
diff --git a/backend/routers/admin.py b/periscope/dependency/backend/routers/admin.py
similarity index 100%
rename from backend/routers/admin.py
rename to periscope/dependency/backend/routers/admin.py
diff --git a/backend/routers/contact.py b/periscope/dependency/backend/routers/contact.py
similarity index 100%
rename from backend/routers/contact.py
rename to periscope/dependency/backend/routers/contact.py
diff --git a/backend/routers/deps.py b/periscope/dependency/backend/routers/deps.py
similarity index 100%
rename from backend/routers/deps.py
rename to periscope/dependency/backend/routers/deps.py
diff --git a/backend/routers/feedback.py b/periscope/dependency/backend/routers/feedback.py
similarity index 100%
rename from backend/routers/feedback.py
rename to periscope/dependency/backend/routers/feedback.py
diff --git a/backend/routers/pipeline.py b/periscope/dependency/backend/routers/pipeline.py
similarity index 100%
rename from backend/routers/pipeline.py
rename to periscope/dependency/backend/routers/pipeline.py
diff --git a/backend/routers/projects.py b/periscope/dependency/backend/routers/projects.py
similarity index 100%
rename from backend/routers/projects.py
rename to periscope/dependency/backend/routers/projects.py
diff --git a/backend/routers/reports.py b/periscope/dependency/backend/routers/reports.py
similarity index 100%
rename from backend/routers/reports.py
rename to periscope/dependency/backend/routers/reports.py
diff --git a/backend/routers/survey.py b/periscope/dependency/backend/routers/survey.py
similarity index 100%
rename from backend/routers/survey.py
rename to periscope/dependency/backend/routers/survey.py
diff --git a/periscope/dependency/backend/services/__init__.py b/periscope/dependency/backend/services/__init__.py
new file mode 100644
index 0000000..b36383a
--- /dev/null
+++ b/periscope/dependency/backend/services/__init__.py
@@ -0,0 +1,3 @@
+from pkgutil import extend_path
+
+__path__ = extend_path(__path__, __name__)
diff --git a/backend/services/admin_settings.py b/periscope/dependency/backend/services/admin_settings.py
similarity index 100%
rename from backend/services/admin_settings.py
rename to periscope/dependency/backend/services/admin_settings.py
diff --git a/backend/services/api_logs.py b/periscope/dependency/backend/services/api_logs.py
similarity index 100%
rename from backend/services/api_logs.py
rename to periscope/dependency/backend/services/api_logs.py
diff --git a/backend/services/billing_hook.py b/periscope/dependency/backend/services/billing_hook.py
similarity index 100%
rename from backend/services/billing_hook.py
rename to periscope/dependency/backend/services/billing_hook.py
diff --git a/backend/services/cost_estimator.py b/periscope/dependency/backend/services/cost_estimator.py
similarity index 100%
rename from backend/services/cost_estimator.py
rename to periscope/dependency/backend/services/cost_estimator.py
diff --git a/backend/services/datasheet_store.py b/periscope/dependency/backend/services/datasheet_store.py
similarity index 100%
rename from backend/services/datasheet_store.py
rename to periscope/dependency/backend/services/datasheet_store.py
diff --git a/backend/services/dedupe_findings.py b/periscope/dependency/backend/services/dedupe_findings.py
similarity index 100%
rename from backend/services/dedupe_findings.py
rename to periscope/dependency/backend/services/dedupe_findings.py
diff --git a/backend/services/digikey.py b/periscope/dependency/backend/services/digikey.py
similarity index 100%
rename from backend/services/digikey.py
rename to periscope/dependency/backend/services/digikey.py
diff --git a/backend/services/email.py b/periscope/dependency/backend/services/email.py
similarity index 100%
rename from backend/services/email.py
rename to periscope/dependency/backend/services/email.py
diff --git a/backend/services/event_bridge.py b/periscope/dependency/backend/services/event_bridge.py
similarity index 100%
rename from backend/services/event_bridge.py
rename to periscope/dependency/backend/services/event_bridge.py
diff --git a/backend/services/extraction.py b/periscope/dependency/backend/services/extraction.py
similarity index 100%
rename from backend/services/extraction.py
rename to periscope/dependency/backend/services/extraction.py
diff --git a/backend/services/job_runner.py b/periscope/dependency/backend/services/job_runner.py
similarity index 100%
rename from backend/services/job_runner.py
rename to periscope/dependency/backend/services/job_runner.py
diff --git a/backend/services/llm/__init__.py b/periscope/dependency/backend/services/llm/__init__.py
similarity index 91%
rename from backend/services/llm/__init__.py
rename to periscope/dependency/backend/services/llm/__init__.py
index 8cb1b88..b2f37ee 100644
--- a/backend/services/llm/__init__.py
+++ b/periscope/dependency/backend/services/llm/__init__.py
@@ -5,6 +5,9 @@ All model calls in the backend route through this package via the
overrides via ``Settings.provider_*`` env vars route specific stages to
Anthropic or Gemini if those keys are configured.
"""
+from pkgutil import extend_path
+
+__path__ = extend_path(__path__, __name__)
from backend.services.llm.factory import call_with_fallback, get_provider
from backend.services.llm.types import (
diff --git a/backend/services/llm/anthropic_provider.py b/periscope/dependency/backend/services/llm/anthropic_provider.py
similarity index 100%
rename from backend/services/llm/anthropic_provider.py
rename to periscope/dependency/backend/services/llm/anthropic_provider.py
diff --git a/backend/services/llm/base.py b/periscope/dependency/backend/services/llm/base.py
similarity index 100%
rename from backend/services/llm/base.py
rename to periscope/dependency/backend/services/llm/base.py
diff --git a/backend/services/llm/factory.py b/periscope/dependency/backend/services/llm/factory.py
similarity index 100%
rename from backend/services/llm/factory.py
rename to periscope/dependency/backend/services/llm/factory.py
diff --git a/backend/services/llm/gemini_provider.py b/periscope/dependency/backend/services/llm/gemini_provider.py
similarity index 100%
rename from backend/services/llm/gemini_provider.py
rename to periscope/dependency/backend/services/llm/gemini_provider.py
diff --git a/backend/services/llm/pricing.py b/periscope/dependency/backend/services/llm/pricing.py
similarity index 100%
rename from backend/services/llm/pricing.py
rename to periscope/dependency/backend/services/llm/pricing.py
diff --git a/backend/services/llm/types.py b/periscope/dependency/backend/services/llm/types.py
similarity index 100%
rename from backend/services/llm/types.py
rename to periscope/dependency/backend/services/llm/types.py
diff --git a/backend/services/normalize_findings.py b/periscope/dependency/backend/services/normalize_findings.py
similarity index 100%
rename from backend/services/normalize_findings.py
rename to periscope/dependency/backend/services/normalize_findings.py
diff --git a/backend/services/pipeline.py b/periscope/dependency/backend/services/pipeline.py
similarity index 100%
rename from backend/services/pipeline.py
rename to periscope/dependency/backend/services/pipeline.py
diff --git a/backend/services/projects.py b/periscope/dependency/backend/services/projects.py
similarity index 100%
rename from backend/services/projects.py
rename to periscope/dependency/backend/services/projects.py
diff --git a/backend/services/purple_parts.py b/periscope/dependency/backend/services/purple_parts.py
similarity index 100%
rename from backend/services/purple_parts.py
rename to periscope/dependency/backend/services/purple_parts.py
diff --git a/backend/services/storage.py b/periscope/dependency/backend/services/storage.py
similarity index 100%
rename from backend/services/storage.py
rename to periscope/dependency/backend/services/storage.py
diff --git a/backend/services/storage_gcs.py b/periscope/dependency/backend/services/storage_gcs.py
similarity index 100%
rename from backend/services/storage_gcs.py
rename to periscope/dependency/backend/services/storage_gcs.py
diff --git a/backend/services/survey.py b/periscope/dependency/backend/services/survey.py
similarity index 100%
rename from backend/services/survey.py
rename to periscope/dependency/backend/services/survey.py
diff --git a/backend/services/validation.py b/periscope/dependency/backend/services/validation.py
similarity index 100%
rename from backend/services/validation.py
rename to periscope/dependency/backend/services/validation.py
diff --git a/backend/skills_manifest.json b/periscope/dependency/backend/skills_manifest.json
similarity index 100%
rename from backend/skills_manifest.json
rename to periscope/dependency/backend/skills_manifest.json
diff --git a/docs/how-it-works.svg b/periscope/dependency/docs/how-it-works.svg
similarity index 100%
rename from docs/how-it-works.svg
rename to periscope/dependency/docs/how-it-works.svg
diff --git a/frontend/.gitignore b/periscope/dependency/frontend/.gitignore
similarity index 100%
rename from frontend/.gitignore
rename to periscope/dependency/frontend/.gitignore
diff --git a/frontend/AGENTS.md b/periscope/dependency/frontend/AGENTS.md
similarity index 100%
rename from frontend/AGENTS.md
rename to periscope/dependency/frontend/AGENTS.md
diff --git a/frontend/CLAUDE.md b/periscope/dependency/frontend/CLAUDE.md
similarity index 100%
rename from frontend/CLAUDE.md
rename to periscope/dependency/frontend/CLAUDE.md
diff --git a/frontend/README.md b/periscope/dependency/frontend/README.md
similarity index 100%
rename from frontend/README.md
rename to periscope/dependency/frontend/README.md
diff --git a/frontend/components.json b/periscope/dependency/frontend/components.json
similarity index 100%
rename from frontend/components.json
rename to periscope/dependency/frontend/components.json
diff --git a/frontend/content/changelog.md b/periscope/dependency/frontend/content/changelog.md
similarity index 98%
rename from frontend/content/changelog.md
rename to periscope/dependency/frontend/content/changelog.md
index 5d85c31..1f444b0 100644
--- a/frontend/content/changelog.md
+++ b/periscope/dependency/frontend/content/changelog.md
@@ -2,6 +2,14 @@
What's new in Periscope.
+## 2.38.0 — 2026-09-20 — Physical split: periscope/src vs periscope/dependency
+
+Split the checkout so native Periscope and inherited PinScope are separate trees. No finding-engine rewrite. AGPL `LICENSE` stays at the repo root. The GitHub fork is not detached.
+
+- [Changed] Native modules live under `periscope/src/` (PCB, placement, finding clamp, DeepSeek, local auth, plugin, Dockerfiles).
+- [Changed] PinScope-derived modules live under `periscope/dependency/` (graph, parsers, `validate.py`, pipeline, OSS Next shell, skills, taxonomy, `simple_project`).
+- [Changed] Docker overlays dependency then src. Local `backend` package merges both trees. Do not delete the inherited tree; Fase C replaces modules incrementally.
+
## 2.37.1 — 2026-09-20 — PCB not_reviewed is board ICs, not the shared library
Emmaforo showed 20 “components not reviewed” that were other projects’ 1.5–1.7 extracts (LAN8720A, W25Q, …). BOM ICs were already 1.13.0 and *were* reviewed.
diff --git a/frontend/content/file-guide.md b/periscope/dependency/frontend/content/file-guide.md
similarity index 100%
rename from frontend/content/file-guide.md
rename to periscope/dependency/frontend/content/file-guide.md
diff --git a/frontend/content/privacy.md b/periscope/dependency/frontend/content/privacy.md
similarity index 100%
rename from frontend/content/privacy.md
rename to periscope/dependency/frontend/content/privacy.md
diff --git a/frontend/content/terms.md b/periscope/dependency/frontend/content/terms.md
similarity index 100%
rename from frontend/content/terms.md
rename to periscope/dependency/frontend/content/terms.md
diff --git a/frontend/eslint.config.mjs b/periscope/dependency/frontend/eslint.config.mjs
similarity index 100%
rename from frontend/eslint.config.mjs
rename to periscope/dependency/frontend/eslint.config.mjs
diff --git a/frontend/next.config.ts b/periscope/dependency/frontend/next.config.ts
similarity index 100%
rename from frontend/next.config.ts
rename to periscope/dependency/frontend/next.config.ts
diff --git a/frontend/package-lock.json b/periscope/dependency/frontend/package-lock.json
similarity index 100%
rename from frontend/package-lock.json
rename to periscope/dependency/frontend/package-lock.json
diff --git a/frontend/package.json b/periscope/dependency/frontend/package.json
similarity index 97%
rename from frontend/package.json
rename to periscope/dependency/frontend/package.json
index ba0d3f4..d29c19a 100644
--- a/frontend/package.json
+++ b/periscope/dependency/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "frontend",
- "version": "2.26.3",
+ "version": "2.38.0",
"private": true,
"scripts": {
"sync-version": "node scripts/sync-version.mjs",
diff --git a/frontend/postcss.config.mjs b/periscope/dependency/frontend/postcss.config.mjs
similarity index 100%
rename from frontend/postcss.config.mjs
rename to periscope/dependency/frontend/postcss.config.mjs
diff --git a/frontend/public/datasheet.gif b/periscope/dependency/frontend/public/datasheet.gif
similarity index 100%
rename from frontend/public/datasheet.gif
rename to periscope/dependency/frontend/public/datasheet.gif
diff --git a/frontend/public/derating.png b/periscope/dependency/frontend/public/derating.png
similarity index 100%
rename from frontend/public/derating.png
rename to periscope/dependency/frontend/public/derating.png
diff --git a/frontend/public/eda-logos/altium.svg b/periscope/dependency/frontend/public/eda-logos/altium.svg
similarity index 100%
rename from frontend/public/eda-logos/altium.svg
rename to periscope/dependency/frontend/public/eda-logos/altium.svg
diff --git a/frontend/public/eda-logos/autodesk.svg b/periscope/dependency/frontend/public/eda-logos/autodesk.svg
similarity index 100%
rename from frontend/public/eda-logos/autodesk.svg
rename to periscope/dependency/frontend/public/eda-logos/autodesk.svg
diff --git a/frontend/public/eda-logos/cadence.svg b/periscope/dependency/frontend/public/eda-logos/cadence.svg
similarity index 100%
rename from frontend/public/eda-logos/cadence.svg
rename to periscope/dependency/frontend/public/eda-logos/cadence.svg
diff --git a/frontend/public/eda-logos/easyeda.svg b/periscope/dependency/frontend/public/eda-logos/easyeda.svg
similarity index 100%
rename from frontend/public/eda-logos/easyeda.svg
rename to periscope/dependency/frontend/public/eda-logos/easyeda.svg
diff --git a/frontend/public/eda-logos/kicad.svg b/periscope/dependency/frontend/public/eda-logos/kicad.svg
similarity index 100%
rename from frontend/public/eda-logos/kicad.svg
rename to periscope/dependency/frontend/public/eda-logos/kicad.svg
diff --git a/frontend/public/eda-logos/orcad.svg b/periscope/dependency/frontend/public/eda-logos/orcad.svg
similarity index 100%
rename from frontend/public/eda-logos/orcad.svg
rename to periscope/dependency/frontend/public/eda-logos/orcad.svg
diff --git a/frontend/public/eda-logos/siemens.svg b/periscope/dependency/frontend/public/eda-logos/siemens.svg
similarity index 100%
rename from frontend/public/eda-logos/siemens.svg
rename to periscope/dependency/frontend/public/eda-logos/siemens.svg
diff --git a/frontend/public/examples/TI-MSP-KICAD9-TUTORIAL-SCHEMATIC.pdf b/periscope/dependency/frontend/public/examples/TI-MSP-KICAD9-TUTORIAL-SCHEMATIC.pdf
similarity index 100%
rename from frontend/public/examples/TI-MSP-KICAD9-TUTORIAL-SCHEMATIC.pdf
rename to periscope/dependency/frontend/public/examples/TI-MSP-KICAD9-TUTORIAL-SCHEMATIC.pdf
diff --git a/frontend/public/examples/TI-MSP-KICAD9-TUTORIAL.asc b/periscope/dependency/frontend/public/examples/TI-MSP-KICAD9-TUTORIAL.asc
similarity index 100%
rename from frontend/public/examples/TI-MSP-KICAD9-TUTORIAL.asc
rename to periscope/dependency/frontend/public/examples/TI-MSP-KICAD9-TUTORIAL.asc
diff --git a/frontend/public/examples/TI-MSP-KICAD9-TUTORIAL.csv b/periscope/dependency/frontend/public/examples/TI-MSP-KICAD9-TUTORIAL.csv
similarity index 100%
rename from frontend/public/examples/TI-MSP-KICAD9-TUTORIAL.csv
rename to periscope/dependency/frontend/public/examples/TI-MSP-KICAD9-TUTORIAL.csv
diff --git a/frontend/public/faradworks-logo-white.png b/periscope/dependency/frontend/public/faradworks-logo-white.png
similarity index 100%
rename from frontend/public/faradworks-logo-white.png
rename to periscope/dependency/frontend/public/faradworks-logo-white.png
diff --git a/frontend/public/favicon_io/android-chrome-192x192.png b/periscope/dependency/frontend/public/favicon_io/android-chrome-192x192.png
similarity index 100%
rename from frontend/public/favicon_io/android-chrome-192x192.png
rename to periscope/dependency/frontend/public/favicon_io/android-chrome-192x192.png
diff --git a/frontend/public/favicon_io/android-chrome-512x512.png b/periscope/dependency/frontend/public/favicon_io/android-chrome-512x512.png
similarity index 100%
rename from frontend/public/favicon_io/android-chrome-512x512.png
rename to periscope/dependency/frontend/public/favicon_io/android-chrome-512x512.png
diff --git a/frontend/public/favicon_io/apple-touch-icon.png b/periscope/dependency/frontend/public/favicon_io/apple-touch-icon.png
similarity index 100%
rename from frontend/public/favicon_io/apple-touch-icon.png
rename to periscope/dependency/frontend/public/favicon_io/apple-touch-icon.png
diff --git a/frontend/public/favicon_io/favicon-16x16.png b/periscope/dependency/frontend/public/favicon_io/favicon-16x16.png
similarity index 100%
rename from frontend/public/favicon_io/favicon-16x16.png
rename to periscope/dependency/frontend/public/favicon_io/favicon-16x16.png
diff --git a/frontend/public/favicon_io/favicon-32x32.png b/periscope/dependency/frontend/public/favicon_io/favicon-32x32.png
similarity index 100%
rename from frontend/public/favicon_io/favicon-32x32.png
rename to periscope/dependency/frontend/public/favicon_io/favicon-32x32.png
diff --git a/frontend/public/favicon_io/favicon.ico b/periscope/dependency/frontend/public/favicon_io/favicon.ico
similarity index 100%
rename from frontend/public/favicon_io/favicon.ico
rename to periscope/dependency/frontend/public/favicon_io/favicon.ico
diff --git a/frontend/public/favicon_io/site.webmanifest b/periscope/dependency/frontend/public/favicon_io/site.webmanifest
similarity index 100%
rename from frontend/public/favicon_io/site.webmanifest
rename to periscope/dependency/frontend/public/favicon_io/site.webmanifest
diff --git a/frontend/public/file.svg b/periscope/dependency/frontend/public/file.svg
similarity index 100%
rename from frontend/public/file.svg
rename to periscope/dependency/frontend/public/file.svg
diff --git a/frontend/public/globe.svg b/periscope/dependency/frontend/public/globe.svg
similarity index 100%
rename from frontend/public/globe.svg
rename to periscope/dependency/frontend/public/globe.svg
diff --git a/frontend/public/next.svg b/periscope/dependency/frontend/public/next.svg
similarity index 100%
rename from frontend/public/next.svg
rename to periscope/dependency/frontend/public/next.svg
diff --git a/frontend/public/power-tree.gif b/periscope/dependency/frontend/public/power-tree.gif
similarity index 100%
rename from frontend/public/power-tree.gif
rename to periscope/dependency/frontend/public/power-tree.gif
diff --git a/frontend/public/report.png b/periscope/dependency/frontend/public/report.png
similarity index 100%
rename from frontend/public/report.png
rename to periscope/dependency/frontend/public/report.png
diff --git a/frontend/public/vercel.svg b/periscope/dependency/frontend/public/vercel.svg
similarity index 100%
rename from frontend/public/vercel.svg
rename to periscope/dependency/frontend/public/vercel.svg
diff --git a/frontend/public/window.svg b/periscope/dependency/frontend/public/window.svg
similarity index 100%
rename from frontend/public/window.svg
rename to periscope/dependency/frontend/public/window.svg
diff --git a/frontend/scripts/sync-version.mjs b/periscope/dependency/frontend/scripts/sync-version.mjs
similarity index 100%
rename from frontend/scripts/sync-version.mjs
rename to periscope/dependency/frontend/scripts/sync-version.mjs
diff --git a/frontend/src/app/(app)/admin/page.tsx b/periscope/dependency/frontend/src/app/(app)/admin/page.tsx
similarity index 100%
rename from frontend/src/app/(app)/admin/page.tsx
rename to periscope/dependency/frontend/src/app/(app)/admin/page.tsx
diff --git a/frontend/src/app/(app)/api/graph/[id]/route.ts b/periscope/dependency/frontend/src/app/(app)/api/graph/[id]/route.ts
similarity index 100%
rename from frontend/src/app/(app)/api/graph/[id]/route.ts
rename to periscope/dependency/frontend/src/app/(app)/api/graph/[id]/route.ts
diff --git a/frontend/src/app/(app)/api/projects/route.ts b/periscope/dependency/frontend/src/app/(app)/api/projects/route.ts
similarity index 100%
rename from frontend/src/app/(app)/api/projects/route.ts
rename to periscope/dependency/frontend/src/app/(app)/api/projects/route.ts
diff --git a/frontend/src/app/(app)/api/report/[id]/route.ts b/periscope/dependency/frontend/src/app/(app)/api/report/[id]/route.ts
similarity index 100%
rename from frontend/src/app/(app)/api/report/[id]/route.ts
rename to periscope/dependency/frontend/src/app/(app)/api/report/[id]/route.ts
diff --git a/frontend/src/app/(app)/dashboard/page.tsx b/periscope/dependency/frontend/src/app/(app)/dashboard/page.tsx
similarity index 100%
rename from frontend/src/app/(app)/dashboard/page.tsx
rename to periscope/dependency/frontend/src/app/(app)/dashboard/page.tsx
diff --git a/frontend/src/app/(app)/feedback/page.tsx b/periscope/dependency/frontend/src/app/(app)/feedback/page.tsx
similarity index 100%
rename from frontend/src/app/(app)/feedback/page.tsx
rename to periscope/dependency/frontend/src/app/(app)/feedback/page.tsx
diff --git a/frontend/src/app/(app)/layout.tsx b/periscope/dependency/frontend/src/app/(app)/layout.tsx
similarity index 100%
rename from frontend/src/app/(app)/layout.tsx
rename to periscope/dependency/frontend/src/app/(app)/layout.tsx
diff --git a/periscope/dependency/frontend/src/app/(app)/library/page.tsx b/periscope/dependency/frontend/src/app/(app)/library/page.tsx
new file mode 120000
index 0000000..561e799
--- /dev/null
+++ b/periscope/dependency/frontend/src/app/(app)/library/page.tsx
@@ -0,0 +1 @@
+../../../../../../src/frontend/src/app/(app)/library/page.tsx
\ No newline at end of file
diff --git a/frontend/src/app/(app)/project/[id]/page.tsx b/periscope/dependency/frontend/src/app/(app)/project/[id]/page.tsx
similarity index 100%
rename from frontend/src/app/(app)/project/[id]/page.tsx
rename to periscope/dependency/frontend/src/app/(app)/project/[id]/page.tsx
diff --git a/periscope/dependency/frontend/src/app/(app)/project/[id]/pcb/page.tsx b/periscope/dependency/frontend/src/app/(app)/project/[id]/pcb/page.tsx
new file mode 120000
index 0000000..3728a98
--- /dev/null
+++ b/periscope/dependency/frontend/src/app/(app)/project/[id]/pcb/page.tsx
@@ -0,0 +1 @@
+../../../../../../../../src/frontend/src/app/(app)/project/[id]/pcb/page.tsx
\ No newline at end of file
diff --git a/periscope/dependency/frontend/src/app/(app)/project/[id]/placement/page.tsx b/periscope/dependency/frontend/src/app/(app)/project/[id]/placement/page.tsx
new file mode 120000
index 0000000..c7f9b95
--- /dev/null
+++ b/periscope/dependency/frontend/src/app/(app)/project/[id]/placement/page.tsx
@@ -0,0 +1 @@
+../../../../../../../../src/frontend/src/app/(app)/project/[id]/placement/page.tsx
\ No newline at end of file
diff --git a/frontend/src/app/(app)/project/[id]/progress/page.tsx b/periscope/dependency/frontend/src/app/(app)/project/[id]/progress/page.tsx
similarity index 100%
rename from frontend/src/app/(app)/project/[id]/progress/page.tsx
rename to periscope/dependency/frontend/src/app/(app)/project/[id]/progress/page.tsx
diff --git a/frontend/src/app/(app)/project/[id]/report/page.tsx b/periscope/dependency/frontend/src/app/(app)/project/[id]/report/page.tsx
similarity index 100%
rename from frontend/src/app/(app)/project/[id]/report/page.tsx
rename to periscope/dependency/frontend/src/app/(app)/project/[id]/report/page.tsx
diff --git a/frontend/src/app/(marketing)/changelog/page.tsx b/periscope/dependency/frontend/src/app/(marketing)/changelog/page.tsx
similarity index 100%
rename from frontend/src/app/(marketing)/changelog/page.tsx
rename to periscope/dependency/frontend/src/app/(marketing)/changelog/page.tsx
diff --git a/frontend/src/app/(marketing)/contact/actions.ts b/periscope/dependency/frontend/src/app/(marketing)/contact/actions.ts
similarity index 100%
rename from frontend/src/app/(marketing)/contact/actions.ts
rename to periscope/dependency/frontend/src/app/(marketing)/contact/actions.ts
diff --git a/frontend/src/app/(marketing)/contact/contact-form.tsx b/periscope/dependency/frontend/src/app/(marketing)/contact/contact-form.tsx
similarity index 100%
rename from frontend/src/app/(marketing)/contact/contact-form.tsx
rename to periscope/dependency/frontend/src/app/(marketing)/contact/contact-form.tsx
diff --git a/frontend/src/app/(marketing)/contact/nav.tsx b/periscope/dependency/frontend/src/app/(marketing)/contact/nav.tsx
similarity index 100%
rename from frontend/src/app/(marketing)/contact/nav.tsx
rename to periscope/dependency/frontend/src/app/(marketing)/contact/nav.tsx
diff --git a/frontend/src/app/(marketing)/contact/page.tsx b/periscope/dependency/frontend/src/app/(marketing)/contact/page.tsx
similarity index 100%
rename from frontend/src/app/(marketing)/contact/page.tsx
rename to periscope/dependency/frontend/src/app/(marketing)/contact/page.tsx
diff --git a/frontend/src/app/(marketing)/file-guide/page.tsx b/periscope/dependency/frontend/src/app/(marketing)/file-guide/page.tsx
similarity index 100%
rename from frontend/src/app/(marketing)/file-guide/page.tsx
rename to periscope/dependency/frontend/src/app/(marketing)/file-guide/page.tsx
diff --git a/frontend/src/app/(marketing)/landing-cta.tsx b/periscope/dependency/frontend/src/app/(marketing)/landing-cta.tsx
similarity index 100%
rename from frontend/src/app/(marketing)/landing-cta.tsx
rename to periscope/dependency/frontend/src/app/(marketing)/landing-cta.tsx
diff --git a/frontend/src/app/(marketing)/layout.tsx b/periscope/dependency/frontend/src/app/(marketing)/layout.tsx
similarity index 100%
rename from frontend/src/app/(marketing)/layout.tsx
rename to periscope/dependency/frontend/src/app/(marketing)/layout.tsx
diff --git a/frontend/src/app/(marketing)/page.tsx b/periscope/dependency/frontend/src/app/(marketing)/page.tsx
similarity index 100%
rename from frontend/src/app/(marketing)/page.tsx
rename to periscope/dependency/frontend/src/app/(marketing)/page.tsx
diff --git a/frontend/src/app/(marketing)/privacy/page.tsx b/periscope/dependency/frontend/src/app/(marketing)/privacy/page.tsx
similarity index 100%
rename from frontend/src/app/(marketing)/privacy/page.tsx
rename to periscope/dependency/frontend/src/app/(marketing)/privacy/page.tsx
diff --git a/periscope/dependency/frontend/src/app/(marketing)/sign-in/page.tsx b/periscope/dependency/frontend/src/app/(marketing)/sign-in/page.tsx
new file mode 120000
index 0000000..2333196
--- /dev/null
+++ b/periscope/dependency/frontend/src/app/(marketing)/sign-in/page.tsx
@@ -0,0 +1 @@
+../../../../../../src/frontend/src/app/(marketing)/sign-in/page.tsx
\ No newline at end of file
diff --git a/periscope/dependency/frontend/src/app/(marketing)/sign-up/page.tsx b/periscope/dependency/frontend/src/app/(marketing)/sign-up/page.tsx
new file mode 120000
index 0000000..5c89b49
--- /dev/null
+++ b/periscope/dependency/frontend/src/app/(marketing)/sign-up/page.tsx
@@ -0,0 +1 @@
+../../../../../../src/frontend/src/app/(marketing)/sign-up/page.tsx
\ No newline at end of file
diff --git a/frontend/src/app/(marketing)/terms/page.tsx b/periscope/dependency/frontend/src/app/(marketing)/terms/page.tsx
similarity index 100%
rename from frontend/src/app/(marketing)/terms/page.tsx
rename to periscope/dependency/frontend/src/app/(marketing)/terms/page.tsx
diff --git a/frontend/src/app/favicon.ico b/periscope/dependency/frontend/src/app/favicon.ico
similarity index 100%
rename from frontend/src/app/favicon.ico
rename to periscope/dependency/frontend/src/app/favicon.ico
diff --git a/frontend/src/app/globals.css b/periscope/dependency/frontend/src/app/globals.css
similarity index 100%
rename from frontend/src/app/globals.css
rename to periscope/dependency/frontend/src/app/globals.css
diff --git a/frontend/src/app/layout.tsx b/periscope/dependency/frontend/src/app/layout.tsx
similarity index 100%
rename from frontend/src/app/layout.tsx
rename to periscope/dependency/frontend/src/app/layout.tsx
diff --git a/frontend/src/app/opengraph-image.tsx b/periscope/dependency/frontend/src/app/opengraph-image.tsx
similarity index 100%
rename from frontend/src/app/opengraph-image.tsx
rename to periscope/dependency/frontend/src/app/opengraph-image.tsx
diff --git a/frontend/src/app/robots.ts b/periscope/dependency/frontend/src/app/robots.ts
similarity index 100%
rename from frontend/src/app/robots.ts
rename to periscope/dependency/frontend/src/app/robots.ts
diff --git a/frontend/src/app/sitemap.ts b/periscope/dependency/frontend/src/app/sitemap.ts
similarity index 100%
rename from frontend/src/app/sitemap.ts
rename to periscope/dependency/frontend/src/app/sitemap.ts
diff --git a/frontend/src/app/twitter-image.tsx b/periscope/dependency/frontend/src/app/twitter-image.tsx
similarity index 100%
rename from frontend/src/app/twitter-image.tsx
rename to periscope/dependency/frontend/src/app/twitter-image.tsx
diff --git a/frontend/src/components/analytics/reddit-pixel-match-keys.tsx b/periscope/dependency/frontend/src/components/analytics/reddit-pixel-match-keys.tsx
similarity index 100%
rename from frontend/src/components/analytics/reddit-pixel-match-keys.tsx
rename to periscope/dependency/frontend/src/components/analytics/reddit-pixel-match-keys.tsx
diff --git a/frontend/src/components/analytics/reddit-pixel.tsx b/periscope/dependency/frontend/src/components/analytics/reddit-pixel.tsx
similarity index 100%
rename from frontend/src/components/analytics/reddit-pixel.tsx
rename to periscope/dependency/frontend/src/components/analytics/reddit-pixel.tsx
diff --git a/frontend/src/components/billing/credits-context.tsx b/periscope/dependency/frontend/src/components/billing/credits-context.tsx
similarity index 100%
rename from frontend/src/components/billing/credits-context.tsx
rename to periscope/dependency/frontend/src/components/billing/credits-context.tsx
diff --git a/frontend/src/components/billing/paused-run-banner.tsx b/periscope/dependency/frontend/src/components/billing/paused-run-banner.tsx
similarity index 100%
rename from frontend/src/components/billing/paused-run-banner.tsx
rename to periscope/dependency/frontend/src/components/billing/paused-run-banner.tsx
diff --git a/frontend/src/components/dashboard/create-project-dialog.tsx b/periscope/dependency/frontend/src/components/dashboard/create-project-dialog.tsx
similarity index 100%
rename from frontend/src/components/dashboard/create-project-dialog.tsx
rename to periscope/dependency/frontend/src/components/dashboard/create-project-dialog.tsx
diff --git a/frontend/src/components/dashboard/onboarding-survey.tsx b/periscope/dependency/frontend/src/components/dashboard/onboarding-survey.tsx
similarity index 100%
rename from frontend/src/components/dashboard/onboarding-survey.tsx
rename to periscope/dependency/frontend/src/components/dashboard/onboarding-survey.tsx
diff --git a/frontend/src/components/dashboard/project-card.tsx b/periscope/dependency/frontend/src/components/dashboard/project-card.tsx
similarity index 100%
rename from frontend/src/components/dashboard/project-card.tsx
rename to periscope/dependency/frontend/src/components/dashboard/project-card.tsx
diff --git a/frontend/src/components/dashboard/projects-table.tsx b/periscope/dependency/frontend/src/components/dashboard/projects-table.tsx
similarity index 100%
rename from frontend/src/components/dashboard/projects-table.tsx
rename to periscope/dependency/frontend/src/components/dashboard/projects-table.tsx
diff --git a/frontend/src/components/feedback/feedback-dialog.tsx b/periscope/dependency/frontend/src/components/feedback/feedback-dialog.tsx
similarity index 100%
rename from frontend/src/components/feedback/feedback-dialog.tsx
rename to periscope/dependency/frontend/src/components/feedback/feedback-dialog.tsx
diff --git a/periscope/dependency/frontend/src/components/layout/auth-gate.tsx b/periscope/dependency/frontend/src/components/layout/auth-gate.tsx
new file mode 120000
index 0000000..686abb6
--- /dev/null
+++ b/periscope/dependency/frontend/src/components/layout/auth-gate.tsx
@@ -0,0 +1 @@
+../../../../../src/frontend/src/components/layout/auth-gate.tsx
\ No newline at end of file
diff --git a/periscope/dependency/frontend/src/components/layout/sidebar-auth.tsx b/periscope/dependency/frontend/src/components/layout/sidebar-auth.tsx
new file mode 120000
index 0000000..0d706d5
--- /dev/null
+++ b/periscope/dependency/frontend/src/components/layout/sidebar-auth.tsx
@@ -0,0 +1 @@
+../../../../../src/frontend/src/components/layout/sidebar-auth.tsx
\ No newline at end of file
diff --git a/frontend/src/components/layout/sidebar.tsx b/periscope/dependency/frontend/src/components/layout/sidebar.tsx
similarity index 100%
rename from frontend/src/components/layout/sidebar.tsx
rename to periscope/dependency/frontend/src/components/layout/sidebar.tsx
diff --git a/frontend/src/components/legal/changelog-timeline.tsx b/periscope/dependency/frontend/src/components/legal/changelog-timeline.tsx
similarity index 100%
rename from frontend/src/components/legal/changelog-timeline.tsx
rename to periscope/dependency/frontend/src/components/legal/changelog-timeline.tsx
diff --git a/frontend/src/components/legal/file-guide-page.tsx b/periscope/dependency/frontend/src/components/legal/file-guide-page.tsx
similarity index 100%
rename from frontend/src/components/legal/file-guide-page.tsx
rename to periscope/dependency/frontend/src/components/legal/file-guide-page.tsx
diff --git a/frontend/src/components/legal/legal-page.tsx b/periscope/dependency/frontend/src/components/legal/legal-page.tsx
similarity index 100%
rename from frontend/src/components/legal/legal-page.tsx
rename to periscope/dependency/frontend/src/components/legal/legal-page.tsx
diff --git a/frontend/src/components/marketing/pricing-section.tsx b/periscope/dependency/frontend/src/components/marketing/pricing-section.tsx
similarity index 100%
rename from frontend/src/components/marketing/pricing-section.tsx
rename to periscope/dependency/frontend/src/components/marketing/pricing-section.tsx
diff --git a/frontend/src/components/pdf/pdf-viewer-panel.tsx b/periscope/dependency/frontend/src/components/pdf/pdf-viewer-panel.tsx
similarity index 100%
rename from frontend/src/components/pdf/pdf-viewer-panel.tsx
rename to periscope/dependency/frontend/src/components/pdf/pdf-viewer-panel.tsx
diff --git a/frontend/src/components/pdf/pdf-viewer-sheet.tsx b/periscope/dependency/frontend/src/components/pdf/pdf-viewer-sheet.tsx
similarity index 100%
rename from frontend/src/components/pdf/pdf-viewer-sheet.tsx
rename to periscope/dependency/frontend/src/components/pdf/pdf-viewer-sheet.tsx
diff --git a/frontend/src/components/progress/pipeline-stepper.tsx b/periscope/dependency/frontend/src/components/progress/pipeline-stepper.tsx
similarity index 100%
rename from frontend/src/components/progress/pipeline-stepper.tsx
rename to periscope/dependency/frontend/src/components/progress/pipeline-stepper.tsx
diff --git a/periscope/dependency/frontend/src/components/project/impedance-panel.tsx b/periscope/dependency/frontend/src/components/project/impedance-panel.tsx
new file mode 120000
index 0000000..93f1817
--- /dev/null
+++ b/periscope/dependency/frontend/src/components/project/impedance-panel.tsx
@@ -0,0 +1 @@
+../../../../../src/frontend/src/components/project/impedance-panel.tsx
\ No newline at end of file
diff --git a/periscope/dependency/frontend/src/components/project/pcb-upload.tsx b/periscope/dependency/frontend/src/components/project/pcb-upload.tsx
new file mode 120000
index 0000000..be4ea02
--- /dev/null
+++ b/periscope/dependency/frontend/src/components/project/pcb-upload.tsx
@@ -0,0 +1 @@
+../../../../../src/frontend/src/components/project/pcb-upload.tsx
\ No newline at end of file
diff --git a/periscope/dependency/frontend/src/components/project/topology-panel.tsx b/periscope/dependency/frontend/src/components/project/topology-panel.tsx
new file mode 120000
index 0000000..57c2a9a
--- /dev/null
+++ b/periscope/dependency/frontend/src/components/project/topology-panel.tsx
@@ -0,0 +1 @@
+../../../../../src/frontend/src/components/project/topology-panel.tsx
\ No newline at end of file
diff --git a/frontend/src/components/report/component-group.tsx b/periscope/dependency/frontend/src/components/report/component-group.tsx
similarity index 100%
rename from frontend/src/components/report/component-group.tsx
rename to periscope/dependency/frontend/src/components/report/component-group.tsx
diff --git a/frontend/src/components/report/finding-card.tsx b/periscope/dependency/frontend/src/components/report/finding-card.tsx
similarity index 100%
rename from frontend/src/components/report/finding-card.tsx
rename to periscope/dependency/frontend/src/components/report/finding-card.tsx
diff --git a/frontend/src/components/report/finding-comments.tsx b/periscope/dependency/frontend/src/components/report/finding-comments.tsx
similarity index 100%
rename from frontend/src/components/report/finding-comments.tsx
rename to periscope/dependency/frontend/src/components/report/finding-comments.tsx
diff --git a/frontend/src/components/report/finding-focus-view.tsx b/periscope/dependency/frontend/src/components/report/finding-focus-view.tsx
similarity index 100%
rename from frontend/src/components/report/finding-focus-view.tsx
rename to periscope/dependency/frontend/src/components/report/finding-focus-view.tsx
diff --git a/periscope/dependency/frontend/src/components/report/finding-review-controls.tsx b/periscope/dependency/frontend/src/components/report/finding-review-controls.tsx
new file mode 120000
index 0000000..1b91591
--- /dev/null
+++ b/periscope/dependency/frontend/src/components/report/finding-review-controls.tsx
@@ -0,0 +1 @@
+../../../../../src/frontend/src/components/report/finding-review-controls.tsx
\ No newline at end of file
diff --git a/frontend/src/components/report/findings-list.tsx b/periscope/dependency/frontend/src/components/report/findings-list.tsx
similarity index 100%
rename from frontend/src/components/report/findings-list.tsx
rename to periscope/dependency/frontend/src/components/report/findings-list.tsx
diff --git a/periscope/dependency/frontend/src/components/report/findings-tree.tsx b/periscope/dependency/frontend/src/components/report/findings-tree.tsx
new file mode 120000
index 0000000..c9e07dd
--- /dev/null
+++ b/periscope/dependency/frontend/src/components/report/findings-tree.tsx
@@ -0,0 +1 @@
+../../../../../src/frontend/src/components/report/findings-tree.tsx
\ No newline at end of file
diff --git a/frontend/src/components/report/mention-input.tsx b/periscope/dependency/frontend/src/components/report/mention-input.tsx
similarity index 100%
rename from frontend/src/components/report/mention-input.tsx
rename to periscope/dependency/frontend/src/components/report/mention-input.tsx
diff --git a/periscope/dependency/frontend/src/components/report/pcb-exam-section.tsx b/periscope/dependency/frontend/src/components/report/pcb-exam-section.tsx
new file mode 120000
index 0000000..f53da65
--- /dev/null
+++ b/periscope/dependency/frontend/src/components/report/pcb-exam-section.tsx
@@ -0,0 +1 @@
+../../../../../src/frontend/src/components/report/pcb-exam-section.tsx
\ No newline at end of file
diff --git a/frontend/src/components/report/report-filters.tsx b/periscope/dependency/frontend/src/components/report/report-filters.tsx
similarity index 100%
rename from frontend/src/components/report/report-filters.tsx
rename to periscope/dependency/frontend/src/components/report/report-filters.tsx
diff --git a/frontend/src/components/report/report-summary.tsx b/periscope/dependency/frontend/src/components/report/report-summary.tsx
similarity index 100%
rename from frontend/src/components/report/report-summary.tsx
rename to periscope/dependency/frontend/src/components/report/report-summary.tsx
diff --git a/frontend/src/components/report/reviewed-section.tsx b/periscope/dependency/frontend/src/components/report/reviewed-section.tsx
similarity index 100%
rename from frontend/src/components/report/reviewed-section.tsx
rename to periscope/dependency/frontend/src/components/report/reviewed-section.tsx
diff --git a/frontend/src/components/report/status-badge.tsx b/periscope/dependency/frontend/src/components/report/status-badge.tsx
similarity index 100%
rename from frontend/src/components/report/status-badge.tsx
rename to periscope/dependency/frontend/src/components/report/status-badge.tsx
diff --git a/frontend/src/components/theme/clerk-theme-provider.tsx b/periscope/dependency/frontend/src/components/theme/clerk-theme-provider.tsx
similarity index 100%
rename from frontend/src/components/theme/clerk-theme-provider.tsx
rename to periscope/dependency/frontend/src/components/theme/clerk-theme-provider.tsx
diff --git a/frontend/src/components/theme/theme-provider.tsx b/periscope/dependency/frontend/src/components/theme/theme-provider.tsx
similarity index 100%
rename from frontend/src/components/theme/theme-provider.tsx
rename to periscope/dependency/frontend/src/components/theme/theme-provider.tsx
diff --git a/frontend/src/components/theme/theme-toggle.tsx b/periscope/dependency/frontend/src/components/theme/theme-toggle.tsx
similarity index 100%
rename from frontend/src/components/theme/theme-toggle.tsx
rename to periscope/dependency/frontend/src/components/theme/theme-toggle.tsx
diff --git a/frontend/src/components/ui/alert-dialog.tsx b/periscope/dependency/frontend/src/components/ui/alert-dialog.tsx
similarity index 100%
rename from frontend/src/components/ui/alert-dialog.tsx
rename to periscope/dependency/frontend/src/components/ui/alert-dialog.tsx
diff --git a/frontend/src/components/ui/badge.tsx b/periscope/dependency/frontend/src/components/ui/badge.tsx
similarity index 100%
rename from frontend/src/components/ui/badge.tsx
rename to periscope/dependency/frontend/src/components/ui/badge.tsx
diff --git a/frontend/src/components/ui/button.tsx b/periscope/dependency/frontend/src/components/ui/button.tsx
similarity index 100%
rename from frontend/src/components/ui/button.tsx
rename to periscope/dependency/frontend/src/components/ui/button.tsx
diff --git a/frontend/src/components/ui/card.tsx b/periscope/dependency/frontend/src/components/ui/card.tsx
similarity index 100%
rename from frontend/src/components/ui/card.tsx
rename to periscope/dependency/frontend/src/components/ui/card.tsx
diff --git a/frontend/src/components/ui/collapsible.tsx b/periscope/dependency/frontend/src/components/ui/collapsible.tsx
similarity index 100%
rename from frontend/src/components/ui/collapsible.tsx
rename to periscope/dependency/frontend/src/components/ui/collapsible.tsx
diff --git a/frontend/src/components/ui/dialog.tsx b/periscope/dependency/frontend/src/components/ui/dialog.tsx
similarity index 100%
rename from frontend/src/components/ui/dialog.tsx
rename to periscope/dependency/frontend/src/components/ui/dialog.tsx
diff --git a/frontend/src/components/ui/dropdown-menu.tsx b/periscope/dependency/frontend/src/components/ui/dropdown-menu.tsx
similarity index 100%
rename from frontend/src/components/ui/dropdown-menu.tsx
rename to periscope/dependency/frontend/src/components/ui/dropdown-menu.tsx
diff --git a/frontend/src/components/ui/input.tsx b/periscope/dependency/frontend/src/components/ui/input.tsx
similarity index 100%
rename from frontend/src/components/ui/input.tsx
rename to periscope/dependency/frontend/src/components/ui/input.tsx
diff --git a/frontend/src/components/ui/label.tsx b/periscope/dependency/frontend/src/components/ui/label.tsx
similarity index 100%
rename from frontend/src/components/ui/label.tsx
rename to periscope/dependency/frontend/src/components/ui/label.tsx
diff --git a/frontend/src/components/ui/progress.tsx b/periscope/dependency/frontend/src/components/ui/progress.tsx
similarity index 100%
rename from frontend/src/components/ui/progress.tsx
rename to periscope/dependency/frontend/src/components/ui/progress.tsx
diff --git a/frontend/src/components/ui/scroll-area.tsx b/periscope/dependency/frontend/src/components/ui/scroll-area.tsx
similarity index 100%
rename from frontend/src/components/ui/scroll-area.tsx
rename to periscope/dependency/frontend/src/components/ui/scroll-area.tsx
diff --git a/frontend/src/components/ui/select.tsx b/periscope/dependency/frontend/src/components/ui/select.tsx
similarity index 100%
rename from frontend/src/components/ui/select.tsx
rename to periscope/dependency/frontend/src/components/ui/select.tsx
diff --git a/frontend/src/components/ui/separator.tsx b/periscope/dependency/frontend/src/components/ui/separator.tsx
similarity index 100%
rename from frontend/src/components/ui/separator.tsx
rename to periscope/dependency/frontend/src/components/ui/separator.tsx
diff --git a/frontend/src/components/ui/sheet.tsx b/periscope/dependency/frontend/src/components/ui/sheet.tsx
similarity index 100%
rename from frontend/src/components/ui/sheet.tsx
rename to periscope/dependency/frontend/src/components/ui/sheet.tsx
diff --git a/frontend/src/components/ui/skeleton.tsx b/periscope/dependency/frontend/src/components/ui/skeleton.tsx
similarity index 100%
rename from frontend/src/components/ui/skeleton.tsx
rename to periscope/dependency/frontend/src/components/ui/skeleton.tsx
diff --git a/frontend/src/components/ui/tabs.tsx b/periscope/dependency/frontend/src/components/ui/tabs.tsx
similarity index 100%
rename from frontend/src/components/ui/tabs.tsx
rename to periscope/dependency/frontend/src/components/ui/tabs.tsx
diff --git a/frontend/src/components/ui/textarea.tsx b/periscope/dependency/frontend/src/components/ui/textarea.tsx
similarity index 100%
rename from frontend/src/components/ui/textarea.tsx
rename to periscope/dependency/frontend/src/components/ui/textarea.tsx
diff --git a/frontend/src/components/ui/toast.tsx b/periscope/dependency/frontend/src/components/ui/toast.tsx
similarity index 100%
rename from frontend/src/components/ui/toast.tsx
rename to periscope/dependency/frontend/src/components/ui/toast.tsx
diff --git a/frontend/src/components/ui/tooltip.tsx b/periscope/dependency/frontend/src/components/ui/tooltip.tsx
similarity index 100%
rename from frontend/src/components/ui/tooltip.tsx
rename to periscope/dependency/frontend/src/components/ui/tooltip.tsx
diff --git a/frontend/src/components/upload/file-upload-zone.tsx b/periscope/dependency/frontend/src/components/upload/file-upload-zone.tsx
similarity index 100%
rename from frontend/src/components/upload/file-upload-zone.tsx
rename to periscope/dependency/frontend/src/components/upload/file-upload-zone.tsx
diff --git a/frontend/src/hooks/use-auth-api.ts b/periscope/dependency/frontend/src/hooks/use-auth-api.ts
similarity index 100%
rename from frontend/src/hooks/use-auth-api.ts
rename to periscope/dependency/frontend/src/hooks/use-auth-api.ts
diff --git a/frontend/src/hooks/use-optional-auth.ts b/periscope/dependency/frontend/src/hooks/use-optional-auth.ts
similarity index 100%
rename from frontend/src/hooks/use-optional-auth.ts
rename to periscope/dependency/frontend/src/hooks/use-optional-auth.ts
diff --git a/periscope/dependency/frontend/src/hooks/use-pcb-progress.ts b/periscope/dependency/frontend/src/hooks/use-pcb-progress.ts
new file mode 120000
index 0000000..2dc9413
--- /dev/null
+++ b/periscope/dependency/frontend/src/hooks/use-pcb-progress.ts
@@ -0,0 +1 @@
+../../../../src/frontend/src/hooks/use-pcb-progress.ts
\ No newline at end of file
diff --git a/frontend/src/hooks/use-pipeline-progress.ts b/periscope/dependency/frontend/src/hooks/use-pipeline-progress.ts
similarity index 100%
rename from frontend/src/hooks/use-pipeline-progress.ts
rename to periscope/dependency/frontend/src/hooks/use-pipeline-progress.ts
diff --git a/periscope/dependency/frontend/src/hooks/use-placement-progress.ts b/periscope/dependency/frontend/src/hooks/use-placement-progress.ts
new file mode 120000
index 0000000..074552e
--- /dev/null
+++ b/periscope/dependency/frontend/src/hooks/use-placement-progress.ts
@@ -0,0 +1 @@
+../../../../src/frontend/src/hooks/use-placement-progress.ts
\ No newline at end of file
diff --git a/frontend/src/hooks/use-report.ts b/periscope/dependency/frontend/src/hooks/use-report.ts
similarity index 100%
rename from frontend/src/hooks/use-report.ts
rename to periscope/dependency/frontend/src/hooks/use-report.ts
diff --git a/frontend/src/hooks/use-reviewed-count.ts b/periscope/dependency/frontend/src/hooks/use-reviewed-count.ts
similarity index 100%
rename from frontend/src/hooks/use-reviewed-count.ts
rename to periscope/dependency/frontend/src/hooks/use-reviewed-count.ts
diff --git a/frontend/src/hooks/use-reviewed-findings.ts b/periscope/dependency/frontend/src/hooks/use-reviewed-findings.ts
similarity index 100%
rename from frontend/src/hooks/use-reviewed-findings.ts
rename to periscope/dependency/frontend/src/hooks/use-reviewed-findings.ts
diff --git a/frontend/src/lib/api.ts b/periscope/dependency/frontend/src/lib/api.ts
similarity index 100%
rename from frontend/src/lib/api.ts
rename to periscope/dependency/frontend/src/lib/api.ts
diff --git a/periscope/dependency/frontend/src/lib/auth.ts b/periscope/dependency/frontend/src/lib/auth.ts
new file mode 120000
index 0000000..a4acb28
--- /dev/null
+++ b/periscope/dependency/frontend/src/lib/auth.ts
@@ -0,0 +1 @@
+../../../../src/frontend/src/lib/auth.ts
\ No newline at end of file
diff --git a/frontend/src/lib/csp-hosts.ts b/periscope/dependency/frontend/src/lib/csp-hosts.ts
similarity index 100%
rename from frontend/src/lib/csp-hosts.ts
rename to periscope/dependency/frontend/src/lib/csp-hosts.ts
diff --git a/periscope/dependency/frontend/src/lib/layout-finding.ts b/periscope/dependency/frontend/src/lib/layout-finding.ts
new file mode 120000
index 0000000..bca9f2f
--- /dev/null
+++ b/periscope/dependency/frontend/src/lib/layout-finding.ts
@@ -0,0 +1 @@
+../../../../src/frontend/src/lib/layout-finding.ts
\ No newline at end of file
diff --git a/frontend/src/lib/mock-data.ts b/periscope/dependency/frontend/src/lib/mock-data.ts
similarity index 100%
rename from frontend/src/lib/mock-data.ts
rename to periscope/dependency/frontend/src/lib/mock-data.ts
diff --git a/periscope/dependency/frontend/src/lib/pinscope-compat.ts b/periscope/dependency/frontend/src/lib/pinscope-compat.ts
new file mode 120000
index 0000000..a45829d
--- /dev/null
+++ b/periscope/dependency/frontend/src/lib/pinscope-compat.ts
@@ -0,0 +1 @@
+../../../../src/frontend/src/lib/pinscope-compat.ts
\ No newline at end of file
diff --git a/frontend/src/lib/report-export.ts b/periscope/dependency/frontend/src/lib/report-export.ts
similarity index 100%
rename from frontend/src/lib/report-export.ts
rename to periscope/dependency/frontend/src/lib/report-export.ts
diff --git a/frontend/src/lib/site.ts b/periscope/dependency/frontend/src/lib/site.ts
similarity index 100%
rename from frontend/src/lib/site.ts
rename to periscope/dependency/frontend/src/lib/site.ts
diff --git a/periscope/dependency/frontend/src/lib/storage-keys.ts b/periscope/dependency/frontend/src/lib/storage-keys.ts
new file mode 120000
index 0000000..ba6cceb
--- /dev/null
+++ b/periscope/dependency/frontend/src/lib/storage-keys.ts
@@ -0,0 +1 @@
+../../../../src/frontend/src/lib/storage-keys.ts
\ No newline at end of file
diff --git a/frontend/src/lib/types.ts b/periscope/dependency/frontend/src/lib/types.ts
similarity index 100%
rename from frontend/src/lib/types.ts
rename to periscope/dependency/frontend/src/lib/types.ts
diff --git a/frontend/src/lib/utils.ts b/periscope/dependency/frontend/src/lib/utils.ts
similarity index 100%
rename from frontend/src/lib/utils.ts
rename to periscope/dependency/frontend/src/lib/utils.ts
diff --git a/frontend/src/lib/version.ts b/periscope/dependency/frontend/src/lib/version.ts
similarity index 64%
rename from frontend/src/lib/version.ts
rename to periscope/dependency/frontend/src/lib/version.ts
index 57f543c..0d85ddc 100644
--- a/frontend/src/lib/version.ts
+++ b/periscope/dependency/frontend/src/lib/version.ts
@@ -1,4 +1,4 @@
// AUTO-GENERATED by scripts/sync-version.mjs from content/changelog.md.
// Do not edit by hand — change the top "## X.Y.Z" heading in the changelog.
-export const APP_VERSION = "2.26.3";
-export const APP_VERSION_DATE = "2026-09-11";
+export const APP_VERSION = "2.38.0";
+export const APP_VERSION_DATE = "2026-09-20";
diff --git a/frontend/src/proxy.ts b/periscope/dependency/frontend/src/proxy.ts
similarity index 100%
rename from frontend/src/proxy.ts
rename to periscope/dependency/frontend/src/proxy.ts
diff --git a/frontend/tsconfig.json b/periscope/dependency/frontend/tsconfig.json
similarity index 100%
rename from frontend/tsconfig.json
rename to periscope/dependency/frontend/tsconfig.json
diff --git a/scripts/backfill_passive_subtype.py b/periscope/dependency/scripts/backfill_passive_subtype.py
similarity index 100%
rename from scripts/backfill_passive_subtype.py
rename to periscope/dependency/scripts/backfill_passive_subtype.py
diff --git a/scripts/clear_rules_from_extractions.py b/periscope/dependency/scripts/clear_rules_from_extractions.py
similarity index 100%
rename from scripts/clear_rules_from_extractions.py
rename to periscope/dependency/scripts/clear_rules_from_extractions.py
diff --git a/scripts/dedup_library_datasheets.py b/periscope/dependency/scripts/dedup_library_datasheets.py
similarity index 100%
rename from scripts/dedup_library_datasheets.py
rename to periscope/dependency/scripts/dedup_library_datasheets.py
diff --git a/scripts/gc_orphan_blobs.py b/periscope/dependency/scripts/gc_orphan_blobs.py
similarity index 100%
rename from scripts/gc_orphan_blobs.py
rename to periscope/dependency/scripts/gc_orphan_blobs.py
diff --git a/scripts/migrate_datasheets_to_blobs.py b/periscope/dependency/scripts/migrate_datasheets_to_blobs.py
similarity index 100%
rename from scripts/migrate_datasheets_to_blobs.py
rename to periscope/dependency/scripts/migrate_datasheets_to_blobs.py
diff --git a/scripts/migrate_datasheets_to_library.py b/periscope/dependency/scripts/migrate_datasheets_to_library.py
similarity index 100%
rename from scripts/migrate_datasheets_to_library.py
rename to periscope/dependency/scripts/migrate_datasheets_to_library.py
diff --git a/scripts/upload_skills.py b/periscope/dependency/scripts/upload_skills.py
similarity index 100%
rename from scripts/upload_skills.py
rename to periscope/dependency/scripts/upload_skills.py
diff --git a/simple_project/TI-MSP-KICAD9-TUTORIAL-SCHEMATIC.pdf b/periscope/dependency/simple_project/TI-MSP-KICAD9-TUTORIAL-SCHEMATIC.pdf
similarity index 100%
rename from simple_project/TI-MSP-KICAD9-TUTORIAL-SCHEMATIC.pdf
rename to periscope/dependency/simple_project/TI-MSP-KICAD9-TUTORIAL-SCHEMATIC.pdf
diff --git a/simple_project/TI-MSP-KICAD9-TUTORIAL.asc b/periscope/dependency/simple_project/TI-MSP-KICAD9-TUTORIAL.asc
similarity index 100%
rename from simple_project/TI-MSP-KICAD9-TUTORIAL.asc
rename to periscope/dependency/simple_project/TI-MSP-KICAD9-TUTORIAL.asc
diff --git a/simple_project/TI-MSP-KICAD9-TUTORIAL.csv b/periscope/dependency/simple_project/TI-MSP-KICAD9-TUTORIAL.csv
similarity index 100%
rename from simple_project/TI-MSP-KICAD9-TUTORIAL.csv
rename to periscope/dependency/simple_project/TI-MSP-KICAD9-TUTORIAL.csv
diff --git a/simple_project/design_graph.json b/periscope/dependency/simple_project/design_graph.json
similarity index 100%
rename from simple_project/design_graph.json
rename to periscope/dependency/simple_project/design_graph.json
diff --git a/simple_project/eval_golden.json b/periscope/dependency/simple_project/eval_golden.json
similarity index 100%
rename from simple_project/eval_golden.json
rename to periscope/dependency/simple_project/eval_golden.json
diff --git a/simple_project/smoke_baseline.json b/periscope/dependency/simple_project/smoke_baseline.json
similarity index 100%
rename from simple_project/smoke_baseline.json
rename to periscope/dependency/simple_project/smoke_baseline.json
diff --git a/skills/extract-pattern/SKILL.md b/periscope/dependency/skills/extract-pattern/SKILL.md
similarity index 100%
rename from skills/extract-pattern/SKILL.md
rename to periscope/dependency/skills/extract-pattern/SKILL.md
diff --git a/skills/extract-pattern/schema.json b/periscope/dependency/skills/extract-pattern/schema.json
similarity index 100%
rename from skills/extract-pattern/schema.json
rename to periscope/dependency/skills/extract-pattern/schema.json
diff --git a/skills/extract-pattern/validate.py b/periscope/dependency/skills/extract-pattern/validate.py
similarity index 100%
rename from skills/extract-pattern/validate.py
rename to periscope/dependency/skills/extract-pattern/validate.py
diff --git a/skills/extract-pintable/SKILL.md b/periscope/dependency/skills/extract-pintable/SKILL.md
similarity index 100%
rename from skills/extract-pintable/SKILL.md
rename to periscope/dependency/skills/extract-pintable/SKILL.md
diff --git a/skills/extract-pintable/schema.json b/periscope/dependency/skills/extract-pintable/schema.json
similarity index 100%
rename from skills/extract-pintable/schema.json
rename to periscope/dependency/skills/extract-pintable/schema.json
diff --git a/skills/extract-pintable/validate.py b/periscope/dependency/skills/extract-pintable/validate.py
similarity index 100%
rename from skills/extract-pintable/validate.py
rename to periscope/dependency/skills/extract-pintable/validate.py
diff --git a/skills/extract-specs/SKILL.md b/periscope/dependency/skills/extract-specs/SKILL.md
similarity index 100%
rename from skills/extract-specs/SKILL.md
rename to periscope/dependency/skills/extract-specs/SKILL.md
diff --git a/skills/extract-specs/schema.json b/periscope/dependency/skills/extract-specs/schema.json
similarity index 100%
rename from skills/extract-specs/schema.json
rename to periscope/dependency/skills/extract-specs/schema.json
diff --git a/skills/extract-specs/validate.py b/periscope/dependency/skills/extract-specs/validate.py
similarity index 100%
rename from skills/extract-specs/validate.py
rename to periscope/dependency/skills/extract-specs/validate.py
diff --git a/taxonomy/connector.json b/periscope/dependency/taxonomy/connector.json
similarity index 100%
rename from taxonomy/connector.json
rename to periscope/dependency/taxonomy/connector.json
diff --git a/taxonomy/crystal.json b/periscope/dependency/taxonomy/crystal.json
similarity index 100%
rename from taxonomy/crystal.json
rename to periscope/dependency/taxonomy/crystal.json
diff --git a/taxonomy/discrete.json b/periscope/dependency/taxonomy/discrete.json
similarity index 100%
rename from taxonomy/discrete.json
rename to periscope/dependency/taxonomy/discrete.json
diff --git a/taxonomy/fuse.json b/periscope/dependency/taxonomy/fuse.json
similarity index 100%
rename from taxonomy/fuse.json
rename to periscope/dependency/taxonomy/fuse.json
diff --git a/taxonomy/ic.json b/periscope/dependency/taxonomy/ic.json
similarity index 100%
rename from taxonomy/ic.json
rename to periscope/dependency/taxonomy/ic.json
diff --git a/taxonomy/passive.json b/periscope/dependency/taxonomy/passive.json
similarity index 100%
rename from taxonomy/passive.json
rename to periscope/dependency/taxonomy/passive.json
diff --git a/taxonomy/switch.json b/periscope/dependency/taxonomy/switch.json
similarity index 100%
rename from taxonomy/switch.json
rename to periscope/dependency/taxonomy/switch.json
diff --git a/taxonomy/test_point.json b/periscope/dependency/taxonomy/test_point.json
similarity index 100%
rename from taxonomy/test_point.json
rename to periscope/dependency/taxonomy/test_point.json
diff --git a/taxonomy/transformer.json b/periscope/dependency/taxonomy/transformer.json
similarity index 100%
rename from taxonomy/transformer.json
rename to periscope/dependency/taxonomy/transformer.json
diff --git a/periscope/src/backend/Dockerfile b/periscope/src/backend/Dockerfile
new file mode 100644
index 0000000..c3e9ec3
--- /dev/null
+++ b/periscope/src/backend/Dockerfile
@@ -0,0 +1,23 @@
+FROM python:3.12-slim
+RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /app
+
+# Overlay: PinScope inherited tree, then native Periscope modules.
+COPY periscope/dependency/backend/requirements*.txt /app/backend/
+RUN pip install --no-cache-dir -r /app/backend/requirements.txt \
+ && if [ -f /app/backend/requirements-gateway.txt ]; then \
+ pip install --no-cache-dir -r /app/backend/requirements-gateway.txt; \
+ fi
+
+COPY periscope/dependency/backend/ /app/backend/
+COPY periscope/src/backend/ /app/backend/
+
+COPY periscope/dependency/taxonomy/ /app/taxonomy/
+COPY periscope/dependency/skills/ /app/skills/
+COPY periscope/dependency/frontend/content/changelog.md /app/changelog.md
+COPY vendor/ /app/vendor/
+
+EXPOSE 8080
+
+CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8080"]
diff --git a/periscope/src/backend/__init__.py b/periscope/src/backend/__init__.py
new file mode 100644
index 0000000..ee69a73
--- /dev/null
+++ b/periscope/src/backend/__init__.py
@@ -0,0 +1,4 @@
+"""Native Periscope backend modules."""
+from pkgutil import extend_path
+
+__path__ = extend_path(__path__, __name__)
diff --git a/backend/_version.py b/periscope/src/backend/_version.py
similarity index 83%
rename from backend/_version.py
rename to periscope/src/backend/_version.py
index 9a24590..6676720 100644
--- a/backend/_version.py
+++ b/periscope/src/backend/_version.py
@@ -12,11 +12,9 @@ from pathlib import Path
def _candidate_paths() -> list[Path]:
- here = Path(__file__).resolve()
- return [
- Path("/app/changelog.md"),
- here.parent.parent / "frontend" / "content" / "changelog.md",
- ]
+ from backend.repo_paths import changelog_paths
+
+ return changelog_paths()
_VERSION_RE = re.compile(r"^##\s+(\d+\.\d+\.\d+)\b", re.MULTILINE)
diff --git a/periscope/src/backend/periscopex/__init__.py b/periscope/src/backend/periscopex/__init__.py
new file mode 100644
index 0000000..c503d2f
--- /dev/null
+++ b/periscope/src/backend/periscopex/__init__.py
@@ -0,0 +1,4 @@
+"""Native Periscope core modules (finding engine, PCB, checks)."""
+from pkgutil import extend_path
+
+__path__ = extend_path(__path__, __name__)
diff --git a/backend/periscopex/antenna_geometry.py b/periscope/src/backend/periscopex/antenna_geometry.py
similarity index 100%
rename from backend/periscopex/antenna_geometry.py
rename to periscope/src/backend/periscopex/antenna_geometry.py
diff --git a/backend/periscopex/antenna_rf.py b/periscope/src/backend/periscopex/antenna_rf.py
similarity index 100%
rename from backend/periscopex/antenna_rf.py
rename to periscope/src/backend/periscopex/antenna_rf.py
diff --git a/backend/periscopex/bom_match_check.py b/periscope/src/backend/periscopex/bom_match_check.py
similarity index 100%
rename from backend/periscopex/bom_match_check.py
rename to periscope/src/backend/periscopex/bom_match_check.py
diff --git a/backend/periscopex/bom_pcb_check.py b/periscope/src/backend/periscopex/bom_pcb_check.py
similarity index 100%
rename from backend/periscopex/bom_pcb_check.py
rename to periscope/src/backend/periscopex/bom_pcb_check.py
diff --git a/backend/periscopex/cad_bridge.py b/periscope/src/backend/periscopex/cad_bridge.py
similarity index 100%
rename from backend/periscopex/cad_bridge.py
rename to periscope/src/backend/periscopex/cad_bridge.py
diff --git a/backend/periscopex/crystal_cl_check.py b/periscope/src/backend/periscopex/crystal_cl_check.py
similarity index 100%
rename from backend/periscopex/crystal_cl_check.py
rename to periscope/src/backend/periscopex/crystal_cl_check.py
diff --git a/backend/periscopex/dnp_check.py b/periscope/src/backend/periscopex/dnp_check.py
similarity index 100%
rename from backend/periscopex/dnp_check.py
rename to periscope/src/backend/periscopex/dnp_check.py
diff --git a/backend/periscopex/emi_check.py b/periscope/src/backend/periscopex/emi_check.py
similarity index 100%
rename from backend/periscopex/emi_check.py
rename to periscope/src/backend/periscopex/emi_check.py
diff --git a/backend/periscopex/errata_check.py b/periscope/src/backend/periscopex/errata_check.py
similarity index 100%
rename from backend/periscopex/errata_check.py
rename to periscope/src/backend/periscopex/errata_check.py
diff --git a/backend/periscopex/esd_return_check.py b/periscope/src/backend/periscopex/esd_return_check.py
similarity index 100%
rename from backend/periscopex/esd_return_check.py
rename to periscope/src/backend/periscopex/esd_return_check.py
diff --git a/backend/periscopex/eval_report.py b/periscope/src/backend/periscopex/eval_report.py
similarity index 100%
rename from backend/periscopex/eval_report.py
rename to periscope/src/backend/periscopex/eval_report.py
diff --git a/backend/periscopex/filter_check.py b/periscope/src/backend/periscopex/filter_check.py
similarity index 100%
rename from backend/periscopex/filter_check.py
rename to periscope/src/backend/periscopex/filter_check.py
diff --git a/backend/periscopex/finding_engine.py b/periscope/src/backend/periscopex/finding_engine.py
similarity index 100%
rename from backend/periscopex/finding_engine.py
rename to periscope/src/backend/periscopex/finding_engine.py
diff --git a/backend/periscopex/functional_groups.py b/periscope/src/backend/periscopex/functional_groups.py
similarity index 100%
rename from backend/periscopex/functional_groups.py
rename to periscope/src/backend/periscopex/functional_groups.py
diff --git a/backend/periscopex/hf_coverage_check.py b/periscope/src/backend/periscopex/hf_coverage_check.py
similarity index 100%
rename from backend/periscopex/hf_coverage_check.py
rename to periscope/src/backend/periscopex/hf_coverage_check.py
diff --git a/backend/periscopex/hierarchy.py b/periscope/src/backend/periscopex/hierarchy.py
similarity index 100%
rename from backend/periscopex/hierarchy.py
rename to periscope/src/backend/periscopex/hierarchy.py
diff --git a/backend/periscopex/impedance.py b/periscope/src/backend/periscopex/impedance.py
similarity index 100%
rename from backend/periscopex/impedance.py
rename to periscope/src/backend/periscopex/impedance.py
diff --git a/backend/periscopex/impedance_traces.py b/periscope/src/backend/periscopex/impedance_traces.py
similarity index 100%
rename from backend/periscopex/impedance_traces.py
rename to periscope/src/backend/periscopex/impedance_traces.py
diff --git a/backend/periscopex/internal_features_check.py b/periscope/src/backend/periscopex/internal_features_check.py
similarity index 100%
rename from backend/periscopex/internal_features_check.py
rename to periscope/src/backend/periscopex/internal_features_check.py
diff --git a/backend/periscopex/layout_rules.py b/periscope/src/backend/periscopex/layout_rules.py
similarity index 100%
rename from backend/periscopex/layout_rules.py
rename to periscope/src/backend/periscopex/layout_rules.py
diff --git a/backend/periscopex/library_gate.py b/periscope/src/backend/periscopex/library_gate.py
similarity index 100%
rename from backend/periscopex/library_gate.py
rename to periscope/src/backend/periscopex/library_gate.py
diff --git a/backend/periscopex/lifecycle.py b/periscope/src/backend/periscopex/lifecycle.py
similarity index 100%
rename from backend/periscopex/lifecycle.py
rename to periscope/src/backend/periscopex/lifecycle.py
diff --git a/backend/periscopex/nc_pin_check.py b/periscope/src/backend/periscopex/nc_pin_check.py
similarity index 100%
rename from backend/periscopex/nc_pin_check.py
rename to periscope/src/backend/periscopex/nc_pin_check.py
diff --git a/backend/periscopex/netlist_bundle.py b/periscope/src/backend/periscopex/netlist_bundle.py
similarity index 100%
rename from backend/periscopex/netlist_bundle.py
rename to periscope/src/backend/periscopex/netlist_bundle.py
diff --git a/backend/periscopex/parsers_kicad.py b/periscope/src/backend/periscopex/parsers_kicad.py
similarity index 100%
rename from backend/periscopex/parsers_kicad.py
rename to periscope/src/backend/periscopex/parsers_kicad.py
diff --git a/backend/periscopex/parsers_kicad_pcb.py b/periscope/src/backend/periscopex/parsers_kicad_pcb.py
similarity index 100%
rename from backend/periscopex/parsers_kicad_pcb.py
rename to periscope/src/backend/periscopex/parsers_kicad_pcb.py
diff --git a/backend/periscopex/passive_rail_check.py b/periscope/src/backend/periscopex/passive_rail_check.py
similarity index 100%
rename from backend/periscopex/passive_rail_check.py
rename to periscope/src/backend/periscopex/passive_rail_check.py
diff --git a/backend/periscopex/pcb_checks.py b/periscope/src/backend/periscopex/pcb_checks.py
similarity index 100%
rename from backend/periscopex/pcb_checks.py
rename to periscope/src/backend/periscopex/pcb_checks.py
diff --git a/backend/periscopex/pcb_inventory.py b/periscope/src/backend/periscopex/pcb_inventory.py
similarity index 100%
rename from backend/periscopex/pcb_inventory.py
rename to periscope/src/backend/periscopex/pcb_inventory.py
diff --git a/backend/periscopex/pcb_net_match.py b/periscope/src/backend/periscopex/pcb_net_match.py
similarity index 100%
rename from backend/periscopex/pcb_net_match.py
rename to periscope/src/backend/periscopex/pcb_net_match.py
diff --git a/backend/periscopex/pcb_power_thermal.py b/periscope/src/backend/periscopex/pcb_power_thermal.py
similarity index 100%
rename from backend/periscopex/pcb_power_thermal.py
rename to periscope/src/backend/periscopex/pcb_power_thermal.py
diff --git a/backend/periscopex/pcb_review.py b/periscope/src/backend/periscopex/pcb_review.py
similarity index 100%
rename from backend/periscopex/pcb_review.py
rename to periscope/src/backend/periscopex/pcb_review.py
diff --git a/backend/periscopex/pdf_text.py b/periscope/src/backend/periscopex/pdf_text.py
similarity index 100%
rename from backend/periscopex/pdf_text.py
rename to periscope/src/backend/periscopex/pdf_text.py
diff --git a/backend/periscopex/pi_check.py b/periscope/src/backend/periscopex/pi_check.py
similarity index 100%
rename from backend/periscopex/pi_check.py
rename to periscope/src/backend/periscopex/pi_check.py
diff --git a/backend/periscopex/placement_check.py b/periscope/src/backend/periscopex/placement_check.py
similarity index 100%
rename from backend/periscopex/placement_check.py
rename to periscope/src/backend/periscopex/placement_check.py
diff --git a/backend/periscopex/placement_pack.py b/periscope/src/backend/periscopex/placement_pack.py
similarity index 100%
rename from backend/periscopex/placement_pack.py
rename to periscope/src/backend/periscopex/placement_pack.py
diff --git a/backend/periscopex/power_margin_check.py b/periscope/src/backend/periscopex/power_margin_check.py
similarity index 100%
rename from backend/periscopex/power_margin_check.py
rename to periscope/src/backend/periscopex/power_margin_check.py
diff --git a/backend/periscopex/quote_verify.py b/periscope/src/backend/periscopex/quote_verify.py
similarity index 100%
rename from backend/periscopex/quote_verify.py
rename to periscope/src/backend/periscopex/quote_verify.py
diff --git a/backend/periscopex/review_fingerprint.py b/periscope/src/backend/periscopex/review_fingerprint.py
similarity index 100%
rename from backend/periscopex/review_fingerprint.py
rename to periscope/src/backend/periscopex/review_fingerprint.py
diff --git a/backend/periscopex/review_workflow.py b/periscope/src/backend/periscopex/review_workflow.py
similarity index 100%
rename from backend/periscopex/review_workflow.py
rename to periscope/src/backend/periscopex/review_workflow.py
diff --git a/backend/periscopex/sequencing_check.py b/periscope/src/backend/periscopex/sequencing_check.py
similarity index 100%
rename from backend/periscopex/sequencing_check.py
rename to periscope/src/backend/periscopex/sequencing_check.py
diff --git a/backend/periscopex/si_check.py b/periscope/src/backend/periscopex/si_check.py
similarity index 100%
rename from backend/periscopex/si_check.py
rename to periscope/src/backend/periscopex/si_check.py
diff --git a/backend/periscopex/spof_check.py b/periscope/src/backend/periscopex/spof_check.py
similarity index 100%
rename from backend/periscopex/spof_check.py
rename to periscope/src/backend/periscopex/spof_check.py
diff --git a/backend/periscopex/thermal_check.py b/periscope/src/backend/periscopex/thermal_check.py
similarity index 100%
rename from backend/periscopex/thermal_check.py
rename to periscope/src/backend/periscopex/thermal_check.py
diff --git a/backend/periscopex/timing_check.py b/periscope/src/backend/periscopex/timing_check.py
similarity index 100%
rename from backend/periscopex/timing_check.py
rename to periscope/src/backend/periscopex/timing_check.py
diff --git a/backend/pinscope_compat.py b/periscope/src/backend/pinscope_compat.py
similarity index 100%
rename from backend/pinscope_compat.py
rename to periscope/src/backend/pinscope_compat.py
diff --git a/periscope/src/backend/repo_paths.py b/periscope/src/backend/repo_paths.py
new file mode 100644
index 0000000..34da1d0
--- /dev/null
+++ b/periscope/src/backend/repo_paths.py
@@ -0,0 +1,89 @@
+"""Locate the git root vs the Docker merged image layout.
+
+Physical split:
+ periscope/src native Periscope
+ periscope/dependency inherited PinScope (in-tree dependency)
+
+Docker overlays both trees into /app/backend, /app/taxonomy, /app/skills.
+"""
+from __future__ import annotations
+
+from functools import lru_cache
+from pathlib import Path
+
+
+@lru_cache(maxsize=1)
+def repo_root() -> Path:
+ here = Path(__file__).resolve()
+ for p in [here, *here.parents]:
+ if (p / "LICENSE").is_file() and (p / "periscope" / "dependency").is_dir():
+ return p
+ if (
+ (p / "backend").is_dir()
+ and (p / "taxonomy").is_dir()
+ and (p / "skills").is_dir()
+ and not (p / "periscope" / "src").is_dir()
+ ):
+ return p
+ return here.parents[3] if len(here.parents) >= 4 else here.parent.parent
+
+
+def _split() -> bool:
+ return (repo_root() / "periscope" / "dependency").is_dir()
+
+
+def dependency_root() -> Path:
+ root = repo_root()
+ if _split():
+ return root / "periscope" / "dependency"
+ return root
+
+
+def src_root() -> Path:
+ root = repo_root()
+ if _split():
+ return root / "periscope" / "src"
+ return root
+
+
+def taxonomy_dir() -> Path:
+ return dependency_root() / "taxonomy"
+
+
+def skills_dir() -> Path:
+ return dependency_root() / "skills"
+
+
+def simple_project_dir() -> Path:
+ return dependency_root() / "simple_project"
+
+
+def data_dir() -> Path:
+ return repo_root() / "data"
+
+
+def vendor_dir() -> Path:
+ return repo_root() / "vendor"
+
+
+def changelog_paths() -> list[Path]:
+ dep = dependency_root()
+ src = src_root()
+ return [
+ Path("/app/changelog.md"),
+ dep / "frontend" / "content" / "changelog.md",
+ src / "frontend" / "content" / "changelog.md",
+ repo_root() / "frontend" / "content" / "changelog.md",
+ ]
+
+
+def env_file() -> Path:
+ root = repo_root()
+ for candidate in (
+ root / ".env",
+ dependency_root() / "backend" / ".env",
+ src_root() / "backend" / ".env",
+ ):
+ if candidate.is_file():
+ return candidate
+ return root / ".env"
diff --git a/periscope/src/backend/routers/__init__.py b/periscope/src/backend/routers/__init__.py
new file mode 100644
index 0000000..b36383a
--- /dev/null
+++ b/periscope/src/backend/routers/__init__.py
@@ -0,0 +1,3 @@
+from pkgutil import extend_path
+
+__path__ = extend_path(__path__, __name__)
diff --git a/backend/routers/auth.py b/periscope/src/backend/routers/auth.py
similarity index 100%
rename from backend/routers/auth.py
rename to periscope/src/backend/routers/auth.py
diff --git a/backend/routers/impedance.py b/periscope/src/backend/routers/impedance.py
similarity index 100%
rename from backend/routers/impedance.py
rename to periscope/src/backend/routers/impedance.py
diff --git a/periscope/src/backend/services/__init__.py b/periscope/src/backend/services/__init__.py
new file mode 100644
index 0000000..b36383a
--- /dev/null
+++ b/periscope/src/backend/services/__init__.py
@@ -0,0 +1,3 @@
+from pkgutil import extend_path
+
+__path__ = extend_path(__path__, __name__)
diff --git a/backend/services/datasheet_finder.py b/periscope/src/backend/services/datasheet_finder.py
similarity index 100%
rename from backend/services/datasheet_finder.py
rename to periscope/src/backend/services/datasheet_finder.py
diff --git a/periscope/src/backend/services/llm/__init__.py b/periscope/src/backend/services/llm/__init__.py
new file mode 100644
index 0000000..fb5cea5
--- /dev/null
+++ b/periscope/src/backend/services/llm/__init__.py
@@ -0,0 +1,33 @@
+"""Native LLM pieces (DeepSeek, local skills, PDF ingest) merged with PinScope providers."""
+from pkgutil import extend_path
+
+__path__ = extend_path(__path__, __name__)
+
+from backend.services.llm.factory import call_with_fallback, get_provider
+from backend.services.llm.types import (
+ Completion,
+ ContentBlock,
+ Message,
+ PdfBlock,
+ TextBlock,
+ ToolCall,
+ ToolChoice,
+ ToolResultBlock,
+ ToolSchema,
+ Usage,
+)
+
+__all__ = [
+ "Completion",
+ "ContentBlock",
+ "Message",
+ "PdfBlock",
+ "TextBlock",
+ "ToolCall",
+ "ToolChoice",
+ "ToolResultBlock",
+ "ToolSchema",
+ "Usage",
+ "call_with_fallback",
+ "get_provider",
+]
diff --git a/backend/services/llm/deepseek_provider.py b/periscope/src/backend/services/llm/deepseek_provider.py
similarity index 100%
rename from backend/services/llm/deepseek_provider.py
rename to periscope/src/backend/services/llm/deepseek_provider.py
diff --git a/backend/services/llm/local_skill.py b/periscope/src/backend/services/llm/local_skill.py
similarity index 100%
rename from backend/services/llm/local_skill.py
rename to periscope/src/backend/services/llm/local_skill.py
diff --git a/backend/services/llm/pdf_ingest.py b/periscope/src/backend/services/llm/pdf_ingest.py
similarity index 100%
rename from backend/services/llm/pdf_ingest.py
rename to periscope/src/backend/services/llm/pdf_ingest.py
diff --git a/backend/services/local_jwt.py b/periscope/src/backend/services/local_jwt.py
similarity index 100%
rename from backend/services/local_jwt.py
rename to periscope/src/backend/services/local_jwt.py
diff --git a/backend/services/local_users.py b/periscope/src/backend/services/local_users.py
similarity index 100%
rename from backend/services/local_users.py
rename to periscope/src/backend/services/local_users.py
diff --git a/backend/services/mouser.py b/periscope/src/backend/services/mouser.py
similarity index 100%
rename from backend/services/mouser.py
rename to periscope/src/backend/services/mouser.py
diff --git a/backend/services/passive_from_distributor.py b/periscope/src/backend/services/passive_from_distributor.py
similarity index 100%
rename from backend/services/passive_from_distributor.py
rename to periscope/src/backend/services/passive_from_distributor.py
diff --git a/backend/services/passive_from_mpn.py b/periscope/src/backend/services/passive_from_mpn.py
similarity index 100%
rename from backend/services/passive_from_mpn.py
rename to periscope/src/backend/services/passive_from_mpn.py
diff --git a/backend/services/passive_from_value.py b/periscope/src/backend/services/passive_from_value.py
similarity index 100%
rename from backend/services/passive_from_value.py
rename to periscope/src/backend/services/passive_from_value.py
diff --git a/backend/services/pcb_pipeline.py b/periscope/src/backend/services/pcb_pipeline.py
similarity index 100%
rename from backend/services/pcb_pipeline.py
rename to periscope/src/backend/services/pcb_pipeline.py
diff --git a/backend/services/pcb_validation.py b/periscope/src/backend/services/pcb_validation.py
similarity index 100%
rename from backend/services/pcb_validation.py
rename to periscope/src/backend/services/pcb_validation.py
diff --git a/backend/services/placement_pipeline.py b/periscope/src/backend/services/placement_pipeline.py
similarity index 100%
rename from backend/services/placement_pipeline.py
rename to periscope/src/backend/services/placement_pipeline.py
diff --git a/backend/services/user_directory.py b/periscope/src/backend/services/user_directory.py
similarity index 100%
rename from backend/services/user_directory.py
rename to periscope/src/backend/services/user_directory.py
diff --git a/backend/vendor_path.py b/periscope/src/backend/vendor_path.py
similarity index 74%
rename from backend/vendor_path.py
rename to periscope/src/backend/vendor_path.py
index dc31989..b3ebfef 100644
--- a/backend/vendor_path.py
+++ b/periscope/src/backend/vendor_path.py
@@ -3,9 +3,10 @@
from __future__ import annotations
import sys
-from pathlib import Path
-VENDOR_DIR = Path(__file__).resolve().parents[1] / "vendor"
+from backend.repo_paths import vendor_dir
+
+VENDOR_DIR = vendor_dir()
def ensure_impedancefinder() -> None:
diff --git a/docs/development/PINSCOPE_AUDIT.md b/periscope/src/docs/development/PINSCOPE_AUDIT.md
similarity index 100%
rename from docs/development/PINSCOPE_AUDIT.md
rename to periscope/src/docs/development/PINSCOPE_AUDIT.md
diff --git a/periscope/src/docs/development/PINSCOPE_FENCE.md b/periscope/src/docs/development/PINSCOPE_FENCE.md
new file mode 100644
index 0000000..b94742f
--- /dev/null
+++ b/periscope/src/docs/development/PINSCOPE_FENCE.md
@@ -0,0 +1,38 @@
+# Recinto PinScope
+
+`LICENSE` AGPL-3.0 is unchanged at the git root. The GitHub fork `manvalan/periscope` → `Faradworks/Pinscope` is **not** detached.
+
+## Physical trees (2.38.0)
+
+| Tree | Path |
+| --- | --- |
+| Native Periscope | `periscope/src/` |
+| Inherited PinScope (in-tree dependency) | `periscope/dependency/` |
+| Third party RF | `vendor/impedancefinder/` |
+
+**Never empty-delete `periscope/dependency/`.** Replace later by adding modules under `periscope/src` (Fase C) and stopping calls into the inherited file.
+
+## Inherited (KEEP)
+
+- `periscope/dependency/backend/periscopex/{graph,models,parsers*,validate,validation_tools,resolve_passives,derating,bom_summary,taxonomy,pin_mux_check,led_current_check,pin_function_tokens}`
+- `periscope/dependency/backend/services/pipeline.py`, `pipeline_worker.py`, `validation.py`, `extraction.py`
+- `periscope/dependency/skills/`, `periscope/dependency/scripts/upload_skills.py`
+- Open-core Clerk/billing stubs under dependency frontend/backend
+- `periscope/dependency/simple_project/`, `periscope/dependency/docs/how-it-works.svg`
+
+Native (do not fold into the fence): `finding_engine.py`, PCB/placement/antenna, DeepSeek, local JWT, KiCad plugin — all under `periscope/src`.
+
+## Write vs read (identifiers)
+
+| Write (native) | Read-only / migrate |
+| --- | --- |
+| `periscope_token` | `pinscope_token` |
+| `periscopex:*` localStorage | `pinscopex:*` |
+| JWT `iss=periscope-local` | also accept `pinscope-local` |
+| `periscope_version` in `project.json` | also accept `pinscope_version` |
+
+Code: `periscope/src/frontend/src/lib/pinscope-compat.ts`, `periscope/src/backend/pinscope_compat.py`.
+
+## Out of scope here
+
+Clerk/Anthropic runtime isolation (B). Rewrite of `validate.py` / finding engine (C). UI FindingCard (D). Parsers/graph (E). Fork detach (legal). Auto-place.
diff --git a/docs/development/PINSCOPE_INDEPENDENCE_PLAN.md b/periscope/src/docs/development/PINSCOPE_INDEPENDENCE_PLAN.md
similarity index 81%
rename from docs/development/PINSCOPE_INDEPENDENCE_PLAN.md
rename to periscope/src/docs/development/PINSCOPE_INDEPENDENCE_PLAN.md
index aa5c8a2..d24235d 100644
--- a/docs/development/PINSCOPE_INDEPENDENCE_PLAN.md
+++ b/periscope/src/docs/development/PINSCOPE_INDEPENDENCE_PLAN.md
@@ -1,8 +1,7 @@
# Piano — indipendenza architettonica e di licenza da PinScope
-**Stato:** solo piano. Nessuna modifica a source di prodotto, remotes, `LICENSE`, né git history.
-**Base:** [PINSCOPE_AUDIT.md](./PINSCOPE_AUDIT.md) (snapshot prodotto `facbaa2` / `cursor/pcb-review-plan-44dd`).
-**Intento di Michele:** *lasciare* il codice PinScope e le sue licenze; **isolare** l’albero ereditato; disegnare dipendenze; marcare cosa **deve** essere riscritto. Non cancellare ogni riga ereditata.
+**Stato:** split fisico shipped in **2.38.0**. Fork GitHub non staccato; `validate.py` non riscritto.
+**Sequenza confermata:** (1) split `periscope/src` vs `periscope/dependency/` — **questa fase**; (2) **poi** sostituzione incrementale dei moduli PinScope (Fase C). **Mai** svuotare o cancellare l’albero ereditato.
Questo piano **non** stacca il fork GitHub (`manvalan/periscope` ← `Faradworks/Pinscope`). Lo stacco è un passo legale successivo, fuori da queste fasi di lavoro, salvo decisione esplicita.
@@ -29,38 +28,39 @@ L’audit già dice: l’indipendenza **non** è bloccata dalle stringhe `pinsco
Trattare come **una dipendenza in-tree**, non come prodotto Periscope:
-| Pacchetto logico | Path odierni | Licenza da audit |
+| Pacchetto logico | Path **dopo lo split** | Licenza da audit |
| --- | --- | --- |
-| Core schematico PinScope | `backend/periscopex/{graph,models,parsers,parsers_edif,validate,validation_tools,resolve_passives,derating,bom_summary,taxonomy,pin_mux_check,led_current_check,pin_function_tokens}.py` | AGPL-3.0 del fork (blob `LICENSE` identico a upstream) |
-| Orchestrazione review | `backend/services/pipeline.py`, `pipeline_worker.py`, `services/validation.py`, `services/extraction.py` (parte DIRECT) | stessa |
-| Skills Anthropic/Console | `skills/extract-pattern`, `skills/extract-specs`, `skills/extract-pintable/{schema.json,validate.py}`, `scripts/upload_skills.py`, `backend/skills_manifest.json` | stessa + contratto Claude |
-| UI OSS / marketing shell | gran parte di `frontend/src` UPSTREAM/DERIVED, `frontend/public`, `frontend/content/terms.md` + `privacy.md` (testo Faradworks) | AGPL + TOS Faradworks **nel contenuto** |
-| Gateway seams | `billing_hook.py`, `proxy.ts`, `clerk-theme-provider.tsx`, `use-optional-auth.ts`, analytics, CSP hosts | stub open-core PinScope |
-| Fixture upstream | `simple_project/` (asc/csv/pdf/`design_graph.json`), `docs/how-it-works.svg` | AGPL / contenuto upstream |
+| Core schematico PinScope | `periscope/dependency/backend/periscopex/{graph,models,parsers,parsers_edif,validate,validation_tools,resolve_passives,derating,bom_summary,taxonomy,pin_mux_check,led_current_check,pin_function_tokens}.py` | AGPL-3.0 del fork (blob `LICENSE` identico a upstream) |
+| Orchestrazione review | `periscope/dependency/backend/services/pipeline.py`, `pipeline_worker.py`, `validation.py`, `extraction.py` (DIRECT) | stessa |
+| Skills Anthropic/Console | `periscope/dependency/skills/…`, `periscope/dependency/scripts/upload_skills.py`, `periscope/dependency/backend/skills_manifest.json` | stessa + contratto Claude |
+| UI OSS / marketing shell | `periscope/dependency/frontend/` (UPSTREAM/DERIVED); file nativi in `periscope/src/frontend/` con symlink nel recinto | AGPL |
+| Gateway seams | `billing_hook.py`, `proxy.ts`, `clerk-theme-provider.tsx`, … sotto `periscope/dependency/` | stub open-core PinScope |
+| Fixture upstream | `periscope/dependency/simple_project/`, `periscope/dependency/docs/how-it-works.svg` | AGPL / contenuto upstream |
-**Regola del recinto (quando si implementerà, non ora):**
+**Regola del recinto:**
- Import **solo** da una facciata stretta (es. `build_graph`, `parse_bom`, `parse_netlist_any`, `Finding` Pydantic, `validate_design_async`).
- Nessun nuovo check PCB/schema scrive dentro `validate.py`.
- Nessun file nativo assume Clerk, Anthropic Console, `pinscope.ai`, Faradworks Inc.
- Il recinto **conserva** `LICENSE` AGPL e l’attribuzione “derived from Faradworks/Pinscope”.
+- **Non cancellare** `periscope/dependency/` quando un modulo viene sostituito: si aggiunge il sostituto in `periscope/src` e si smette di *chiamare* il vecchio file. Empty-delete è fuori piano.
-Implementazione fisica futura (non in fase identità): directory tipo `vendor/pinscope/` **oppure** package `backend/pinscope_legacy/` con shim `periscopex` — solo dopo che i consumer nativi non importano più path sparsi. Non è un rename cosmetico (`pinscopex` → `periscopex` è già successo e **non** è un recinto).
+**Split fisico (2.38.0):** `periscope/src` (nativo) + `periscope/dependency/` (PinScope in-tree). Docker overlay: dependency poi src. Glue locale: `backend/__init__.py`. Non è un rename cosmetico (`pinscopex` → `periscopex` **non** era un recinto).
### 2.2 Nativo Periscope (fuori recinto)
| Area | Path | Perché è nativo |
| --- | --- | --- |
-| Contratto finding | `finding_engine.py`, `docs/motore-finding.md` | NEW; clamp FACT/REQUIREMENT/INFERENCE |
-| PCB exam | `pcb_pipeline.py`, `pcb_*.py`, `parsers_kicad_pcb.py`, UI `pcb/` | NEW; INDIRECT sul grafo |
-| Placement F2 | `placement_pipeline.py`, `placement_pack.py`, `functional_groups.py` | NEW |
-| RF / antenna | `antenna_*.py`, parte Impedance UI | NEW |
-| Facade Z0 | `periscopex/impedance.py` | NEW; il solver è terzi |
-| Auth self-host | `local_jwt.py`, `local_users.py`, `routers/auth.py` | REPLACEMENT |
-| LLM DeepSeek | `deepseek_provider.py`, `local_skill.py`, `pdf_ingest.py` | NEW |
-| Deploy | `scripts/update-periscope.sh`, `docker-compose.yml` | NEW (nomi `pinscope_*` ancora WEAK) |
-| Plugin KiCad | `plugins/kicad/` | NEW |
-| Check deterministici fork | `dnp_check`, `sequencing_check`, `layout_rules`, … | NEW ma **INDIRECT**: usano `models` / graph |
+| Contratto finding | `periscope/src/backend/periscopex/finding_engine.py`, `periscope/src/docs/motore-finding.md` | NEW; clamp FACT/REQUIREMENT/INFERENCE |
+| PCB exam | `periscope/src/backend/…/pcb_*.py`, UI `periscope/src/frontend/…/pcb/` | NEW; INDIRECT sul grafo |
+| Placement F2 | `periscope/src/backend/…/placement_*.py`, `functional_groups.py` | NEW |
+| RF / antenna | `periscope/src/backend/periscopex/antenna_*.py` | NEW |
+| Facade Z0 | `periscope/src/backend/periscopex/impedance.py` | NEW; il solver è terzi |
+| Auth self-host | `periscope/src/backend/services/local_jwt.py`, `routers/auth.py` | REPLACEMENT |
+| LLM DeepSeek | `periscope/src/backend/services/llm/deepseek_provider.py`, `local_skill.py`, `pdf_ingest.py` | NEW |
+| Deploy | `scripts/update-periscope.sh`, `docker-compose.yml`, `periscope/src/backend/Dockerfile` | NEW (nomi `pinscope_*` ancora WEAK) |
+| Plugin KiCad | `periscope/src/plugins/kicad/` | NEW |
+| Check deterministici fork | `dnp_check`, `sequencing_check`, `layout_rules`, … under `periscope/src` | NEW ma **INDIRECT**: usano `models` / graph |
### 2.3 Terzi (né PinScope né “nostro motore”)
@@ -191,8 +191,18 @@ Allineato all’audit §11 punti 1–4: `finding_engine` / `validate.py` **non**
4. Compatibility fence: un modulo/doc per chiavi `pinscope_*`; smettere di **scrivere** `pinscopex:` in `localStorage`.
5. Dichiarare a codice (commento/ADR) il recinto §2 — ancora senza muovere file.
+**Shipped 2.36.0:** operatore Michele Bigi; `pinscope_compat`; fork invariato.
+
**Done quando:** UI e legal non dicono Faradworks Inc.; AGPL e fork parent invariati; PCB/schema invariati.
+### Split fisico — `periscope/src` vs `periscope/dependency/` (**prima** di riscrivere PinScope)
+
+Confermato da Michele: **prima** lo split degli alberi, **poi** sostituzione incrementale (Fase C). Non si parte da empty-delete.
+
+**Shipped 2.38.0:** git mv + path/Docker/test/deploy. `validate.py` e finding engine **non** riscritti. `LICENSE` AGPL visibile alla root. Fork GitHub non staccato. Auto-place invariato (non in scope).
+
+**Done quando:** l’app bootstrappa con i due alberi; Docker overlay `dependency` poi `src`; pytest verde; nessun modulo PinScope cancellato.
+
### Fase B — Seam runtime (Clerk, Anthropic, vendor license)
Audit §11 punti 5–6, 8–9.
diff --git a/docs/motore-finding.md b/periscope/src/docs/motore-finding.md
similarity index 100%
rename from docs/motore-finding.md
rename to periscope/src/docs/motore-finding.md
diff --git a/docs/piano-implementazione.md b/periscope/src/docs/piano-implementazione.md
similarity index 100%
rename from docs/piano-implementazione.md
rename to periscope/src/docs/piano-implementazione.md
diff --git a/docs/piano-pcb-review.md b/periscope/src/docs/piano-pcb-review.md
similarity index 100%
rename from docs/piano-pcb-review.md
rename to periscope/src/docs/piano-pcb-review.md
diff --git a/frontend/dockerfile b/periscope/src/frontend/dockerfile
similarity index 87%
rename from frontend/dockerfile
rename to periscope/src/frontend/dockerfile
index f619651..8f95493 100644
--- a/frontend/dockerfile
+++ b/periscope/src/frontend/dockerfile
@@ -7,10 +7,11 @@ ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
ARG NEXT_PUBLIC_AUTH_MODE
ENV NEXT_PUBLIC_AUTH_MODE=$NEXT_PUBLIC_AUTH_MODE
-COPY package*.json ./
+COPY periscope/dependency/frontend/package*.json ./
RUN npm ci
-COPY . .
+COPY periscope/dependency/frontend/ ./
+COPY periscope/src/frontend/ ./
ENV NEXT_TELEMETRY_DISABLED=1
diff --git a/frontend/src/app/(app)/library/page.tsx b/periscope/src/frontend/src/app/(app)/library/page.tsx
similarity index 100%
rename from frontend/src/app/(app)/library/page.tsx
rename to periscope/src/frontend/src/app/(app)/library/page.tsx
diff --git a/frontend/src/app/(app)/project/[id]/pcb/page.tsx b/periscope/src/frontend/src/app/(app)/project/[id]/pcb/page.tsx
similarity index 100%
rename from frontend/src/app/(app)/project/[id]/pcb/page.tsx
rename to periscope/src/frontend/src/app/(app)/project/[id]/pcb/page.tsx
diff --git a/frontend/src/app/(app)/project/[id]/placement/page.tsx b/periscope/src/frontend/src/app/(app)/project/[id]/placement/page.tsx
similarity index 100%
rename from frontend/src/app/(app)/project/[id]/placement/page.tsx
rename to periscope/src/frontend/src/app/(app)/project/[id]/placement/page.tsx
diff --git a/frontend/src/app/(marketing)/sign-in/page.tsx b/periscope/src/frontend/src/app/(marketing)/sign-in/page.tsx
similarity index 100%
rename from frontend/src/app/(marketing)/sign-in/page.tsx
rename to periscope/src/frontend/src/app/(marketing)/sign-in/page.tsx
diff --git a/frontend/src/app/(marketing)/sign-up/page.tsx b/periscope/src/frontend/src/app/(marketing)/sign-up/page.tsx
similarity index 100%
rename from frontend/src/app/(marketing)/sign-up/page.tsx
rename to periscope/src/frontend/src/app/(marketing)/sign-up/page.tsx
diff --git a/frontend/src/components/layout/auth-gate.tsx b/periscope/src/frontend/src/components/layout/auth-gate.tsx
similarity index 100%
rename from frontend/src/components/layout/auth-gate.tsx
rename to periscope/src/frontend/src/components/layout/auth-gate.tsx
diff --git a/frontend/src/components/layout/sidebar-auth.tsx b/periscope/src/frontend/src/components/layout/sidebar-auth.tsx
similarity index 100%
rename from frontend/src/components/layout/sidebar-auth.tsx
rename to periscope/src/frontend/src/components/layout/sidebar-auth.tsx
diff --git a/frontend/src/components/project/impedance-panel.tsx b/periscope/src/frontend/src/components/project/impedance-panel.tsx
similarity index 100%
rename from frontend/src/components/project/impedance-panel.tsx
rename to periscope/src/frontend/src/components/project/impedance-panel.tsx
diff --git a/frontend/src/components/project/pcb-upload.tsx b/periscope/src/frontend/src/components/project/pcb-upload.tsx
similarity index 100%
rename from frontend/src/components/project/pcb-upload.tsx
rename to periscope/src/frontend/src/components/project/pcb-upload.tsx
diff --git a/frontend/src/components/project/topology-panel.tsx b/periscope/src/frontend/src/components/project/topology-panel.tsx
similarity index 100%
rename from frontend/src/components/project/topology-panel.tsx
rename to periscope/src/frontend/src/components/project/topology-panel.tsx
diff --git a/frontend/src/components/report/finding-review-controls.tsx b/periscope/src/frontend/src/components/report/finding-review-controls.tsx
similarity index 100%
rename from frontend/src/components/report/finding-review-controls.tsx
rename to periscope/src/frontend/src/components/report/finding-review-controls.tsx
diff --git a/frontend/src/components/report/findings-tree.tsx b/periscope/src/frontend/src/components/report/findings-tree.tsx
similarity index 100%
rename from frontend/src/components/report/findings-tree.tsx
rename to periscope/src/frontend/src/components/report/findings-tree.tsx
diff --git a/frontend/src/components/report/pcb-exam-section.tsx b/periscope/src/frontend/src/components/report/pcb-exam-section.tsx
similarity index 100%
rename from frontend/src/components/report/pcb-exam-section.tsx
rename to periscope/src/frontend/src/components/report/pcb-exam-section.tsx
diff --git a/frontend/src/hooks/use-pcb-progress.ts b/periscope/src/frontend/src/hooks/use-pcb-progress.ts
similarity index 100%
rename from frontend/src/hooks/use-pcb-progress.ts
rename to periscope/src/frontend/src/hooks/use-pcb-progress.ts
diff --git a/frontend/src/hooks/use-placement-progress.ts b/periscope/src/frontend/src/hooks/use-placement-progress.ts
similarity index 100%
rename from frontend/src/hooks/use-placement-progress.ts
rename to periscope/src/frontend/src/hooks/use-placement-progress.ts
diff --git a/frontend/src/lib/auth.ts b/periscope/src/frontend/src/lib/auth.ts
similarity index 100%
rename from frontend/src/lib/auth.ts
rename to periscope/src/frontend/src/lib/auth.ts
diff --git a/frontend/src/lib/layout-finding.ts b/periscope/src/frontend/src/lib/layout-finding.ts
similarity index 100%
rename from frontend/src/lib/layout-finding.ts
rename to periscope/src/frontend/src/lib/layout-finding.ts
diff --git a/frontend/src/lib/pinscope-compat.ts b/periscope/src/frontend/src/lib/pinscope-compat.ts
similarity index 100%
rename from frontend/src/lib/pinscope-compat.ts
rename to periscope/src/frontend/src/lib/pinscope-compat.ts
diff --git a/frontend/src/lib/storage-keys.ts b/periscope/src/frontend/src/lib/storage-keys.ts
similarity index 100%
rename from frontend/src/lib/storage-keys.ts
rename to periscope/src/frontend/src/lib/storage-keys.ts
diff --git a/plugins/__init__.py b/periscope/src/plugins/__init__.py
similarity index 100%
rename from plugins/__init__.py
rename to periscope/src/plugins/__init__.py
diff --git a/plugins/kicad/__init__.py b/periscope/src/plugins/kicad/__init__.py
similarity index 100%
rename from plugins/kicad/__init__.py
rename to periscope/src/plugins/kicad/__init__.py
diff --git a/plugins/kicad/focus.py b/periscope/src/plugins/kicad/focus.py
similarity index 100%
rename from plugins/kicad/focus.py
rename to periscope/src/plugins/kicad/focus.py
diff --git a/plugins/kicad/periscope_plugin.py b/periscope/src/plugins/kicad/periscope_plugin.py
similarity index 100%
rename from plugins/kicad/periscope_plugin.py
rename to periscope/src/plugins/kicad/periscope_plugin.py
diff --git a/periscope/src/scripts/smoke_simple_project.py b/periscope/src/scripts/smoke_simple_project.py
new file mode 100644
index 0000000..6829795
--- /dev/null
+++ b/periscope/src/scripts/smoke_simple_project.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python3
+"""P0-5 smoke: deepseek-flash defaults + simple_project offline checks.
+
+Usage:
+ python3 scripts/smoke_simple_project.py # offline (no API)
+ python3 scripts/smoke_simple_project.py --live # needs DEEPSEEK_API_KEY
+
+Offline asserts: model defaults, vision gate, graph+eval golden, shortest_path.
+Live (optional): PDF ingest attaches page images under vision.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve()
+for _p in ROOT.parents:
+ if (_p / "LICENSE").is_file() and (_p / "periscope" / "dependency").is_dir():
+ ROOT = _p
+ break
+sys.path.insert(0, str(ROOT))
+
+SIMPLE = ROOT / "periscope" / "dependency" / "simple_project"
+BASELINE = SIMPLE / "smoke_baseline.json"
+
+
+def _check_model_defaults() -> list[str]:
+ from backend.config import settings
+
+ errs: list[str] = []
+ if settings.deepseek_model != "deepseek-flash":
+ errs.append(f"deepseek_model={settings.deepseek_model!r}, want deepseek-flash")
+ # Mirror deepseek_provider._is_vision_model without importing openai.
+ name = settings.deepseek_model.strip().lower()
+ vision_ok = (
+ name in {"deepseek-flash", "deepseek-v4-flash", "deepseek-v4-flash-vision-exp"}
+ or "vision" in name
+ or name.startswith("deepseek-flash")
+ )
+ if not vision_ok:
+ errs.append("deepseek-flash not treated as vision model")
+ effort = (settings.deepseek_reasoning_effort or "").lower()
+ if effort not in {"low", "high", "max"}:
+ errs.append(f"deepseek_reasoning_effort={effort!r}, want low|high|max")
+ return errs
+
+
+def _check_simple_project_offline() -> list[str]:
+ from backend.periscopex.models import DesignGraph
+ from backend.periscopex.validation_tools import shortest_path
+
+ errs: list[str] = []
+ graph_path = SIMPLE / "design_graph.json"
+ if not graph_path.is_file():
+ return ["simple_project/design_graph.json missing"]
+ g = DesignGraph.model_validate_json(graph_path.read_text(encoding="utf-8"))
+
+ golden_path = SIMPLE / "eval_golden.json"
+ if not golden_path.is_file():
+ return ["simple_project/eval_golden.json missing"]
+ golden_doc = json.loads(golden_path.read_text(encoding="utf-8"))
+ for ref in golden_doc.get("required_refs") or []:
+ if ref not in g.components:
+ errs.append(f"missing ref {ref}")
+ min_c = golden_doc.get("min_components")
+ if min_c and len(g.components) < int(min_c):
+ errs.append(f"components {len(g.components)} < {min_c}")
+ min_n = golden_doc.get("min_nets")
+ if min_n and len(g.nets) < int(min_n):
+ errs.append(f"nets {len(g.nets)} < {min_n}")
+
+ if "U3" in g.components and "X1" in g.components:
+ pin = next(iter(g.components["U3"].pins), None)
+ xpin = next(iter(g.components["X1"].pins), None)
+ if pin and xpin:
+ msg = shortest_path(g, {}, "U3", pin, "X1", xpin)
+ print(f"note: shortest_path U3–X1 → {msg}")
+
+ baseline = {
+ "component_count": len(g.components),
+ "net_count": len(g.nets),
+ "graph_ok": not errs,
+ }
+ if BASELINE.is_file():
+ prev = json.loads(BASELINE.read_text(encoding="utf-8"))
+ for k in ("component_count", "net_count"):
+ if prev.get(k) != baseline.get(k):
+ errs.append(f"{k} {baseline.get(k)} != baseline {prev.get(k)}")
+ else:
+ BASELINE.write_text(json.dumps(baseline, indent=2) + "\n", encoding="utf-8")
+ print(f"Wrote baseline {BASELINE}")
+
+ print(json.dumps({"offline": baseline, "errors": errs}, indent=2))
+ return errs
+
+
+def _check_live() -> list[str]:
+ from backend.config import settings
+ from backend.services.llm.deepseek_provider import _is_vision_model
+ from backend.services.llm.pdf_ingest import pdf_to_openai_content
+
+ if not settings.deepseek_api_key:
+ return ["DEEPSEEK_API_KEY not set"]
+ pdfs = list((ROOT / "library" / "datasheets").rglob("*.pdf"))[:1]
+ if not pdfs:
+ pdfs = list(SIMPLE.rglob("*.pdf"))
+ if not pdfs:
+ return ["no PDF found for live vision check"]
+ pdf = pdfs[0]
+ parts = pdf_to_openai_content(
+ pdf, vision=_is_vision_model(settings.deepseek_model), max_images=4,
+ )
+ n_img = sum(1 for p in parts if p.get("type") == "image_url")
+ print(json.dumps({"live_pdf": str(pdf), "content_parts": len(parts), "images": n_img}))
+ if n_img == 0 and _is_vision_model(settings.deepseek_model):
+ return ["vision model produced 0 page images"]
+ return []
+
+
+def main() -> int:
+ ap = argparse.ArgumentParser(description=__doc__)
+ ap.add_argument("--live", action="store_true", help="Also exercise PDF page images")
+ args = ap.parse_args()
+ errs = _check_model_defaults() + _check_simple_project_offline()
+ if args.live:
+ errs += _check_live()
+ if errs:
+ print("SMOKE FAIL:", *errs, sep="\n - ")
+ return 1
+ print("SMOKE OK")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..c7b23ec
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,3 @@
+[pytest]
+pythonpath = .
+testpaths = tests
diff --git a/scripts/materialize-frontend.sh b/scripts/materialize-frontend.sh
new file mode 100755
index 0000000..e6667db
--- /dev/null
+++ b/scripts/materialize-frontend.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+# Overlay PinScope frontend (dependency) with native Periscope files (src).
+set -euo pipefail
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+DEST="${1:-"$ROOT/.merge/frontend"}"
+python3 - "$ROOT" "$DEST" <<'PY'
+import shutil
+import sys
+from pathlib import Path
+
+root, dest = Path(sys.argv[1]), Path(sys.argv[2])
+dep = root / "periscope" / "dependency" / "frontend"
+src = root / "periscope" / "src" / "frontend"
+if dest.exists():
+ shutil.rmtree(dest)
+shutil.copytree(dep, dest, dirs_exist_ok=True)
+if src.is_dir():
+ shutil.copytree(src, dest, dirs_exist_ok=True)
+print(dest)
+PY
diff --git a/scripts/smoke_simple_project.py b/scripts/smoke_simple_project.py
index 77ed15f..4459ad9 100644
--- a/scripts/smoke_simple_project.py
+++ b/scripts/smoke_simple_project.py
@@ -1,134 +1,9 @@
#!/usr/bin/env python3
-"""P0-5 smoke: deepseek-flash defaults + simple_project offline checks.
-
-Usage:
- python3 scripts/smoke_simple_project.py # offline (no API)
- python3 scripts/smoke_simple_project.py --live # needs DEEPSEEK_API_KEY
-
-Offline asserts: model defaults, vision gate, graph+eval golden, shortest_path.
-Live (optional): PDF ingest attaches page images under vision.
-"""
-
-from __future__ import annotations
-
-import argparse
-import json
-import sys
+"""Wrapper kept at the historic path after the tree split."""
from pathlib import Path
+import runpy
-ROOT = Path(__file__).resolve().parents[1]
-sys.path.insert(0, str(ROOT))
-
-SIMPLE = ROOT / "simple_project"
-BASELINE = SIMPLE / "smoke_baseline.json"
-
-
-def _check_model_defaults() -> list[str]:
- from backend.config import settings
-
- errs: list[str] = []
- if settings.deepseek_model != "deepseek-flash":
- errs.append(f"deepseek_model={settings.deepseek_model!r}, want deepseek-flash")
- # Mirror deepseek_provider._is_vision_model without importing openai.
- name = settings.deepseek_model.strip().lower()
- vision_ok = (
- name in {"deepseek-flash", "deepseek-v4-flash", "deepseek-v4-flash-vision-exp"}
- or "vision" in name
- or name.startswith("deepseek-flash")
- )
- if not vision_ok:
- errs.append("deepseek-flash not treated as vision model")
- effort = (settings.deepseek_reasoning_effort or "").lower()
- if effort not in {"low", "high", "max"}:
- errs.append(f"deepseek_reasoning_effort={effort!r}, want low|high|max")
- return errs
-
-
-def _check_simple_project_offline() -> list[str]:
- from backend.periscopex.models import DesignGraph
- from backend.periscopex.validation_tools import shortest_path
-
- errs: list[str] = []
- graph_path = SIMPLE / "design_graph.json"
- if not graph_path.is_file():
- return ["simple_project/design_graph.json missing"]
- g = DesignGraph.model_validate_json(graph_path.read_text(encoding="utf-8"))
-
- golden_path = SIMPLE / "eval_golden.json"
- if not golden_path.is_file():
- return ["simple_project/eval_golden.json missing"]
- golden_doc = json.loads(golden_path.read_text(encoding="utf-8"))
- for ref in golden_doc.get("required_refs") or []:
- if ref not in g.components:
- errs.append(f"missing ref {ref}")
- min_c = golden_doc.get("min_components")
- if min_c and len(g.components) < int(min_c):
- errs.append(f"components {len(g.components)} < {min_c}")
- min_n = golden_doc.get("min_nets")
- if min_n and len(g.nets) < int(min_n):
- errs.append(f"nets {len(g.nets)} < {min_n}")
-
- if "U3" in g.components and "X1" in g.components:
- pin = next(iter(g.components["U3"].pins), None)
- xpin = next(iter(g.components["X1"].pins), None)
- if pin and xpin:
- msg = shortest_path(g, {}, "U3", pin, "X1", xpin)
- print(f"note: shortest_path U3–X1 → {msg}")
-
- baseline = {
- "component_count": len(g.components),
- "net_count": len(g.nets),
- "graph_ok": not errs,
- }
- if BASELINE.is_file():
- prev = json.loads(BASELINE.read_text(encoding="utf-8"))
- for k in ("component_count", "net_count"):
- if prev.get(k) != baseline.get(k):
- errs.append(f"{k} {baseline.get(k)} != baseline {prev.get(k)}")
- else:
- BASELINE.write_text(json.dumps(baseline, indent=2) + "\n", encoding="utf-8")
- print(f"Wrote baseline {BASELINE}")
-
- print(json.dumps({"offline": baseline, "errors": errs}, indent=2))
- return errs
-
-
-def _check_live() -> list[str]:
- from backend.config import settings
- from backend.services.llm.deepseek_provider import _is_vision_model
- from backend.services.llm.pdf_ingest import pdf_to_openai_content
-
- if not settings.deepseek_api_key:
- return ["DEEPSEEK_API_KEY not set"]
- pdfs = list((ROOT / "library" / "datasheets").rglob("*.pdf"))[:1]
- if not pdfs:
- pdfs = list(SIMPLE.rglob("*.pdf"))
- if not pdfs:
- return ["no PDF found for live vision check"]
- pdf = pdfs[0]
- parts = pdf_to_openai_content(
- pdf, vision=_is_vision_model(settings.deepseek_model), max_images=4,
- )
- n_img = sum(1 for p in parts if p.get("type") == "image_url")
- print(json.dumps({"live_pdf": str(pdf), "content_parts": len(parts), "images": n_img}))
- if n_img == 0 and _is_vision_model(settings.deepseek_model):
- return ["vision model produced 0 page images"]
- return []
-
-
-def main() -> int:
- ap = argparse.ArgumentParser(description=__doc__)
- ap.add_argument("--live", action="store_true", help="Also exercise PDF page images")
- args = ap.parse_args()
- errs = _check_model_defaults() + _check_simple_project_offline()
- if args.live:
- errs += _check_live()
- if errs:
- print("SMOKE FAIL:", *errs, sep="\n - ")
- return 1
- print("SMOKE OK")
- return 0
-
-
-if __name__ == "__main__":
- raise SystemExit(main())
+runpy.run_path(
+ str(Path(__file__).resolve().parents[1] / "periscope" / "src" / "scripts" / "smoke_simple_project.py"),
+ run_name="__main__",
+)
diff --git a/scripts/update-periscope.sh b/scripts/update-periscope.sh
index 2b7332e..ab5dcd9 100755
--- a/scripts/update-periscope.sh
+++ b/scripts/update-periscope.sh
@@ -126,6 +126,9 @@ if [[ ! -f .env ]]; then
if [[ -f backend/.env ]]; then
log "No ./.env — copying backend/.env"
cp backend/.env .env
+ elif [[ -f periscope/dependency/backend/.env.example ]]; then
+ log "No ./.env — copying periscope/dependency/backend/.env.example (you must set DEEPSEEK_API_KEY)"
+ cp periscope/dependency/backend/.env.example .env
elif [[ -f backend/.env.example ]]; then
log "No ./.env — copying backend/.env.example (you must set DEEPSEEK_API_KEY)"
cp backend/.env.example .env
diff --git a/tests/conftest.py b/tests/conftest.py
index 101e7d7..0816048 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -10,8 +10,14 @@ import pytest
# Ensure the repo root is on sys.path so `import backend.*` works whether
# pytest is invoked from the repo root or a subdirectory.
REPO_ROOT = Path(__file__).resolve().parents[1]
-if str(REPO_ROOT) not in sys.path:
- sys.path.insert(0, str(REPO_ROOT))
+for p in (
+ REPO_ROOT / "periscope" / "dependency",
+ REPO_ROOT / "periscope" / "src",
+ REPO_ROOT,
+):
+ s = str(p)
+ if p.is_dir() and s not in sys.path:
+ sys.path.insert(0, s)
from backend.vendor_path import ensure_impedancefinder
diff --git a/tests/paths.py b/tests/paths.py
new file mode 100644
index 0000000..4baac96
--- /dev/null
+++ b/tests/paths.py
@@ -0,0 +1,9 @@
+"""Shared fixture paths after the periscope/src vs periscope/dependency split."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+REPO_ROOT = Path(__file__).resolve().parents[1]
+SIMPLE_PROJECT = REPO_ROOT / "periscope" / "dependency" / "simple_project"
+TAXONOMY = REPO_ROOT / "periscope" / "dependency" / "taxonomy"
diff --git a/tests/test_cost_estimator.py b/tests/test_cost_estimator.py
index 2edc3e7..7c86da5 100644
--- a/tests/test_cost_estimator.py
+++ b/tests/test_cost_estimator.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from pathlib import Path
+from tests.paths import SIMPLE_PROJECT
import pytest
@@ -14,9 +14,6 @@ from backend.services.cost_estimator import (
)
-SIMPLE_PROJECT = Path(__file__).resolve().parents[1] / "simple_project"
-
-
def _seed_project(storage, user_id: str, project_id: str) -> None:
"""Create a minimal project with the simple_project BOM uploaded."""
proj_svc.create_project(storage, user_id, "Fixture")
diff --git a/tests/test_crystal_cl_check.py b/tests/test_crystal_cl_check.py
index 05d35d7..54eafc8 100644
--- a/tests/test_crystal_cl_check.py
+++ b/tests/test_crystal_cl_check.py
@@ -2,6 +2,8 @@
from __future__ import annotations
+from tests.paths import SIMPLE_PROJECT, TAXONOMY
+
from backend.periscopex.crystal_cl_check import check_crystal_cl
from backend.periscopex.models import (
Component,
@@ -92,6 +94,6 @@ def test_with_stray_mismatch_warns():
def test_simple_project_without_cl_silent():
from pathlib import Path
from backend.periscopex.models import DesignGraph
- path = Path(__file__).resolve().parents[1] / "simple_project" / "design_graph.json"
+ path = SIMPLE_PROJECT / "design_graph.json"
g = DesignGraph.model_validate_json(path.read_text())
assert check_crystal_cl(g) == []
diff --git a/tests/test_datasheet_excerpt_tool.py b/tests/test_datasheet_excerpt_tool.py
index 832a829..171e837 100644
--- a/tests/test_datasheet_excerpt_tool.py
+++ b/tests/test_datasheet_excerpt_tool.py
@@ -2,6 +2,8 @@
from __future__ import annotations
+from tests.paths import SIMPLE_PROJECT, TAXONOMY
+
import json
from pathlib import Path
@@ -19,7 +21,7 @@ from backend.periscopex.validation_tools import (
from backend.services.llm.types import PdfBlock
from backend.services.validation import _signal_neighbors
-GRAPH = Path(__file__).resolve().parent.parent / "simple_project" / "design_graph.json"
+GRAPH = SIMPLE_PROJECT / "design_graph.json"
IC_MPNS = {
"U1": "SPX3819M5-L-3-3/TR",
"U2": "CH340E",
diff --git a/tests/test_eval_report.py b/tests/test_eval_report.py
index 8389015..4980734 100644
--- a/tests/test_eval_report.py
+++ b/tests/test_eval_report.py
@@ -8,6 +8,8 @@ Unverified quotes are citation misses, not hits.
from __future__ import annotations
+from tests.paths import SIMPLE_PROJECT, TAXONOMY
+
from pathlib import Path
from backend.periscopex.eval_report import (
@@ -80,7 +82,7 @@ def test_citation_rate_ignores_deterministic_and_counts_unverified():
def test_simple_project_eval_matches_committed_golden():
- root = Path(__file__).resolve().parents[1] / "simple_project"
+ root = SIMPLE_PROJECT
scores = eval_simple_project(root)
assert scores.graph_ok, scores.graph_errors
assert scores.precision == 1.0
@@ -94,7 +96,7 @@ def test_simple_project_eval_rejects_truncated_graph(tmp_path: Path):
import json
import shutil
- src = Path(__file__).resolve().parents[1] / "simple_project"
+ src = SIMPLE_PROJECT
dest = tmp_path / "simple_project"
shutil.copytree(src, dest)
g = json.loads((dest / "design_graph.json").read_text())
diff --git a/tests/test_functional_groups.py b/tests/test_functional_groups.py
index 04370ee..b69397a 100644
--- a/tests/test_functional_groups.py
+++ b/tests/test_functional_groups.py
@@ -2,13 +2,15 @@
from __future__ import annotations
+from tests.paths import SIMPLE_PROJECT, TAXONOMY
+
import json
from pathlib import Path
from backend.periscopex.functional_groups import build_functional_groups
from backend.periscopex.models import DesignGraph
-SIMPLE = Path(__file__).resolve().parents[1] / "simple_project"
+SIMPLE = SIMPLE_PROJECT
def _graph() -> DesignGraph:
diff --git a/tests/test_passive_from_distributor.py b/tests/test_passive_from_distributor.py
index 1b630a3..19ef58b 100644
--- a/tests/test_passive_from_distributor.py
+++ b/tests/test_passive_from_distributor.py
@@ -1,3 +1,4 @@
+from tests.paths import SIMPLE_PROJECT, TAXONOMY
from backend.services.passive_from_distributor import specs_from_distributor
@@ -91,7 +92,7 @@ def test_auto_resolve_skips_llm_when_catalog_parses(monkeypatch):
raise AssertionError("LLM must not run")
monkeypatch.setattr("backend.services.extraction.call_with_fallback", boom)
- tax = Path(__file__).resolve().parents[1] / "taxonomy"
+ tax = TAXONOMY
model = asyncio.run(
auto_resolve_specs(
mpn="CL21B225KPFNNNE",
@@ -115,7 +116,7 @@ def test_auto_resolve_use_llm_false_on_ferrite(monkeypatch):
raise AssertionError("LLM must not run")
monkeypatch.setattr("backend.services.extraction.call_with_fallback", boom)
- tax = Path(__file__).resolve().parents[1] / "taxonomy"
+ tax = TAXONOMY
model = asyncio.run(
auto_resolve_specs(
mpn="BLM18PG121SN1D",
diff --git a/tests/test_passive_from_value.py b/tests/test_passive_from_value.py
index f7d080e..d0da194 100644
--- a/tests/test_passive_from_value.py
+++ b/tests/test_passive_from_value.py
@@ -1,3 +1,4 @@
+from tests.paths import SIMPLE_PROJECT, TAXONOMY
import asyncio
from pathlib import Path
@@ -57,7 +58,7 @@ def test_resolve_from_value_skips_llm_when_parseable(monkeypatch):
raise AssertionError("LLM must not run")
monkeypatch.setattr("backend.services.extraction.call_with_fallback", boom)
- tax = Path(__file__).resolve().parents[1] / "taxonomy"
+ tax = TAXONOMY
model = asyncio.run(
resolve_from_value(
mpn="18pF",
@@ -76,7 +77,7 @@ def test_resolve_from_value_placeholder_no_llm(monkeypatch):
raise AssertionError("LLM must not run")
monkeypatch.setattr("backend.services.extraction.call_with_fallback", boom)
- tax = Path(__file__).resolve().parents[1] / "taxonomy"
+ tax = TAXONOMY
with pytest.raises(ValueError, match="Placeholder"):
asyncio.run(
resolve_from_value(
diff --git a/tests/test_pcb_review.py b/tests/test_pcb_review.py
index 4a1a068..c73d939 100644
--- a/tests/test_pcb_review.py
+++ b/tests/test_pcb_review.py
@@ -2,6 +2,8 @@
from __future__ import annotations
+from tests.paths import SIMPLE_PROJECT, TAXONOMY
+
from pathlib import Path
from backend.periscopex.functional_groups import FunctionalGroupsReport, PlacementDomain, PlacementIcGroup
@@ -26,7 +28,7 @@ from backend.periscopex.pcb_net_match import check_pcb_net_match
from backend.periscopex.pcb_review import build_pcb_layout_context
from backend.services.projects import ProjectMeta, STATUS_QUEUED, STATUS_RUNNING
-SIMPLE = Path(__file__).resolve().parents[1] / "simple_project"
+SIMPLE = SIMPLE_PROJECT
def _graph() -> DesignGraph:
diff --git a/tests/test_pin_mux_check.py b/tests/test_pin_mux_check.py
index b01d780..6d5e3fb 100644
--- a/tests/test_pin_mux_check.py
+++ b/tests/test_pin_mux_check.py
@@ -13,6 +13,8 @@ Locks in:
from __future__ import annotations
+from tests.paths import SIMPLE_PROJECT, TAXONOMY
+
from backend.periscopex.models import (
Component,
ComponentConstraints,
@@ -184,7 +186,7 @@ def test_simple_project_uart0_nets_are_feasible_on_mspm0_pins():
from backend.periscopex.models import DesignGraph
graph = DesignGraph.model_validate_json(
- (Path(__file__).resolve().parents[1] / "simple_project" / "design_graph.json").read_text()
+ (SIMPLE_PROJECT / "design_graph.json").read_text()
)
cmap = {
"MSPM0G3507SPTR": _constraints(
@@ -206,7 +208,7 @@ def test_simple_project_uart0_swapped_on_mspm0_is_error():
from backend.periscopex.models import DesignGraph
graph = DesignGraph.model_validate_json(
- (Path(__file__).resolve().parents[1] / "simple_project" / "design_graph.json").read_text()
+ (SIMPLE_PROJECT / "design_graph.json").read_text()
)
cmap = {
"MSPM0G3507SPTR": _constraints(
diff --git a/tests/test_placement_check.py b/tests/test_placement_check.py
index 9bd2f06..03e0095 100644
--- a/tests/test_placement_check.py
+++ b/tests/test_placement_check.py
@@ -6,6 +6,8 @@ Against: no PCB; same copper; same_layer unset; via in courtyard.
from __future__ import annotations
+from tests.paths import SIMPLE_PROJECT, TAXONOMY
+
from pathlib import Path
from backend.periscopex.eval_report import eval_simple_project
@@ -21,7 +23,7 @@ from backend.periscopex.models import (
)
from backend.periscopex.placement_check import _in_poly, check_placement
-SIMPLE = Path(__file__).resolve().parents[1] / "simple_project"
+SIMPLE = SIMPLE_PROJECT
def _graph() -> DesignGraph:
diff --git a/tests/test_placement_pack.py b/tests/test_placement_pack.py
index 227c1e7..d44525c 100644
--- a/tests/test_placement_pack.py
+++ b/tests/test_placement_pack.py
@@ -2,6 +2,8 @@
from __future__ import annotations
+from tests.paths import SIMPLE_PROJECT, TAXONOMY
+
from pathlib import Path
from backend.periscopex.functional_groups import (
@@ -18,7 +20,7 @@ from backend.periscopex.models import (
)
from backend.periscopex.placement_pack import build_placement_pack
-SIMPLE = Path(__file__).resolve().parents[1] / "simple_project"
+SIMPLE = SIMPLE_PROJECT
def _graph() -> DesignGraph:
diff --git a/tests/test_placement_pipeline.py b/tests/test_placement_pipeline.py
index 0614c47..b3ad4d1 100644
--- a/tests/test_placement_pipeline.py
+++ b/tests/test_placement_pipeline.py
@@ -2,6 +2,8 @@
from __future__ import annotations
+from tests.paths import SIMPLE_PROJECT, TAXONOMY
+
from pathlib import Path
import pytest
@@ -14,7 +16,7 @@ from backend.periscopex.models import DesignGraph
ROOT = Path(__file__).resolve().parents[1]
-SIMPLE = ROOT / "simple_project"
+SIMPLE = SIMPLE_PROJECT
@pytest.fixture
diff --git a/tests/test_si_check.py b/tests/test_si_check.py
index ff0e24c..53ba3ee 100644
--- a/tests/test_si_check.py
+++ b/tests/test_si_check.py
@@ -7,6 +7,8 @@ I2C/GPIO/CC are not 50/90 Ω. ImpedenceFinder numbers vs layout_rules only.
from __future__ import annotations
+from tests.paths import SIMPLE_PROJECT, TAXONOMY
+
from pathlib import Path
from backend.periscopex.eval_report import eval_simple_project
@@ -26,7 +28,7 @@ from backend.periscopex.models import (
)
from backend.periscopex.si_check import bus_class, check_si, partner_net, skip_si_net
-SIMPLE = Path(__file__).resolve().parents[1] / "simple_project"
+SIMPLE = SIMPLE_PROJECT
def _graph() -> DesignGraph:
diff --git a/tests/test_validation_no_tool_recovery.py b/tests/test_validation_no_tool_recovery.py
index 9a2737d..efc59fc 100644
--- a/tests/test_validation_no_tool_recovery.py
+++ b/tests/test_validation_no_tool_recovery.py
@@ -9,6 +9,8 @@ so it survives future prompt regressions.
from __future__ import annotations
+from tests.paths import SIMPLE_PROJECT, TAXONOMY
+
import json
from pathlib import Path
@@ -20,7 +22,7 @@ from backend.services import validation as val
from backend.services.llm.types import Completion, ToolCall, Usage
from backend.services.storage import LocalStorageBackend
-GRAPH = Path(__file__).resolve().parent.parent / "simple_project" / "design_graph.json"
+GRAPH = SIMPLE_PROJECT / "design_graph.json"
IC_MPNS = {
"U1": "SPX3819M5-L-3-3/TR",
"U2": "CH340E",
diff --git a/tests/test_validation_trace_log.py b/tests/test_validation_trace_log.py
index aceba75..55f13e6 100644
--- a/tests/test_validation_trace_log.py
+++ b/tests/test_validation_trace_log.py
@@ -8,6 +8,8 @@ and that trace failures never break the review/report.
from __future__ import annotations
+from tests.paths import SIMPLE_PROJECT, TAXONOMY
+
import json
import re
from pathlib import Path
@@ -20,7 +22,7 @@ from backend.services import validation as val
from backend.services.llm.types import Completion, ToolCall, Usage
from backend.services.storage import LocalStorageBackend
-GRAPH = Path(__file__).resolve().parent.parent / "simple_project" / "design_graph.json"
+GRAPH = SIMPLE_PROJECT / "design_graph.json"
IC_MPNS = {
"U1": "SPX3819M5-L-3-3/TR",
"U2": "CH340E",