Flag keepout tracks in the courtyard and mark the plan map OK.

PS-PLC-004 is a foreign net endpoint inside the KiCad courtyard. The lista↔codice table and sprints 0–10+ get an OK column where the hole is closed without invented millimetres.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-11 00:11:02 +02:00
co-authored by Cursor
parent 412c32e9b2
commit 796cd9d8a2
4 changed files with 152 additions and 46 deletions
+38
View File
@@ -7,6 +7,7 @@ min_via_count — no invented pad radius. same_layer (`PS-PLC-003`) uses
the boolean parameter plus footprint layers from the PCB. Crystals use
the same decoupling_proximity rule. Track length is shortest path on
segments vs max_distance_mm — no invented “much larger than euclidean”.
Keepout (`PS-PLC-004`) is a foreign net endpoint inside the courtyard.
"""
from __future__ import annotations
@@ -124,6 +125,8 @@ def check_placement(
)
elif kind == "thermal_via":
findings.extend(_thermal_via_finding(ref, comp, cons, rule, layout))
elif kind == "keepout":
findings.extend(_keepout_finding(ref, comp, cons, rule, graph, layout))
return findings
@@ -275,3 +278,38 @@ def _thermal_via_finding(ref, comp, cons, rule, layout: LayoutGraph) -> list[Fin
pins=[pin] if pin else [],
source_page=rule.get("source_page"),
)]
def _keepout_finding(ref, comp, cons, rule, graph: DesignGraph, layout: LayoutGraph) -> list[Finding]:
fp = layout.footprints.get(ref)
if not fp or len(fp.courtyard) < 3:
return []
pin_no = _pin_number(cons, str(rule.get("pin") or ""))
own = _net_for_pin(graph, ref, pin_no) if pin_no else None
if not own:
return []
foreign: list[str] = []
for s in layout.segments:
if not s.net or s.net == own:
continue
if _in_poly(s.start[0], s.start[1], fp.courtyard) or _in_poly(
s.end[0], s.end[1], fp.courtyard
):
foreign.append(s.net)
if not foreign:
return []
net = sorted(set(foreign))[0]
return [Finding(
designator=ref,
mpn=comp.mpn or cons.mpn,
aspect="placement",
finding=f"Track on {net} enters courtyard of {ref} (keepout on {own}).",
why="layout_rules kind=keepout.",
status="WARNING",
recommendation="Keep other nets out of the courtyard.",
source="placement_check",
rule_id="PS-PLC-004",
net=net,
pins=[pin_no],
source_page=rule.get("source_page"),
)]
+48 -46
View File
@@ -102,20 +102,20 @@ Passi:
## Mappa lista ↔ codice attuale
| Blocco utente | Già c’è | Buco |
| --- | --- | --- |
| 1 Plugin / telemetry / BOM match | Parser KiCad XML/sexp/`.kicad_sch` **singolo foglio**; wizard BOM | Hierarchie multi-file; uuid; plugin; **EasyEDA fuori scope** |
| 2 Datasheet / errata / OCR blocchi | Pintable+abs-max, PDF text+vision, excerpt per topic, quote_verify | Nessun RAG vendor; niente errata; vision non estrae clamp/ESD dal block diagram in schema strutturato |
| 3 Impedenze / stackup | — | Motore assente; niente PCB |
| 4 Filtri | Review LLM può parlarne | Nessun matcher topologico, niente \(f_c\) |
| 5 Capacità PI | Decoupling check **sulla net**; derating V | No DC bias; ESR/ESL; Cin/Cout; **niente distanza cap↔pin sul PCB** |
| 6 Elettrico | Pin mux; I2C/reset pull-up; power tree UI; LED current | Sequencing assente; pull-up non dimensionati (solo presenza); drop IR assente |
| 7 RF | Review ruolo parte” / bias-T | Nessun matching 50 Ω, niente clearance |
| 8 HV / isolation | — | Serve layout + profilo normativo |
| 9 Termico | Excerpt topic thermal | Nessun \(T_j\), niente P=I²R resistori |
| 10 SI / DNP | Fingerprint skip IC; skipped/not_reviewed | DNP non è un modello; niente length/crosstalk |
| 11 Lifecycle | DigiKey/LCSC/Mouser per datasheet e passivi | Nessun EOL/NRND/RoHS in report |
| 13 Placement da datasheet | Review può citare “place close to pin”; vision su application pages | Nessuna misura mm sul `.kicad_pcb`; nessuna struttura `layout_rules` estratta |
| Blocco utente | Già c’è | Buco | Stato |
| --- | --- | --- | --- |
| 1 Plugin / telemetry / BOM match | Parser KiCad XML/sexp/`.kicad_sch` gerarchico; wizard BOM; `plugins/kicad/` + cad-bridge JSON | EasyEDA fuori scope; uuid/sheet sul plugin da verificare su board reale | **OK** |
| 2 Datasheet / errata / OCR blocchi | Pintable, excerpt, quote_verify, errata, `internal_features` | Nessun RAG vendor | **OK** |
| 3 Impedenze / stackup | ImpedenceFinder + tab Impedance + `.kicad_dru` | CPWG non nel vendor; niente Z0 sul rame del PCB | **OK** |
| 4 Filtri | `check_filters` (solo con poli/numeri in specs) | Niente \(f_c\) inventata | **OK** |
| 5 Capacità PI | Decoupling sulla net; derating V; DC-bias/ESR se c’è il numero; mm sul PCB (`PS-PLC-001`) | — | **OK** |
| 6 Elettrico | Pin mux; I2C/reset pull-up; LED; sequencing/IR/power margin se c’è il parametro | Senza numero in specs → skip | **OK** |
| 7 RF | Tab impedenza 50 Ω; review ruolo parte | Nessun clearance CPW inventato | parziale |
| 8 HV / isolation | — | Serve `layout_rules` + V/mm dal datasheet, non IEC inventato | — |
| 9 Termico | `check_thermal` se θJA/I sono in specs; via courtyard vs `min_via_count` | Niente \(T_j\) senza parametro | **OK** |
| 10 SI / DNP | DNP enable; `PS-SI-001` solo con `length_match` mm | Niente 3W/crosstalk inventati | **OK** |
| 11 Lifecycle | `lifecycle_check` su cache distributore (EOL/NRND/RoHS esplicito) | Niente equivalente LLM | **OK** |
| 13 Placement da datasheet | `layout_rules` + `.kicad_pcb`: PLC-001…004, same_layer, crystal, keepout, path | 3W/creepage/CPW/isolation senza numero | **OK** |
---
@@ -411,11 +411,13 @@ Passi:
Passi:
1. Length matching / intra-pair skew vs limite datasheet (USB/HDMI/PCIe).
2. 3W: distanza centro-centro vs W aggressore.
3. Creepage/clearance: profilo IEC 62368 (pollution, RMS V dai net).
4. Isolation barrier: bbox isolator + divieto piste LV nel courtyard HV.
5. CPW: gap verso GND copper vs valore del calcolatore D2.
1. Length matching / intra-pair skew vs limite datasheet (USB/HDMI/PCIe). **OK** (`PS-SI-001`, solo `length_match` mm).
2. 3W: distanza centro-centro vs W aggressore. — skip senza numero (non IEC/USB folklore).
3. Creepage/clearance: profilo IEC 62368 (pollution, RMS V dai net). — skip senza V/mm nel datasheet.
4. Isolation barrier: bbox isolator + divieto piste LV nel courtyard HV. — skip senza regola.
5. CPW: gap verso GND copper vs valore del calcolatore D2. — CPWG non nel vendor.
**G2. Placement vs datasheet (piste, decoupling, thermal)**
**G2. Placement vs datasheet (piste, decoupling, thermal)**
@@ -423,13 +425,13 @@ Lo schema dice se C12 è sul net VDD. Il PCB dice se C12 è a 8 mm dal pad. Ques
Passi:
1. Per ogni pin alimentazione del pintable: footprint pad xy sul `.kicad_pcb`; condensatori sul medesimo net (grafo); distanza euclidea pad-cap (pin cap verso GND/VDD).
2. Se `max_distance_mm` estratto: ERROR/WARNING se oltre. Se assente: WARNING oltre default configurabile, testo “datasheet non specifica mm; usato default 3 mm”.
3. Via in pad / via sotto EP: contare via nel courtyard del thermal pad vs `min_via_count`.
4. Stesso layer: se `same_layer: true` e il cap è sullaltro lato senza via sotto il pin → WARNING.
5. Piste: lunghezza net VDD dal pin al cap (somma segmenti) come proxy di “loop area”; se >> distanza euclidea, c’è un giro largo.
6. Crystal: cap load vs pin XIN/XOUT (stessa metrica di distanza), se X1 è nel grafo.
7. Finding `PS-PLC-001` con `pins`, `net`, pagina datasheet. Plugin: focus footprint su pcbnew.
1. Per ogni pin alimentazione del pintable: footprint pad xy sul `.kicad_pcb`; condensatori sul medesimo net (grafo); distanza euclidea pad-cap (pin cap verso GND/VDD). **OK**
2. Se `max_distance_mm` estratto: ERROR/WARNING se oltre. Se assente: skip (niente default 3 mm). **OK**
3. Via in pad / via sotto EP: contare via nel courtyard del thermal pad vs `min_via_count`. **OK** (`PS-PLC-002`)
4. Stesso layer: se `same_layer: true` e il cap è sullaltro lato senza via sotto il pin → WARNING. **OK** (`PS-PLC-003`)
5. Piste: lunghezza net dal pin al cap = shortest path sui segmenti vs `max_distance_mm`. **OK**
6. Crystal: cap load vs pin XIN/XOUT (stessa metrica). **OK** (`X1`/`C9`/`C10`)
7. Finding `PS-PLC-001``004` con `pins`, `net`; plugin focus pcbnew. **OK** (keepout = `PS-PLC-004`)
Non confrontare una foto del layout TI con il board pixel-a-pixel. Solo vincoli numerici/topologici.
@@ -456,19 +458,19 @@ Commenti esistono: non rifarli, agganciarli allo stato.
Non parallelizzare A0 schema finding con G.
| Sprint (indicativo) | Contenuto | Dipende da |
| --- | --- | --- |
| 0 | Schema finding + eval + smoke V4.1 | — |
| 1 | A1 KiCad GA + A2 BOM match | 0 |
| 2 | B3B5 pull-up/decoupling/Cin-Cout | 0 |
| 3 | B6B9 DC bias, ESR, filtri, termico | 2 |
| 4 | B1B2 power drop/sequencing + B10 DNP | 1 |
| 5 | A3 plugin KiCad + E2 JSON | 1, 0 |
| 6 | C2 errata + C3 internal features + **C4 layout_rules** | eval C1 |
| 7 | D1D2 calcolatrice impedenza + export netclass | — |
| 8 | F lifecycle | APIs già presenti |
| 9 | H review workflow / ECO | 0 |
| 10+ | G layout `.kicad_pcb` + G2 placement datasheet | A1 + C4 |
| Sprint (indicativo) | Contenuto | Dipende da | Stato |
| --- | --- | --- | --- |
| 0 | Schema finding + eval + smoke V4.1 | — | **OK** |
| 1 | A1 KiCad GA + A2 BOM match | 0 | **OK** |
| 2 | B3B5 pull-up/decoupling/Cin-Cout | 0 | **OK** |
| 3 | B6B9 DC bias, ESR, filtri, termico | 2 | **OK** |
| 4 | B1B2 power drop/sequencing + B10 DNP | 1 | **OK** |
| 5 | A3 plugin KiCad + E2 JSON | 1, 0 | **OK** |
| 6 | C2 errata + C3 internal features + **C4 layout_rules** | eval C1 | **OK** |
| 7 | D1D2 calcolatrice impedenza + export netclass | — | **OK** |
| 8 | F lifecycle | APIs già presenti | **OK** |
| 9 | H review workflow / ECO | 0 | **OK** |
| 10+ | G layout `.kicad_pcb` + G2 placement datasheet | A1 + C4 | **OK** (G1 3W/creepage/CPW/HV ancora skip senza numero) |
Stima onesta: Wave AB (schema) sono il ritorno; G è un secondo prodotto. Non promettere creepage nel plugin schema.
@@ -499,12 +501,12 @@ Stima onesta: Wave AB (schema) sono il ritorno; G è un secondo prodotto. Non
## Prossimo passo concreto (quando si apre lo sviluppo)
Sprint 0, in questordine:
Sprint 010+ del piano di lavoro sono **OK** nel codice (parametro o skip).
1. Estensione `Finding` (`rule_id`, `pins`, `net`).
2. Eval script `simple_project` (finding count + citation).
3. Flatten `.kicad_sch` gerarchico (fixture multi-foglio).
4. Upload `.kicad_pcb` accanto allo schema (parse, ancora senza check SI).
5. BOM mismatch finding.
Resta, senza inventare numeri:
Il plugin KiCad aspetta uuid + sheet path. Placement decoupling in mm e 3W aspettano `.kicad_pcb` + `layout_rules`.
1. HV / isolation (blocco 8) se il datasheet dà V/mm o keepout HV.
2. 3W / creepage / CPW solo con numero in `layout_rules` o dal calcolatore D2.
3. Un `.kicad_pcb` reale di progetto (non fixture USB inventata) per vedere `PS-PLC`/`PS-SI` sul board.
Il plugin KiCad aspetta ancora verifica uuid + sheet su un progetto multi-foglio vero.
+6
View File
@@ -2,6 +2,12 @@
What's new in Pinscope.
## 2.25.0 — 2026-09-10 — Keepout courtyard
`layout_rules` `keepout` flags a foreign net whose track endpoint is inside the KiCad courtyard. Own net and missing courtyard skip. No invented analog/digital classes.
- [New] `PS-PLC-004` WARNING. Tests use `X1` / `/HFXIN` / `GND` from `simple_project`.
## 2.24.0 — 2026-09-10 — Crystal load caps and track path
Load caps on XIN/XOUT use the same `max_distance_mm` as decoupling. If the PCB has segments on the net, the limit is shortest-path length, not a guessed “loop is too big” ratio.
+60
View File
@@ -218,3 +218,63 @@ def test_track_path_longer_than_max_distance_mm_is_ps_plc_001():
plc = [f for f in findings if f.rule_id == "PS-PLC-001"]
assert len(plc) == 1
assert plc[0].net == "/HFXIN"
def _xtal_keepout_cons():
mpn = "AV08000301"
return {
mpn: ComponentConstraints(
mpn=mpn,
pintable=[Pin(number=1, name="/HFXIN")],
absolute_maximum_ratings=[],
rules=[],
layout_rules=[{"kind": "keepout", "pin": "1"}],
)
}
def _x1_keepout_layout(*, net: str, courtyard=True):
poly = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] if courtyard else []
return LayoutGraph(
footprints={
"X1": LayoutFootprint(
reference="X1", x=0, y=0, layer="F.Cu",
pads=[LayoutPad(number="1", x=0.0, y=0.0, net="/HFXIN")],
courtyard=poly,
),
},
segments=[
LayoutSegment(
start=(0.4, 0.4), end=(0.6, 0.4),
width=0.2, layer="F.Cu", net=net,
),
],
)
def test_keepout_foreign_track_in_courtyard_is_ps_plc_004():
findings = check_placement(
_graph(),
_xtal_keepout_cons(),
_x1_keepout_layout(net="GND"),
)
plc = [f for f in findings if f.rule_id == "PS-PLC-004"]
assert len(plc) == 1
assert plc[0].designator == "X1"
assert plc[0].net == "GND"
def test_keepout_own_net_in_courtyard_is_silent():
assert check_placement(
_graph(),
_xtal_keepout_cons(),
_x1_keepout_layout(net="/HFXIN"),
) == []
def test_keepout_without_courtyard_is_silent():
assert check_placement(
_graph(),
_xtal_keepout_cons(),
_x1_keepout_layout(net="GND", courtyard=False),
) == []