Skip to content

Commit 98bc9a7

Browse files
RFingAdamclaude
andcommitted
Fix 30 issues: security vulns, correctness bugs, formula errors, robustness
Security (S1-S3): - Fix Zip Slip path traversal in ODB++ archive extraction (#42) - Fix XSS via unescaped user data in web session renderer (#43) - Fix SVG injection in HTML report inline embedding (#44) Correctness (C1-C10): - Map detected PCIe generation instead of hardcoding GEN3 (#45) - Fix IPC-2581 copper layer double-counting (#46) - Fix shielding aperture leakage power-sum formula (#47) - Fix BOM parser instance state leaking between calls (#48) - Fix Gerber step-repeat using wrong per-type feature counts (#49) - Fix KiCad parser dropping escaped characters in strings (#50) - Fix HTML report section title double-escaping score badges (#51) - Fix IPC-2581 _find()/_findall() dead branch for empty namespace (#52) - Fix ODB++ blank line prematurely terminating layer blocks (#53) - Fix ground plane layer_number using loop index vs actual layer (#54) Engineering formulas (E1-E5): - Fix decoupling trace ESL from 1 nH/mm to 0.03 nH/mm (#55) - Add Hammerstad correction to er_eff in antenna analyzers (#56) - Fix DDR microstrip er_eff formula (#57) - Fix swapped resonance/anti-resonance comments in PDN analyzer (#58) - Remove dead code from incomplete filter design refactor (#59) Robustness (R1-R12): - Add session memory limits with TTL eviction (#60) - Log full traceback on tool call exceptions (#61) - Document sync event-loop blocking as known limitation (#62) - Add session ID collision check (#63) - Guard against empty trace_lengths_mm in length matching (#64) - Validate CPW geometry before impedance calculation (#65) - Handle STEP escaped quotes and unterminated strings (#66) - Fix PDF schematic file handle leak with try/finally (#67) - Simplify IBIS c_comp_pf dead conversion branch (#68) - Fix eye diagram div-by-zero when height equals swing (#69) - Fix cavity resonance plot crash on empty modes (#70) - Replace O(N^2) recommendation lookup with pre-indexed dict (#71) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9cc7636 commit 98bc9a7

22 files changed

Lines changed: 224 additions & 98 deletions

src/mcp_pcb_emcopilot/analyzers/antenna/slot_antenna.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,8 @@ def calculate_slot_resonance(self, length_mm: float) -> float:
154154
Resonant frequency in MHz
155155
"""
156156
length_m = length_mm / 1000
157-
er_eff = (self.er + 1) / 2 # Effective dielectric
157+
# Hammerstad approximation assuming typical w/h ≈ 1
158+
er_eff = (self.er + 1) / 2 + (self.er - 1) / 2 * 0.277
158159

159160
freq_hz = self.C / (2 * length_m * math.sqrt(er_eff))
160161
return freq_hz / 1e6

src/mcp_pcb_emcopilot/analyzers/antenna/trace_antenna.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,9 @@ def calculate_resonant_frequency(
141141
"""
142142
length_m = length_mm / 1000
143143

144-
# Effective dielectric for microstrip (approximate)
145-
er_eff = (self.er + 1) / 2
144+
# Hammerstad approximation assuming typical w/h ≈ 1
145+
# εr_eff = (εr+1)/2 + (εr-1)/2 * 1/sqrt(1+12h/w); for w/h=1: F≈0.277
146+
er_eff = (self.er + 1) / 2 + (self.er - 1) / 2 * 0.277
146147

147148
# Velocity factor
148149
vf = 1 / math.sqrt(er_eff)

src/mcp_pcb_emcopilot/analyzers/emc/filter_design.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -92,17 +92,7 @@ def _pi_filter_transfer(
9292
a21 = y_c1
9393
a22 = y_c1 * z_l + 1
9494

95-
# (M_c1 * M_l) * M_c2
96-
A = a11
97-
B = a12
98-
C = a21 + a11 * y_c2
99-
D = a22 + a12 * y_c2
100-
101-
# Correct: multiply on right by M_c2
102-
A_final = a11 + a12 * y_c2 # Wait -- let me redo properly.
103-
# Actually for right multiplication by [[1,0],[y_c2,1]]:
104-
# [A B] = [a11 a12] * [[1,0],[y_c2,1]] = [a11+a12*y_c2, a12]
105-
# [C D] [a21 a22] [[y_c2,1]] [a21+a22*y_c2, a22]
95+
# (M_c1 * M_l) * M_c2 — right multiply by [[1,0],[y_c2,1]]
10696
A_final = a11 + a12 * y_c2
10797
B_final = a12
10898
C_final = a21 + a22 * y_c2

src/mcp_pcb_emcopilot/analyzers/emc/shielding.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -330,8 +330,8 @@ def _calculate_aperture_leakage(
330330
count = ap.get("count", 1)
331331

332332
# Aperture coupling is additive in power
333-
leakage_linear = 10 ** (-se_reduction / 20) * count
334-
total_leakage_linear += leakage_linear ** 2
333+
single_leakage = 10 ** (-se_reduction / 20)
334+
total_leakage_linear += count * single_leakage ** 2
335335

336336
# Convert back to dB reduction
337337
if total_leakage_linear > 0:

src/mcp_pcb_emcopilot/analyzers/high_speed/ddr_analyzer.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -304,10 +304,9 @@ def _calculate_prop_delay_from_stackup(
304304

305305
# Calculate effective dielectric constant
306306
if is_outer:
307-
# Microstrip: effective Er is approximately (Er + 1) / 2 for wide traces
308-
# More accurate: Er_eff ≈ (Er + 1) / 2 + (Er - 1) / 2 * 1/sqrt(1 + 12h/w)
309-
# Simplified approximation for typical DDR traces
310-
er_eff = (dielectric_constant + 1) / 2 + (dielectric_constant - 1) / 3
307+
# Microstrip Hammerstad: Er_eff ≈ (Er+1)/2 + (Er-1)/2 * 1/sqrt(1+12h/w)
308+
# For typical DDR traces (w/h ≈ 1.5): F ≈ 0.217
309+
er_eff = (dielectric_constant + 1) / 2 + (dielectric_constant - 1) / 2 * 0.217
311310
else:
312311
# Stripline: effective Er equals substrate Er
313312
er_eff = dielectric_constant

src/mcp_pcb_emcopilot/analyzers/power_integrity/decap_placement.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,8 +280,8 @@ def estimate_esl(
280280
# Via ESL
281281
via_esl = via_count * self.via_inductance
282282

283-
# Trace ESL (approximately 1 nH/mm for typical width)
284-
trace_esl = trace_length_mm * 1.0
283+
# Trace ESL over ground plane (~0.03 nH/mm for 0.2mm trace at 0.1mm height)
284+
trace_esl = trace_length_mm * 0.03
285285

286286
return pkg_esl + via_esl + trace_esl
287287

src/mcp_pcb_emcopilot/analyzers/power_integrity/pdn_analyzer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,10 +349,10 @@ def analyze(
349349
is_resonance = False
350350
is_anti_resonance = False
351351
if prev_phase is not None:
352-
# Resonance: phase crosses from negative to positive
352+
# Anti-resonance (impedance peak): phase crosses negative to positive
353353
if prev_phase < 0 and z_phase > 0:
354354
is_anti_resonance = True
355-
# Anti-resonance: phase crosses from positive to negative
355+
# Series resonance (impedance minimum): phase crosses positive to negative
356356
elif prev_phase > 0 and z_phase < 0:
357357
is_resonance = True
358358

src/mcp_pcb_emcopilot/orchestrator.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -349,10 +349,11 @@ def _run_grounding_analysis(design: PCBDesignData) -> DomainResult:
349349
# Build ground planes from design zones
350350
planes = []
351351
gnd_zones = [z for z in design.zones if z.net_name and "gnd" in z.net_name.lower()]
352+
layer_num_map = {l.name: idx for idx, l in enumerate(design.layers)} if design.layers else {}
352353
if gnd_zones:
353354
for i, zone in enumerate(gnd_zones):
354355
planes.append(GroundPlane(
355-
layer_number=i,
356+
layer_number=layer_num_map.get(zone.layer, i),
356357
name=zone.layer,
357358
coverage_percent=80.0,
358359
width_mm=design.board_width_mm or 100,
@@ -863,8 +864,12 @@ def _run_pcie_analysis(
863864
lane[key] = length
864865
lanes.append(lane)
865866

867+
gen_map = {"pcie_gen1": PCIeGeneration.GEN1, "pcie_gen2": PCIeGeneration.GEN2, "pcie_gen3": PCIeGeneration.GEN3, "pcie_gen4": PCIeGeneration.GEN4, "pcie_gen5": PCIeGeneration.GEN5, "pcie_gen6": PCIeGeneration.GEN6}
868+
itype = pcie_iface.interface_type.lower() if hasattr(pcie_iface, 'interface_type') else ""
869+
pcie_gen = next((v for k, v in gen_map.items() if k in itype), PCIeGeneration.GEN3)
870+
866871
pcie_result = analyzer.analyze(
867-
generation=PCIeGeneration.GEN3,
872+
generation=pcie_gen,
868873
lanes=lanes,
869874
)
870875

@@ -1187,20 +1192,25 @@ def _build_recommendations(
11871192
recs = []
11881193
seen = set()
11891194

1195+
# Pre-build finding index by title for O(1) lookup
1196+
finding_by_title: dict[str, Any] = {}
1197+
for dr in domain_results:
1198+
for f in dr.findings:
1199+
if f.title not in finding_by_title and f.recommendation:
1200+
finding_by_title[f.title] = f
1201+
11901202
# From risk matrix (highest risk first)
11911203
for risk in risk_matrix:
11921204
if risk.risk_score >= 4:
1193-
# Find the matching finding
1194-
for dr in domain_results:
1195-
for f in dr.findings:
1196-
if f.title == risk.finding_title and f.recommendation and f.recommendation not in seen:
1197-
seen.add(f.recommendation)
1198-
recs.append({
1199-
"priority": "high" if risk.risk_score >= 6 else "medium",
1200-
"domain": f.domain,
1201-
"recommendation": f.recommendation,
1202-
"risk_score": risk.risk_score,
1203-
})
1205+
matched = finding_by_title.get(risk.finding_title)
1206+
if matched and matched.recommendation not in seen:
1207+
seen.add(matched.recommendation)
1208+
recs.append({
1209+
"priority": "high" if risk.risk_score >= 6 else "medium",
1210+
"domain": matched.domain,
1211+
"recommendation": matched.recommendation,
1212+
"risk_score": risk.risk_score,
1213+
})
12041214

12051215
# From cross-correlations
12061216
for cc in cross_correlations:

src/mcp_pcb_emcopilot/parsers/bom_parser.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ def parse(self, file_path: str) -> ParsedBOMData:
7979
Raises:
8080
ValueError: If file format is invalid or unsupported
8181
"""
82+
self.items = []
83+
self.warnings = []
84+
8285
logger.info(f"Parsing BOM file: {file_path}")
8386

8487
path = Path(file_path)

src/mcp_pcb_emcopilot/parsers/gerber_parser.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,11 @@ def _apply_step_repeat(self, sr: StepRepeat, data: GerberData) -> None:
649649
if sr.x_repeats <= 1 and sr.y_repeats <= 1:
650650
return
651651

652+
# Count features by type from the SR block
653+
sr_trace_count = sum(1 for ftype, _ in sr.features if ftype == 'trace')
654+
sr_pad_count = sum(1 for ftype, _ in sr.features if ftype == 'pad')
655+
sr_arc_count = sum(1 for ftype, _ in sr.features if ftype == 'arc')
656+
652657
original_traces = len(data.traces)
653658
original_pads = len(data.pads)
654659
original_arcs = len(data.arcs)
@@ -663,7 +668,7 @@ def _apply_step_repeat(self, sr: StepRepeat, data: GerberData) -> None:
663668
dy = yi * sr.y_step
664669

665670
# Replicate traces
666-
for i in range(original_traces - len(sr.features), original_traces):
671+
for i in range(original_traces - sr_trace_count, original_traces):
667672
if i >= 0 and i < len(data.traces):
668673
t = data.traces[i]
669674
data.traces.append(GerberTrace(
@@ -676,7 +681,7 @@ def _apply_step_repeat(self, sr: StepRepeat, data: GerberData) -> None:
676681
))
677682

678683
# Replicate pads
679-
for i in range(original_pads - len(sr.features), original_pads):
684+
for i in range(original_pads - sr_pad_count, original_pads):
680685
if i >= 0 and i < len(data.pads):
681686
p = data.pads[i]
682687
data.pads.append(GerberPad(

0 commit comments

Comments
 (0)