From 721ede424327e0c78e99bbe3cdaea65982093925 Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Sat, 19 Sep 2026 21:41:27 +0200 Subject: [PATCH] Add PCB review plan: verify placement and routing, no auto-place. Documents a parallel MODE=pcb exam job (inventory + datasheet-gated checks) that reuses placement_check, si_check, and the placement job pattern. Parks F2 packing/export. --- docs/piano-pcb-review.md | 284 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 docs/piano-pcb-review.md diff --git a/docs/piano-pcb-review.md b/docs/piano-pcb-review.md new file mode 100644 index 0000000..2587c0c --- /dev/null +++ b/docs/piano-pcb-review.md @@ -0,0 +1,284 @@ +# Piano — PCB review (controllo placement + routing) + +Documento **solo di piano**. Non è un implementazione sprint e **non** è un auto-placer. + +Prodotto: **Periscope Layout review** — esaminare un board KiCad già sbrogliato (o parzialmente sbrogliato) e dire *dove* placement e routing violano geometria + datasheet. + +Contratto rispetto a [`docs/piano-implementazione.md`](piano-implementazione.md): + +| | Schema (già in produzione) | PCB review (questo piano) | Placement F2 pack (fuori da questo piano) | +| --- | --- | --- | --- | +| Promessa | Lo schema rispetta il datasheet | Il rame e le posizioni **esistenti** rispettano datasheet + geometria | Proporre xy nuovi | +| Trigger | BOM + netlist / `.kicad_sch` | + `.kicad_pcb` | PCB + `layout_rules` numerici | +| Output | Finding pin/net, derating, review LLM | Finding mm / net / Z0 / skew / via EP, `rule_id` `PE-PLC*` / `PE-SI*` / `PE-LAY*` | `placement_pack.json` proposte | +| Job | `MODE=run` (`status`) | Job parallelo `MODE=pcb` (`pcb_status`) | già `MODE=placement` (`placement_status`) | + +Senza `.kicad_pcb` il progetto resta un Periscope schema **completo**. Il report schema **non** si riempie di `PE-PLC` / `PE-SI`. + +--- + +## 0. Postura: esame, non progettazione + +### Placement = verifica + +Si misura ciò che è **già** sul board: + +- distanza footprint/pad (euclidea e shortest-path sui segmenti); +- layer (F.Cu vs B.Cu) vs `same_layer`; +- via nel courtyard del pad termico vs `min_via_count`; +- keepout: net estraneo nel courtyard; +- crystal: load cap vs pin XIN/XOUT con la stessa metrica. + +Non si spostano footprint. Non si scrive `.kicad_pcb`. Non si usa `placement_pack` / `build_placement_pack` come obiettivo di questo piano. + +`placement_check` (`backend/periscopex/placement_check.py`, `PE-PLC-001`…`004`) è già questa postura. Resta il nucleo placement del job PCB. + +La pipeline Placement esistente (`POST …/placement/start`, `placement_plan.json`) è **topologia routing-first** (domini, satelliti, `role_hint`). Utile come contesto (“questo C è decoupling di U1”) ma **non** è auto-placement. F2 packing (`placement_pack.json`, collisioni, export pcbnew) resta **parcheggiato**. + +### Routing = la stessa postura + +Si misura ciò che è **già** in rame: + +- lunghezza net / coppia / bus (somma segmenti, non folklore USB); +- intra-pair skew vs `layout_rules.kind=length_match` **solo se** c’è un mm nel datasheet (`si_check`, `PE-SI-001`); +- Z0 sulle tracce dove stackup + larghezza esistono (`impedance_traces.py` + ImpedanceFinder); +- mismatch pad PCB vs pin schema (net name su pad vs `DesignGraph.pin_net`); +- elenco tracce / bus per il report (inventario, poi check). + +Non si sbroglia. Non si cambia larghezza. Non si inventano 3W, creepage IEC, o target 50 Ω se il datasheet/stackup non li danno. + +### Datasheet compliance (layout) + +Solo vincoli **strutturati** già estratti in `ComponentConstraints.layout_rules` (skill C4, `layout_rules.py`): + +| `kind` | Check PCB | Skip se | +| --- | --- | --- | +| `decoupling_proximity` | distanza / path vs `max_distance_mm`; crystal stesso | `max_distance_mm` null | +| `thermal_via` | via in courtyard vs `min_via_count` | courtyard o count assenti | +| `keepout` | endpoint net estraneo in courtyard | courtyard assente | +| `length_match` | skew coppia vs `max_distance_mm` | mm assente | + +Niente default 3 mm. Niente “foto del layout TI vs gerber”. Citazione: `source_page` sul finding, come oggi. + +--- + +## 1. Come sta in parallelo alla pipeline schema + +Oggi i check layout (`placement_check`, `si_check`) girano **dentro** la review schema (`backend/services/validation.py` → `_run_deterministic_checks`) se esiste `layout_graph.json`. Funziona, ma mescola due prodotti nello stesso `report.json` e nello stesso `status`. + +Target: + +``` + ┌─ MODE=run → status → report.json (schema) + Project files ───┼─ MODE=placement → placement_status → placement_plan.json + └─ MODE=pcb → pcb_status → pcb_report.json +``` + +Stesso pattern job/stage/findings della Placement sibling, **non** un secondo auto-placer. + +### Riuso obbligatorio (non reinventare) + +| Pezzo | Path | Ruolo nel job PCB | +| --- | --- | --- | +| Workspace / SSE broker | `backend/services/pipeline.py` (`PipelineWorkspace`, `broker`) | Download upload, `layout_graph.json`, eventi | +| Job dispatch | `backend/services/job_runner.py`, `backend/pipeline_worker.py` | Nuovo `MODE=pcb` accanto a `run` / `regen` / `placement` | +| API sibling | `backend/routers/pipeline.py` (`placement/start|cancel|events`) | `pcb/start`, `pcb/cancel`, `pcb/events` | +| Meta progetto | `backend/services/projects.py` (`placement_status*`) | `pcb_status`, `pcb_state`, `pcb_execution_name`, `pcb_cancel_requested` | +| Parser board | `backend/periscopex/parsers_kicad_pcb.py` | `LayoutGraph` (footprint, pad net, segmenti, via, zone, stackup) | +| Grafo schema | `design_graph.json` / `build_graph(..., pcb_path=)` | Pin↔net; `capacitors_on_net`; CAD index | +| Placement verify | `placement_check.py` | PE-PLC-* | +| SI verify | `si_check.py` | PE-SI-001 | +| BOM/schema | `bom_match_check.py` | Resta **schema**. Sul PCB: analogo **pad-net match** (nuovo check piccolo, stesso schema `Finding`) | +| Cad bridge | `cad_bridge.py` (`target: pcb` per `PE-PLC`/`PE-SI`/`PE-LAY`) | Plugin pcbnew focus | +| Validation helpers | `assign_finding_ids`, fail-soft per check, `annotate_findings_cad` | IDs + CAD | +| Impedenza tracce | `impedance_traces.py`, `routers/impedance.py`, `impedance_nets.json` | Inventario Z0; finding solo se target numerico esiste | +| UI | `finding-card`, report filters, `types.ts` / `api.ts`, `pcb-upload.tsx`, pagine/hooks placement come stampo | Tab Layout + findings nel report | +| Test | `tests/test_kicad_pcb.py`, `test_placement_check.py`, `test_si_check.py`, `test_placement_pipeline.py` | Fixtures + job smoke | + +### Contratto job (come Placement) + +- **Non** tocca `ProjectMeta.status` dell’analisi schema. +- Soft-cancel: flag `pcb_cancel_requested` (come `placement_cancel_requested`). +- Mutex: non partire se analisi `queued|running` **o** placement `queued|running` **o** pcb già attivo (l’event log è condiviso e Placement fa `clear_history`). +- SSE: eventi `pcb_step_update` / `pcb_complete` / `pcb_error` / `pcb_cancelled`. Lo stream schema ignora `pcb_*` (come già ignora `placement_*`). +- Locale: `enqueue` con `proc_key=pcb:{id}`, `execution_name=local/pcb/{id}` (stesso trick di `local/placement/`). +- Heal zombie: `heal_if_pcb_stuck` speculare a `heal_if_placement_stuck` (evento terminale, o artefatto `pcb_report.json` se il worker è morto dopo la scrittura). +- Billing: job **free** (come placement). Nessun LLM in v1. + +### Stages del job PCB (v1) + +1. **`ensure_graph`** — riusa `design_graph.json`; se manca, `graph_build` da BOM+netlist (stesso helper Placement). Senza grafo: 400, non inventare connettività dal solo PCB. +2. **`parse_pcb`** — riusa `layout_graph.json` o `parse_kicad_pcb(uploads/pcb.kicad_pcb)` e upload. Fail → `pcb_status=error` (a differenza dello schema, qui il PCB è l’ingresso). +3. **`inventory`** — lista net con rame, lunghezze, coppie `_DP/_DM` `_P/_N`, bus omonimi, Z0 dove stackup+width ci sono. Artefatto `pcb_inventory.json` (leggibile in UI). Nessun finding da soli numeri. +4. **`checks`** — funzioni pure in `periscopex/` (fail-soft per check). Vedi §3. +5. **`write_report`** — `pcb_report.json` (`ValidationReport`), cad-bridge merge, IDs `PCB-{designator}-{001}` per non collidere con lo schema. + +Senza datasheet / `layout_rules`: l’inventory c’è comunque; i finding datasheet-gated sono lista vuota, non errore. + +--- + +## 2. Ingressi + +| Ingresso | Dove | Obbligatorio v1 | +| --- | --- | --- | +| `.kicad_pcb` | `uploads/pcb.kicad_pcb`, `has_pcb` | Sì | +| Design graph | `design_graph.json` (da BOM+netlist / KiCad sch flatten) | Sì (per pin↔net e MPN) | +| Estrazioni IC | `extracted/` + library | Per `layout_rules` / pintable pin names | +| Datasheet PDF | library blobs | Solo per `source_page` in UI; i check v1 sono deterministici | +| Stackup nel PCB | `LayoutGraph.stackup` (εr, h, t, layer names) | Per Z0; skip se assente | +| `layout_rules` | JSON MPN, `KNOWN_KINDS` | Per PE-PLC / PE-SI; skip se mm/count null | + +Non in v1: Gerber come ingresso, EasyEDA JSON, PADS dump come layout, zip gerarchico PCB (il board non è gerarchico). + +Allineamento net: i nomi net del PCB devono combaciare con lo schema **dopo** flatten gerarchico (`U1.4` ↔ pad). Un mismatch è un finding (`PE-LAY-001`), non un crash. + +--- + +## 3. Cosa elenca e cosa controlla questa stage + +Due uscite distinte: **inventario** (sempre, se il parse riesce) e **findings** (solo regola + numero). + +### 3.1 Inventario (report Layout, non finding) + +Per ogni net con rame: + +- nome, `NetType` dallo schema se noto; +- lunghezza mm (somma segmenti, come `si_check.net_length_mm`); +- layer usati, min/max width, n. via; +- coppia differenziale se suffisso noto; +- Z0 stimata se stackup+width (riuso `impedance_traces.analyze_where_needed` / `impedance_nets.json`); +- appartenenza a bus (prefisso comune: `D[0-7]`, `A[0-15]`, `USB_DP/DM`) — lunghezze affiancate, **senza** soglia inventata. + +UI: tabella “Traces / buses” sulla pagina PCB o tab Layout. Serve a *vedere* il board prima dei finding. + +### 3.2 Check deterministici (findings, stesso modello `Finding`) + +Registrati in un unico `run_pcb_checks(graph, constraints_map, layout)` (nuovo wrapper sottile; **non** SDK LLM in `periscopex/`). + +| ID | Source | Cosa | Gate | +| --- | --- | --- | --- | +| `PE-LAY-001` | nuovo `pcb_net_match` (specchio di `bom_match_check`) | Pad net ≠ pin net schema, stesso ref+pin | Pad ha `net` e lo schema ha `pin_net` | +| `PE-LAY-002` | stesso modulo | Ref in schema senza footprint PCB (o viceversa, skip `#PWR`) | Graph + footprints | +| `PE-PLC-001` | `placement_check` | Decoupling / crystal oltre `max_distance_mm` (path se esiste, senno euclidea) | regola + mm | +| `PE-PLC-002` | idem | Via termiche < `min_via_count` | courtyard + count | +| `PE-PLC-003` | idem | `same_layer: true` e cap sull’altro lato senza via sotto il pin | boolean regola + layer footprint | +| `PE-PLC-004` | idem | Keepout: net estraneo in courtyard | courtyard | +| `PE-SI-001` | `si_check` | Intra-pair skew > `length_match` mm | coppia nominale **e** mm datasheet | +| `PE-Z0-001` (fase 2) | da `impedance_traces` | Z0 fuori target | target in `layout_rules` **o** netclass KiCad esplicita; mai 50 Ω di default | + +Campi finding (già sul modello): `rule_id`, `net`, `pins[]`, `source`, `cad_sheet` / `cad_uuid` se in `cad_index`, `status` ERROR/WARNING, `source_page` dalla regola. + +Normalization: **downgrade-only** se un giorno si aggiunge un pass; v1 non alza severità. + +### 3.3 Fuori dai finding (skip esplicito, log) + +- 3W / crosstalk senza distanza datasheet o calcolatore. +- Creepage/clearance IEC 62368 senza V/mm nel datasheet. +- Isolation barrier / HV courtyard senza keepout HV. +- CPWG se il vendor ImpedanceFinder non lo dà. +- Length-match “USB spec” / 150 mil folklore. +- Confronto visivo application-circuit vs PCB. + +Queste restano Wave G residue in `piano-implementazione.md`, non questo job. + +--- + +## 4. UI + +Gating: `hasPcb`. Senza PCB, nessun bottone “Run PCB review”, nessun finding layout nel report schema. + +| Superficie | Comportamento | +| --- | --- | +| Project hub | Accanto a “Build placement plan”: **Run PCB review** (verify). Upload già `PcbUploadButton`. | +| `/project/[id]/pcb` | Stampo di `/placement`: stepper SSE (`ensure_graph` → `parse_pcb` → `inventory` → `checks` → `write_report`), cancel, errore, link al report. | +| Sidebar | Voce **Layout** (`/pcb` o `report?domain=layout`). Nested route = `forceDocument` come placement. | +| Report | `GET /report/{id}` **merge** `report.json` + `pcb_report.json`. Filtro URL `?domain=schema\|layout\|all`. Finding card: badge “Layout” + `rule_id` (`PE-PLC-001`) oltre “Automated check”. | +| Inventory | Tabella tracce/bus/Z0 sulla pagina PCB (dati `pcb_inventory.json` / `impedance_nets.json`). | +| Plugin | `periscope-findings.json` include finding `target: pcb` dopo il job PCB (cad-bridge già distingue prefissi). | + +Commenti / review_states restano keyed by `finding_id` su `report.json` (o dict parallelo); il merge in GET non deve perdere commenti schema. IDs prefissati `PCB-` evitano collisioni `U3-001`. + +Re-run schema **non** cancella `pcb_report.json`. Re-run PCB **non** tocca findings LLM. + +--- + +## 5. Fasi + +Ordine: non parallelizzare con auto-pack. Non mischiare changelog Core vs Layout. + +### Fase 0 — Confine (doc + split) + +- Questo documento = contratto. +- Togliere `placement_check` / `si_check` da `_run_deterministic_checks` della review schema (restano no-op senza layout; lo split evita PE-PLC nello schema). +- Eval harness schema (`eval_report.py`) continua a chiamarli con `layout=None` (no-op) **oppure** li sposta in eval PCB dedicato. + +**Done when:** run schema senza PCB = zero `PE-PLC`/`PE-SI` nel report. + +### Fase 1 — Job parallelo + inventory + check già esistenti (slice usabile) + +- Meta `pcb_*`, `enqueue` `MODE=pcb`, router start/cancel/events, worker. +- Stages §1; artefatti `layout_graph.json`, `pcb_inventory.json`, `pcb_report.json`. +- Check: `pcb_net_match` + `check_placement` + `check_si`. +- UI: start, progress, merge findings nel report, filtro Layout, `rule_id` sulla card. +- Test: wrapper `run_pcb_checks` su fixture (decoupling 15 mm → `PE-PLC-001`; 1 mm → niente); pad net mismatch → `PE-LAY-001`; job busy helpers come `test_placement_pipeline.py`; pytest API start 400 senza PCB. + +**Done when:** upload `.kicad_pcb` → start PCB → finding visibili nel report; schema invariato. + +### Fase 2 — Inventario ricco (tracce, bus, Z0) + +- Tabella lunghezze + coppie + bus. +- Z0 per net signal con stackup (già `impedance_traces`); finding `PE-Z0-001` **solo** con target numerico (regola o netclass). +- Collegare `impedance_nets.json` al job PCB (oggi è side-effect dello schema `graph_build`). + +**Done when:** board con stackup mostra Z0 in UI; senza stackup, skip loggato, zero finding inventati. + +### Fase 3 — Plugin + eval PCB + +- Cad-bridge dopo `write_report`; click finding centra footprint in pcbnew (`plugins/kicad/periscope_plugin.py`). +- Golden keys su fixture (USB skew voluta → `PE-SI-001`; simple_project senza PCB → vuoto). +- Changelog Layout (voce distinta, non “un po’ di Periscope in più”). + +### Fase 4 — Non fare (parcheggio) + +- `placement_pack` / collisioni courtyard / export xy / muovere rame. +- Auto-place antenna in `.kicad_pcb`. +- 3W, creepage IEC, CPWG, isolation HV senza numero. +- LLM che “legge” il layout come review schema. +- Secondo ingresso Gerber/EasyEDA. + +--- + +## 6. Fuori scope (esplicito) + +- Auto-placement, packing mm, write-back pcbnew, “migliora il floorplan”. +- Autore di sbroglio / change width / length-tune. +- Simulazione SPICE / IBIS / EM / VSWR. +- Inventare millimetri, Ω, o V/mm. +- Plugin EasyEDA; parser JSON EasyEDA. +- Layout da `!PADS-POWERPCB`. +- Unire forzatamente job PCB nella run schema. +- Toccare ssh / keys / `sshd` (irrilevante a questo piano). + +--- + +## 7. Come si misura il “done” + +Fixture minima (non un USB inventato in produzione, ma un `.kicad_pcb` di test in `tests/`): + +1. C di decoupling a 15 mm da VDD con regola 2 mm → `PE-PLC-001`. +2. Stesso C a 1 mm → nessun finding proximity. +3. Coppia `USB_DP`/`USB_DM` con skew > `length_match` → `PE-SI-001`. +4. Pad `U1.1` net `+3V3` vs schema `GND` → `PE-LAY-001`. +5. Progetto senza PCB: job PCB rifiutato; report schema pulito. + +Niente LLM. Niente coordinate proposte. + +--- + +## 8. Relazione col piano esistente + +`piano-implementazione.md` Wave G (G0 ingest, G1 SI gated, G2 placement vs datasheet) è **questo** prodotto, riletto come job parallelo di **esame**. + +La roadmap Placement F2 (pack v1/v2/export) **non** entra in queste fasi. `placement_check` resta verifica; `placement_pack` resta skeleton gated e non si estende qui. + +Chats Cursor precedenti: nessuna decisione di “PCB error pipeline” come job; solo note RF (“auto-place rame nel PCB = dopo”). Questo piano è la decisione: **review**, non place.