← All Pull Requests

PCBWay 4L 1oz: sourced KiCad rules, severity error, separate annular rings, 0.45 mm hole spacing, board clearance floor (DRAFT; replaces #6; merge blocked) #7

Closed Draft opened by Josh Aberdeen 2026-11-01

DRAFT, NOT FOR MERGE. Merge is blocked by the items under "Merge blockers". This replaces PR 6 (global 379), which is being closed: PR 6 contained an unconditional clearance rule that overrides netclass clearance (see below).

What this proposes (self-contained for PCBWay standard 4-layer 1 oz)

Source: tools/generate.py, rules/records/pcbway.json, tools/test_pcbway_4l.py. Generated: kicad/pcbway-4L.kicad_dru, kicad/pcbway-4L.board-setup.json.

  • KiCad keyword hole_size (not hole, which KiCad 10 rejects in all published files).
  • Every emitted rule is an explicit (severity error) (hole-to-hole and silk were non-gating warnings).
  • Separate via and PTH annular rings: via A.Type == 'Via' 0.15 mm; PTH pad A.Type == 'Pad' && A.Pad_Type == 'Through-hole' 0.254 mm.
  • Hole-to-hole 0.45 mm, all pairs. Inner hole-to-copper 0.1778 mm (hole_clearance, no layer qualifier; a layer qualifier made KiCad drop the whole file). Silk height 0.8 mm and width 0.15 mm.
  • Track width 0.127 mm (outer 1 oz, all layers). Copper to edge 0.25 mm. Drill 0.2 mm. Via pad 0.5 mm (derived).
  • NO unconditional clearance rule. The 0.1524 mm clearance floor is carried as the board minimum clearance (minClearanceMm in board setup), which applies over netclasses.

Netclass finding (why PR 6 is closed) - measured in KiCad 10 DRC, isolated coupons

  • Default netclass clearance 0.2 mm. Two tracks, 0.18 mm gap:
    • no custom rules file: violation (clearance 0.2, actual 0.18) (cpn_nc_nodru).
    • with a global clearance (min 0.1524) rule (PR 6): NO violation (cpn_nc). The rule weakens the netclass.
    • with the rule removed and board minimum 0.1524 set: violation against the netclass 0.2 (cpn_fb).
  • Floor test: netclass Default set to 0.1, board minimum 0.1524, gap 0.14: violation "board minimum clearance 0.1524 mm" (cpn_fd). Netclass 0.2, gap 0.14: violation against netclass (cpn_fa). Gap 0.25: clean (cpn_fc).
  • Conclusion: the floor is enforced without weakening any netclass; a netclass stricter than the floor still applies.

Sources (archived 2026-11-02; SHA-256 of archived bytes)

Disagreements (stricter value taken; weaker value recorded, not used): PTH ring 10 mil (0.254) vs 0.20 (tolerances page); hole spacing 16 mil (0.4064) / 11 mil (0.2794) vs 0.45 / 0.30 (2025 page) -> 0.45 all pairs; drill 0.15 vs 0.2 (mobile) -> 0.2.

Verified

  • Parser: 55/55 vendor .kicad_dru from the revised generator parse in KiCad 10 (validation-pr-final2.json, SHA-256 6c30d1b8f03f97e28b455f577c495d23d966e999b42a8b2f9598690e57a7a8be).
  • Tests: test_pcbway_4l.py 11/11 (includes: no hole keyword, all rules explicit error, via and PTH rings separate, hole spacing 0.45 all pairs, inner hole-to-copper emitted, silk, edge, drill, via diameter, record carries source hashes, no unconditional clearance, board floor 0.1524). PR #2's test_constraints.py is not included (it remains in #2).
  • Native DRC, violating coupon (cpn_v4) with the revised rules and board floor: named error categories for via annular, PTH annular, inner hole-to-copper, hole-to-hole, via hole size, via diameter, track width, copper edge, silk thickness and silk height; clearance found against the netclass and board floor (drc-cpn_v4-floor.json, SHA-256 7537bb292acc9c1fa8e0a98b2168d3bcf3145ea3019b28931955f9f7ebaa49b5).
  • Compliant control (cpn_v4c, pad gap 0.25 mm, meeting the 0.2 mm netclass as well as the vendor floor): zero named violations and zero non-artifact findings (drc-cpn_v4c-floor2.json, SHA-256 9e9f59ac0fa3aefd2953e08cd2c4fa6200208855801efaee79c5503985a55ae3).
  • Netclass and floor evidence: drc-cpn_nc, drc-cpn_nc_nodru, drc-cpn_fa, drc-cpn_fb, drc-cpn_fc, drc-cpn_fd (hashes in the review folder).

Merge blockers (explicit; do not merge until resolved)

  1. Pad-to-zone clearance is NOT verified: a filled pour 0.10 mm from a pad produced no violation (earlier DRC runs). Board-stage check; not claimed here.
  2. Solder-mask bridge is NOT verified: solder_mask_min_width set in the board produced no violation at a 0.08 mm gap. Board-stage check; not claimed here.
  3. Netclass-zone interaction and netclass-specific limits are NOT tested beyond the Default netclass. The floor mechanism depends on the board setup being applied (kicad_board_rules minClearance); that application is a board-stage step.
  4. The generator change applies to all vendors (clearance rule removed, severity added). Only the PCBWay 4-layer outputs are included. Other vendors' outputs must be regenerated on merge; those are not included here.
  5. Pre-existing drift: main's committed Fusion, Altium, README table and index for some vendors do not match main's own generator output (e.g. 3rd party fab 2L pad value). This PR does not change or include them. Maintainers must resolve that drift separately.
  6. Maintainer confirmation needed for the 0.45 mm hole spacing (2025 official page) over the 16 mil capabilities table, and for the 0.254 mm ring over the 0.20 mm tolerances value.

🤖 Generated with Claude Code

Diff Skip to comments (4)

--- a/tools/generate.py+++ b/tools/generate.py@@ -1,573 +1,568 @@⋯ 63 unchanged lines ⋯     drill = via.get("drillMm")     if drill is None and rec.get("minDrill"):         drill = rec["minDrill"]["mm"]+    if drill is not None and rec.get("minDrill"):+        drill = max(drill, rec["minDrill"]["mm"])     pad = via.get("padMm") if via else None-    if pad is None and drill is not None and rec.get("annularRing"):-        pad = round(drill + 2 * rec["annularRing"]["viaMm"], 4)+    # Each field is an independent minimum, not a guaranteed legal pair.+    ring = (rec.get("annularRing") or {}).get("viaMm")+    if drill is not None and ring is not None:+        required = round(drill + 2 * ring, 4)+        pad = max(pad or 0, required)     return drill, pad  ⋯ 293 unchanged lines ⋯ # ---------------------------------------------------------------- KiCad  def gen_kicad_dru(vendor, rec):+    """Export source constraints for KiCad 9/10; native DRC qualification is separate."""     tw = rec["minTraceWidth"]["mm"]     sp = rec["minSpacing"]["mm"]     dr = rec["minDrill"]["mm"]     via_d, via_p = effective_via(rec)-    c2e = g(rec, "copperToBoardEdge")-    silk = g(rec, "silkLineWidth")-    mask = g(rec, "maskExpansion")-    layers = rec["layerCount"]-    desc = description_text(vendor, rec)-    out = [-        "(version 1)",-        "",-        "# " + desc.replace("\n", "\n# "),-        "",-        '(rule "min-track-width"',-        f"  (constraint track_width (min {fmm(tw)}mm))",-        f"  (layer \"{'*.Cu' if layers == 2 else '*.Cu'}\")",-        "  (condition \"A.type == 'track'\"))",-        "",-        '(rule "min-clearance"',-        f"  (constraint clearance (min {fmm(sp)}mm))",-        "  )" if vendor == "JLCPCB" else "  (condition \"A.type == 'track' && B.type == 'track'\"))",-        "",-        '(rule "min-hole"',-        f"  (constraint hole (min {fmm(dr)}mm))",-        "  (condition \"A.type == 'pad' || A.type == 'via'\"))",-        "",-    ]-    if via_p:-        out += [-            '(rule "min-via-diameter"',-            f"  (constraint via_diameter (min {fmm(via_p)}mm))",-            "  (condition \"A.type == 'via'\"))",-            "",-        ]-    if c2e:-        out += [-            '(rule "min-edge-clearance"',-            f"  (constraint edge_clearance (min {fmm(c2e)}mm))",-            "  (condition \"A.type == 'track'\"))",-            "",-        ]-    if silk:-        out += [-            '(rule "min-silk-thickness"',-            f"  (constraint text_thickness (min {fmm(silk)}mm))",-            "  (layer \"*.SilkS\")",-            "  (condition \"A.type == 'text' || A.type == 'graphic'\"))",-            "",-        ]+    out = ["(version 1)", "", "# " + description_text(vendor, rec).replace("\n", "\n# "), ""]++    def rule(name, kind, minimum, condition=None, layer=None, severity=None):+        if minimum is None:+            return+        rows = [f'(rule "{name}"', f'  (severity {severity or "error"})', f'  (constraint {kind} (min {fmm(minimum)}mm))']+        if layer:+            rows.append(f'  (layer "{layer}")')+        if condition:+            rows.append(f'  (condition "{condition}")')+        rows[-1] += ")"+        out.extend(rows + [""])++    # Unconditional electrical clearance covers tracks, arcs, pads, vias and zones.+    # KiCad evaluates clearance on different nets; do not add type filters here.+    rule("min-track-width", "track_width", tw)+    # NO unconditional clearance rule: a custom clearance rule overrides netclass clearance (measured: a 0.2 mm netclass+    # passed a 0.18 mm gap once a 0.1524 mm global rule was present). The vendor clearance is carried as the BOARD minimum+    # clearance (board setup, minClearanceMm), which is a floor over netclasses (measured: 0.1 mm netclass, 0.14 mm gap fails+    # against the 0.1524 mm board minimum).+    # Later specific rules override earlier general rules in KiCad.+    pp = g(rec, "minPadSpacing")+    if pp is not None:+        rule("min-pad-clearance", "clearance", max(sp, pp), "A.Type == 'Pad' && B.Type == 'Pad'")+    rule("min-hole", "hole_size", dr)+    rule("min-via-hole", "hole_size", via_d, "A.Type == 'Via'")+    rule("min-via-diameter", "via_diameter", via_p, "A.Type == 'Via'")+    rule("min-via-annular-width", "annular_width", g(rec, "annularRing", "viaMm"), "A.Type == 'Via'")+    rule("min-pad-annular-width", "annular_width", g(rec, "annularRing", "padMm"), "A.Type == 'Pad' && A.Pad_Type == 'Through-hole'")+    rule("min-edge-clearance", "edge_clearance", g(rec, "copperToBoardEdge"))+    # KiCad reports hole_to_hole as a warning unless the rule states severity: a warning does not fail a board+    rule("min-hole-to-hole", "hole_to_hole", g(rec, "holeToHole"), severity="error")+    rule("min-hole-to-inner-copper", "hole_clearance", g(rec, "minHoleToInnerCopper", "mm"))+    rule("min-silk-height", "text_height", g(rec, "silkMinHeightMm", "mm"), "A.Type == 'Text' || A.Type == 'Text Box'", "*.SilkS")+    rule("min-silk-thickness", "text_thickness", g(rec, "silkLineWidth"), "A.Type == 'Text' || A.Type == 'Text Box'", "*.SilkS")     return "\n".join(out)  ⋯ 6 unchanged lines ⋯         "boardSetupConstraints": {             "minTrackWidthMm": rec["minTraceWidth"]["mm"],             "minClearanceMm": rec["minSpacing"]["mm"],+            "minPadClearanceMm": g(rec, "minPadSpacing"),             "minViaDrillMm": via_d,             "minViaDiameterMm": via_p,             "minHoleMm": rec["minDrill"]["mm"],⋯ 4 unchanged lines ⋯             "silkMinLineWidthMm": g(rec, "silkLineWidth"),             "maskExpansionMm": g(rec, "maskExpansion"),         },-        "notes": "Apply as Board Setup, Constraints. The matching .kicad_dru file carries the same limits as custom rules; the two are redundant on purpose so both the GUI constraints and kicad-cli DRC enforce them.",+        "notes": "Apply as Board Setup, Constraints. Apply the matching .kicad_dru alongside the board and project with the same stem. This JSON is an adapter contract, not a native .kicad_pro file. Separate pad/via rules and pad-pair clearance need custom rules. Confirm loaded source hashes and native negative-control findings before claiming enforcement.",         "sources": rec.get("sources", []),     }-    if rec.get("silkscreenPolicy"):-        setup["silkscreenPolicy"] = rec["silkscreenPolicy"]     return json.dumps(setup, indent=2) + "\n"  ⋯ 122 unchanged lines ⋯  if __name__ == "__main__":     main()+--- a/tools/test_pcbway_4l.py+++ b/tools/test_pcbway_4l.py@@ -0,0 +1,53 @@+"""PCBWay standard 4-layer 1 oz: sourced limits must appear in the generated KiCad rules, each as an explicit error.+Source archive SHA-256 values are the ones recorded in the PCBDS r7 proof. Syntax-only passes are NOT accepted as proof: see DRC coupon evidence."""+import unittest, importlib.util, json, re+from pathlib import Path+BASE = Path(__file__).resolve().parents[1]+spec = importlib.util.spec_from_file_location('rules', BASE / 'tools' / 'generate.py')+g = importlib.util.module_from_spec(spec); spec.loader.exec_module(g)+REC = [r for r in json.loads((BASE / 'rules/records/pcbway.json').read_text())['records'] if r['process'] == 'standard' and r['layerCount'] == 4][0]+TXT = g.gen_kicad_dru('PCBWay', REC)+STD_SHA = '694ce30266e8c0bde8173d590c3ac71e6d928eb730fad3555e6a1dd298e0a980'+LIM_SHA = '9cebdeed859c3c61d4e104185aa5e6a54fb1a5c6ac5864591fbd5993479145e1'+TOL_SHA = '51142afe99832cc0ee41af19bf7d4c552a2658da466f04850523fab09bb66948'+def rule(name):+    m = re.search(r'\(rule "%s"\n(.*?)(?:\n\n|\Z)' % re.escape(name), TXT, flags=re.S)+    return m.group(1) if m else None+class PCBWay4L(unittest.TestCase):+    def test_no_invalid_keyword_or_layer_qualifier(self):+        self.assertNotIn('(constraint hole ', TXT)+        self.assertNotIn('(layer "inner")', TXT)+    def test_every_rule_is_explicit_error(self):+        blocks = re.findall(r'\(rule "[^"]+"\n(.*?)(?:\n\n|\Z)', TXT, flags=re.S)+        self.assertTrue(blocks)+        for b in blocks: self.assertIn('(severity error)', b)+    def test_track_width_outer_1oz_applied_globally(self):+        self.assertIn('(constraint track_width (min 0.127mm))', rule('min-track-width'))+    def test_no_unconditional_clearance_rule_and_board_floor_carries_0_1524(self):+        self.assertIsNone(rule('min-clearance'))  # would override netclass clearance (measured)+        bs = json.loads(g.gen_kicad_board_setup('PCBWay', REC))+        self.assertEqual(bs['boardSetupConstraints']['minClearanceMm'], 0.1524)+    def test_via_and_pth_annular_rings_are_separate(self):+        self.assertIn("A.Type == 'Via'", rule('min-via-annular-width'))+        self.assertIn('(constraint annular_width (min 0.15mm))', rule('min-via-annular-width'))+        self.assertIn("A.Pad_Type == 'Through-hole'", rule('min-pad-annular-width'))+        self.assertIn('(constraint annular_width (min 0.254mm))', rule('min-pad-annular-width'))+    def test_hole_spacing_at_least_0_45mm_for_all_pairs(self):+        self.assertIn('(constraint hole_to_hole (min 0.45mm))', rule('min-hole-to-hole'))+        self.assertIsNone(re.search(r'hole_to_hole[^\n]*condition', TXT))+    def test_inner_hole_to_copper_7mil_is_emitted(self):+        self.assertIn('(constraint hole_clearance (min 0.1778mm))', rule('min-hole-to-inner-copper'))+    def test_silk_height_and_width(self):+        self.assertIn('(constraint text_height (min 0.8mm))', rule('min-silk-height'))+        self.assertIn('(constraint text_thickness (min 0.15mm))', rule('min-silk-thickness'))+    def test_copper_edge_and_drill_and_via_diameter(self):+        self.assertIn('(constraint edge_clearance (min 0.25mm))', rule('min-edge-clearance'))+        self.assertIn('(constraint hole_size (min 0.2mm))', rule('min-hole'))+        self.assertIn('(constraint via_diameter (min 0.5mm))', rule('min-via-diameter'))+    def test_record_carries_source_archive_hashes(self):+        shas = [s.get('archiveSha256') for s in REC.get('sources', [])]+        for need in (STD_SHA, LIM_SHA, TOL_SHA): self.assertIn(need, shas)+    def test_pth_ring_is_strictest_of_sources(self):+        self.assertGreaterEqual(REC['annularRing']['padMm'], 0.254)  # tolerances page 0.20 is weaker and is not used+if __name__ == '__main__': unittest.main(verbosity=2)+--- a/rules/records/pcbway.json+++ b/rules/records/pcbway.json@@ -1,616 +1,653 @@⋯ 105 unchanged lines ⋯       "process": "standard",       "layerCount": 4,       "minTraceWidth": {-        "mm": 0.1016,-        "mil": 4.0,-        "condition": "inner layer, 1oz (35um) copper, before compensation; inner 0.5oz: 4mil, 2oz: 6mil, 3oz: 8mil. Outer layer 1oz: 5mil"-      },-      "minSpacing": {         "mm": 0.127,         "mil": 5.0,-        "condition": "inner layer, 1oz (35um) copper, before compensation; inner 0.5oz: 5mil (4/4mil class), 2oz: 8mil, 3oz: 11mil. Outer layer 1oz: 6mil"+        "condition": "4L 1oz: outer conventional 5mil (0.127) applied to every layer; inner 4mil (0.1016) is looser, not used. Sources: capabilities table (std sha 694ce30266e8); mobile page suggests 6mil. "       },+      "minSpacing": {+        "mm": 0.1524,+        "mil": 6.0,+        "condition": "4L 1oz: outer conventional 6mil (0.1524) applied to every layer; inner 5mil (0.127) looser, not used. Sources: capabilities table (std sha 694ce30266e8); mobile page suggests 6mil."+      },       "minDrill": {         "mm": 0.2,         "mil": 7.87⋯ 1 unchanged line ⋯       "minVia": {         "drillMm": 0.2,         "drillMil": 7.87,-        "padMm": 0.454,-        "padMil": 17.87+        "padMm": 0.5,+        "padMil": 19.69,+        "note": "derived: drill 0.2 + 2 x 0.15 via ring (vendor page does not publish a via pad)"       },       "annularRing": {-        "viaMm": 0.127,-        "viaMil": 5.0,-        "note": "via, 1oz outer copper; component hole 1oz: 10mil"+        "viaMm": 0.15,+        "viaMil": 6.0,+        "padMm": 0.254,+        "padMil": 10.0,+        "note": "viaMm: pads with vias, 0.15mm (6mil): capabilities page AND manufacturing-tolerances page (0.15 via ring) agree. padMm: 1oz outer component-hole ring 10mil (0.254) from the capabilities table; the manufacturing-tolerances page gives 0.20 for the PTH ring, which is WEAKER and not used. Source disagreement recorded in notes."       },       "copperToBoardEdge": {         "mm": 0.25,⋯ 1 unchanged line ⋯         "condition": "line to board edge, CNC routing"       },       "holeToHole": {-        "mm": 0.2794,-        "mil": 11.0,-        "condition": "via hole to via hole, via diameter 0.45mm or less; component holes: 16mil (0.4064mm)"+        "mm": 0.45,+        "mil": 17.72,+        "condition": "different-net plated holes: 0.45mm (limited_holes_spacing page, last updated 2025-06-25) - stricter than the capabilities table (16mil = 0.4064 PTH; 11mil = 0.2794 via). Via-to-via 0.30mm on that page is looser than 0.45 and is not used separately: 0.45 applies to all pairs (conservative)."       },       "maskExpansion": {         "mm": 0.0508,⋯ 49 unchanged lines ⋯         {           "url": "https://www.pcbway.com/capabilities.html",           "dateRead": "2026-09-03"+        },+        {+          "url": "https://www.pcbway.com/capabilities.html",+          "dateRead": "2026-11-02",+          "archiveSha256": "694ce30266e8c0bde8173d590c3ac71e6d928eb730fad3555e6a1dd298e0a980",+          "note": "observational archive; page footer '© 2024 PCBWay' only; no vendor revision date"+        },+        {+          "url": "https://www.pcbway.com/helpcenter/Engineering_Questions/limited_holes_spacing.html",+          "dateRead": "2026-11-02",+          "archiveSha256": "9cebdeed859c3c61d4e104185aa5e6a54fb1a5c6ac5864591fbd5993479145e1",+          "note": "official PCBWay Engineering Questions page; states last updated 2025-06-25"+        },+        {+          "url": "https://www.pcbway.com/pcb_prototype/PCB_Manufacturing_tolerances.html",+          "dateRead": "2026-11-02",+          "archiveSha256": "51142afe99832cc0ee41af19bf7d4c552a2658da466f04850523fab09bb66948",+          "note": "official PCBWay tolerances page; PTH ring 0.20 and via ring 0.15 (PTH ring weaker than capabilities table, not used)"+        },+        {+          "url": "https://m.pcbway.com/capabilities.html",+          "dateRead": "2026-11-02",+          "archiveSha256": "d7be6170136dad3df83304c5e34dab85775f7df80e632d8de8eae85969da3357",+          "note": "mobile capabilities page (summary only): drill 0.2 min, trace/spacing 4mil min with 6mil suggested, annular 0.15"         }-      ]+      ],+      "minHoleToInnerCopper": {+        "mm": 0.1778,+        "mil": 7.0,+        "condition": "4-layer inner-layer isolation ring (hole to inner layer conductor, before compensation) 7mil, per capabilities table 'Minimum isolation ring of Inner layer' 4L >= 7mil (archive sha 694ce302...)"+      },+      "silkMinHeightMm": {+        "mm": 0.8,+        "condition": "minimum legend character height 0.8mm (width:height 1:5), per capabilities page silkscreen row (archive sha 694ce302...)"+      }     },     {       "process": "standard",⋯ 412 unchanged lines ⋯     "https://www.pcbway.com/advanced-pcb.html"   ] }+--- a/kicad/pcbway-4L.kicad_dru+++ b/kicad/pcbway-4L.kicad_dru@@ -1,46 +1,77 @@⋯ 1 unchanged line ⋯  # PCBWay 4-layer design rules (standard process) # -# Min trace width: 0.1016 mm (4.0 mil) - inner layer, 1oz (35um) copper, before compensation; inner 0.5oz: 4mil, 2oz: 6mil, 3oz: 8mil. Outer layer 1oz: 5mil-# Min spacing: 0.127 mm (5.0 mil) - inner layer, 1oz (35um) copper, before compensation; inner 0.5oz: 5mil (4/4mil class), 2oz: 8mil, 3oz: 11mil. Outer layer 1oz: 6mil+# Min trace width: 0.127 mm (5.0 mil) - 4L 1oz: outer conventional 5mil (0.127) applied to every layer; inner 4mil (0.1016) is looser, not used. Sources: capabilities table (std sha 694ce30266e8); mobile page suggests 6mil. +# Min spacing: 0.1524 mm (6.0 mil) - 4L 1oz: outer conventional 6mil (0.1524) applied to every layer; inner 5mil (0.127) looser, not used. Sources: capabilities table (std sha 694ce30266e8); mobile page suggests 6mil. # Min drill: 0.2 mm (7.87 mil)-# Min via: 0.2 mm hole / 0.454 mm pad-# Annular ring: via 0.127 mm+# Min via: 0.2 mm hole / 0.5 mm pad+# Annular ring: via 0.15 mm, pad 0.254 mm # Copper to board edge: 0.25 mm-# Hole to hole: 0.2794 mm+# Hole to hole: 0.45 mm # Board thickness options: 0.6 mm, 0.8 mm, 1.0 mm, 1.2 mm, 1.6 mm, 2.0 mm, 2.4 mm # Copper weight options: 0.5 oz, 1 oz, 1.5 oz, 2 oz, 3 oz, 4 oz # Surface finishes: HASL with lead, HASL lead free, Immersion gold (ENIG), OSP, Immersion Tin⋯ 3 unchanged lines ⋯ #  # Sources: # - https://www.pcbway.com/capabilities.html (read 2026-09-03)+# - https://www.pcbway.com/capabilities.html (read 2026-11-02)+# - https://www.pcbway.com/helpcenter/Engineering_Questions/limited_holes_spacing.html (read 2026-11-02)+# - https://www.pcbway.com/pcb_prototype/PCB_Manufacturing_tolerances.html (read 2026-11-02)+# - https://m.pcbway.com/capabilities.html (read 2026-11-02)  (rule "min-track-width"-  (constraint track_width (min 0.1016mm))-  (layer "*.Cu")-  (condition "A.type == 'track'"))--(rule "min-clearance"-  (constraint clearance (min 0.127mm))-  (condition "A.type == 'track' && B.type == 'track'"))+  (severity error)+  (constraint track_width (min 0.127mm)))  (rule "min-hole"-  (constraint hole (min 0.2mm))-  (condition "A.type == 'pad' || A.type == 'via'"))+  (severity error)+  (constraint hole_size (min 0.2mm))) +(rule "min-via-hole"+  (severity error)+  (constraint hole_size (min 0.2mm))+  (condition "A.Type == 'Via'"))+ (rule "min-via-diameter"-  (constraint via_diameter (min 0.454mm))-  (condition "A.type == 'via'"))+  (severity error)+  (constraint via_diameter (min 0.5mm))+  (condition "A.Type == 'Via'")) +(rule "min-via-annular-width"+  (severity error)+  (constraint annular_width (min 0.15mm))+  (condition "A.Type == 'Via'"))++(rule "min-pad-annular-width"+  (severity error)+  (constraint annular_width (min 0.254mm))+  (condition "A.Type == 'Pad' && A.Pad_Type == 'Through-hole'"))+ (rule "min-edge-clearance"-  (constraint edge_clearance (min 0.25mm))-  (condition "A.type == 'track'"))+  (severity error)+  (constraint edge_clearance (min 0.25mm))) +(rule "min-hole-to-hole"+  (severity error)+  (constraint hole_to_hole (min 0.45mm)))++(rule "min-hole-to-inner-copper"+  (severity error)+  (constraint hole_clearance (min 0.1778mm)))++(rule "min-silk-height"+  (severity error)+  (constraint text_height (min 0.8mm))+  (layer "*.SilkS")+  (condition "A.Type == 'Text' || A.Type == 'Text Box'"))+ (rule "min-silk-thickness"+  (severity error)   (constraint text_thickness (min 0.15mm))   (layer "*.SilkS")-  (condition "A.type == 'text' || A.type == 'graphic'"))+  (condition "A.Type == 'Text' || A.Type == 'Text Box'"))+--- a/kicad/pcbway-4L.board-setup.json+++ b/kicad/pcbway-4L.board-setup.json@@ -1,25 +1,51 @@⋯ 2 unchanged lines ⋯   "process": "standard",   "layerCount": 4,   "boardSetupConstraints": {-    "minTrackWidthMm": 0.1016,-    "minClearanceMm": 0.127,+    "minTrackWidthMm": 0.127,+    "minClearanceMm": 0.1524,+    "minPadClearanceMm": null,     "minViaDrillMm": 0.2,-    "minViaDiameterMm": 0.454,+    "minViaDiameterMm": 0.5,     "minHoleMm": 0.2,     "copperEdgeClearanceMm": 0.25,-    "holeToHoleMm": 0.2794,-    "annularRingViaMm": 0.127,-    "annularRingPadMm": null,+    "holeToHoleMm": 0.45,+    "annularRingViaMm": 0.15,+    "annularRingPadMm": 0.254,     "silkMinLineWidthMm": 0.15,     "maskExpansionMm": 0.0508   },-  "notes": "Apply as Board Setup, Constraints. The matching .kicad_dru file carries the same limits as custom rules; the two are redundant on purpose so both the GUI constraints and kicad-cli DRC enforce them.",+  "notes": "Apply as Board Setup, Constraints. Apply the matching .kicad_dru alongside the board and project with the same stem. This JSON is an adapter contract, not a native .kicad_pro file. Separate pad/via rules and pad-pair clearance need custom rules. Confirm loaded source hashes and native negative-control findings before claiming enforcement.",   "sources": [     {       "url": "https://www.pcbway.com/capabilities.html",       "dateRead": "2026-09-03"+    },+    {+      "url": "https://www.pcbway.com/capabilities.html",+      "dateRead": "2026-11-02",+      "archiveSha256": "694ce30266e8c0bde8173d590c3ac71e6d928eb730fad3555e6a1dd298e0a980",+      "note": "observational archive; page footer '\u00a9 2024 PCBWay' only; no vendor revision date"+    },+    {+      "url": "https://www.pcbway.com/helpcenter/Engineering_Questions/limited_holes_spacing.html",+      "dateRead": "2026-11-02",+      "archiveSha256": "9cebdeed859c3c61d4e104185aa5e6a54fb1a5c6ac5864591fbd5993479145e1",+      "note": "official PCBWay Engineering Questions page; states last updated 2025-06-25"+    },+    {+      "url": "https://www.pcbway.com/pcb_prototype/PCB_Manufacturing_tolerances.html",+      "dateRead": "2026-11-02",+      "archiveSha256": "51142afe99832cc0ee41af19bf7d4c552a2658da466f04850523fab09bb66948",+      "note": "official PCBWay tolerances page; PTH ring 0.20 and via ring 0.15 (PTH ring weaker than capabilities table, not used)"+    },+    {+      "url": "https://m.pcbway.com/capabilities.html",+      "dateRead": "2026-11-02",+      "archiveSha256": "d7be6170136dad3df83304c5e34dab85775f7df80e632d8de8eae85969da3357",+      "note": "mobile capabilities page (summary only): drill 0.2 min, trace/spacing 4mil min with 6mil suggested, annular 0.15"     }   ] }+

Comments

Josh Aberdeen · 9d ago

PCB Design Studio commissioning request for maintainer review of PR #7.

We have independently reviewed the Stage 2 blocker and reproduced the most consequential safety finding: an unconditional KiCad custom clearance rule can OVERRIDE a stricter netclass. PR #7 removes the unsafe rule and places PCBWay's 0.1524 mm standard-4L-1oz floor in the companion Board Setup JSON. Hydrogen's native KiCad 10 negative/control coupons demonstrate 0.20 mm netclass is retained, the 0.1524 mm floor is still enforced if a netclass is looser, and a 0.25 mm gap passes. We specifically rejected the earlier PR #6 behaviour rather than relaxing a standard.

Additional evidence: 55/55 generated rules parse; eleven new generator/source tests pass; PR #7 includes the generated PCBWay 4L KiCad DRU and board-setup JSON, not just source. Exact candidate rules SHA begins ed114795. Circuit Forge's independent v5 gate now checks both source-hashed files and the netclass/floor outcomes, and 63/63 local validation regression tests pass. Our source/acceptance contract: https://github.com/Unclefungussz/PCB-Design-Studio-/pull/162 .

What the maintainer needs to decide and verify before any default-branch merge:

  • Cross-check PCBWay's official 2025 hole-spacing (0.45 mm PTH) and standard 1 oz 10 mil component annular ring against the earlier capabilities/tolerances pages. We chose the stricter stated numbers, not weaker ones.
  • Confirm exact source generator, canonical record, generated KiCad DRU, Board Setup JSON and rules index resolve together when published. A source-only merge cannot fix ADOM 0.2.30 Rules Stage: native rules use downloads only default-branch generated bytes.
  • The generator has global consequences for other vendor packs. Validate/regenerate relevant outputs together, or explicitly scope the release without regressing other manufacturers. Pre-existing Fusion/Altium/README drift should be independently recorded, not silently overwritten.
  • Owner-approved deferral is LIMITED to checks needing an actual PCB (pad-zone, netclass-zone and final inner-copper clearance at Stage16; board thickness at Stage17; mask and final silk at Stage22). PCBDS Postgres migration M067 records these as immutable mandatory future gates and rejects advancing their due stages without evidence. They are not claimed passed. Any current-stage fabrication or tool-source defect still blocks Stage 2.

We cannot merge via our contributor role, and we have not changed the published rules, live native run.json or any PCBWay order. This is a request for normal ADOM maintainer engineering review and a source-matched authoritative publication route. After publication we will verify default-branch hash readback, native rules use and our independent PostgreSQL gate. No waiver or shortcut is requested.

Josh Aberdeen · 9d ago

Circuit Forge PCB Design Studio commissioning review. PR #7 correctly replaces unsafe PR #6, but requires ADOM maintainer review and published-package readback before UMCU21 may accept Stage 2.

Independent PCBDS manager checks:

  • 63/63 regression tests passed with v5 source-bound machine gate: https://github.com/Unclefungussz/PCB-Design-Studio-/pull/162 .
  • Native KiCad 10 coupons prove an unconditional custom clearance rule overrided a stricter netclass. PR #7 removes that unsafe rule. A Board Setup minimum clearance of 0.1524 mm preserves both the manufacturer floor and stricter netclasses, independently checked with strict, loose and compliant coupons.
  • DRU and companion board-setup JSON must be a single published, source-hashed package. Stage 2 rejects a mismatch between actual ADOM default branch, native rules use bytes and tested KiCad results. Unpublished drafts are not considered installed.
  • Tests requiring real board geometry (pad-zone/netclass-zone, thickness, mask and final silk) have exact mandatory future PostgreSQL acceptance gates at Stages 16/17/22 and are NOT claimed passed. Those should not endlessly block source rules publication when its current-stage generator and Board Setup are correct.

ACTION FOR ADOM MAINTAINER: Reconcile PR #2 generator changes and PR #7, especially generator impacts on other vendors. Regenerate and test affected outputs together, or strictly scope the generator and track the general repair separately. Do not publish source/output drift, relax any vendor limit or restore a global KiCad clearance rule. Confirm source-backed PCBWay 4L 1oz limits, and review the complete five-file package for merge. Once qualified, read back source and generated DRU plus board setup on default branch, with exact SHA-256.

Hydrogen has no permission to merge adom/pcb-design-rules, and PCBDS will not bypass it through a private executable or direct native run modifications. Maintainer publication is the remaining external dependency. After that, PCBDS will use native ADOM AI Flow rules use, parse actual loaded bytes, verify KiCad positive/negative coupons, close the PostgreSQL blocker and accept Stage 2 only on evidence. Stage 3 stays locked until then.

Separate native presence-only rules-check safety bug: https://wiki.adom.inc/adom/adom-aiflow/issues/120 .

Josh Aberdeen · 9d ago

Pointer and correction from the Circuit Forge PCBDS team. A smaller replacement is open as draft #8 (global 382): it changes only the PCBWay standard 4-layer export, behind an opt-in record flag (kicadStrict). Measured against main's generator, 108 of 110 generated KiCad files stay byte-identical, so the 54 other vendor packs are no longer changed by this path. Correction: our earlier statement that KiCad 10 rejects the hole keyword is wrong. Desktop KiCad 10.0.7 DRC accepts it and enforces it as min-hole. The rejection came from the bridge pre-check, not KiCad's DRC loader. The real defects in the published pack are the unconditional clearance override, the weak outer track minimum, and the missing annular, hole-to-hole and hole-to-inner-copper rules. Please review #8 and close #7 if #8 is accepted.

John Lauer · 7d ago

Closing as superseded by #9, which is merged (per your note on #9).

Log in to comment.