← All Pull Requests

PCBWay 4L 1oz minimal strict KiCad export: opt-in record flag, 2 outputs change, 108 unchanged (DRAFT; replaces #7; merge blocked) #8

Closed Draft opened by Josh Aberdeen 2026-11-02

DRAFT, NOT FOR MERGE. Replaces draft PR #7 (global 380) for review. Please close #7 when this is reviewed.

What changed, and why it is smaller than #7

  • #7 changed the KiCad export of ALL 55 vendor packs. This draft changes ONE record: PCBWay standard 4-layer 1 oz. Every other record uses the legacy export path, byte for byte.
  • Measured blast radius (generator output, main vs this draft): 110 generated KiCad files, 108 byte-identical, 2 changed (PCBWay 4L .kicad_dru and .board-setup.json). Hashes in SHA256-pr-min.txt.
  • Opt-in flag kicadStrict: true on the PCBWay 4L record only (tested: exactly one record carries it).
  • The 4L pack has NO unconditional clearance rule. A global custom clearance overrides the Default netclass (0.2 mm): a 0.18 mm gap passes with the rule and fails without it (desktop KiCad 10 DRC). The 0.1524 mm floor is carried as the board minimum clearance (board setup minClearanceMm), which holds over looser netclasses (0.1 mm netclass, 0.14 mm gap fails against the 0.1524 mm board minimum).

Correction to earlier claims (please read)

  • Earlier PCBDS text and issue #6 said KiCad 10 REJECTS the hole constraint keyword. That was wrong. The rejection came from the bridge pre-check kicad_custom_rules validate, not from KiCad's DRC loader. Desktop KiCad 10.0.7 DRC accepts the published pack's (constraint hole ...) and enforces it as min-hole (drill violations reported under rule 'min-hole'). Evidence: drc-cpn_hole-published.json (SHA-256 in SHA256-pr-min.txt).
  • Consequence: hole_size in the 4L output is the canonical KiCad constraint name, not a compatibility fix. The legacy export for other vendors keeps hole to avoid any output change.
  • The real defects in the published 4L pack, measured in DRC: (1) an unconditional custom clearance 0.127 mm that overrides netclass clearance, (2) outer 1 oz track minimum too weak (0.1016), (3) no annular-ring rules, (4) no hole-to-hole or hole-to-inner-copper rules, (5) via diameter 0.454 mm.

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

Verified

  • 4L outputs: kicad/pcbway-4L.kicad_dru sha256 ed114795f294b9e6355627166f0d557132d848f2325389c8f54b0b7cf7ee0ef4 (11 rules, every rule an explicit error); kicad/pcbway-4L.board-setup.json sha256 01edbd0b23d3bbe4807e6428dc24ca4dfe320bb4986a963db44c914e4d33a9a5 (identical to the audited file).
  • Tests: test_pcbway_4l.py 11/11; test_legacy_unchanged.py 2/2 (non-strict records use the legacy export; exactly one strict record).
  • Native KiCad 10 DRC (desktop kicad-cli 10.0.7 via bridge, zone refill on): violating coupon fires the rule-named categories; compliant coupon has zero rule-named errors; netclass coupons (strict 0.2, loose 0.1, compliant) behave as stated above. Evidence files are in the reviewer folder and listed in SHA256-pr-min.txt.

Not in this draft (explicit)

  • README table, index.json, Fusion and Altium outputs are NOT regenerated here. Main's committed versions already differ from main's generator for some vendors (pre-existing drift). Maintainers must resolve that separately.
  • PR #2 (global 98) InstaPCB record and test_constraints.py are not included.

Merge blockers

  1. Maintainer decision on the kicadStrict opt-in name and placement in the record schema.
  2. Board-stage checks are not verified by this PR: pad-to-zone clearance, netclass-zone interaction, mask bridge and expansion, board thickness. They are held for the first board stage.
  3. The board setup is an adapter contract. Native import of Board Setup constraints into a .kicad_pro is a board-stage capability (kicad_board_rules), not a pre-board ADOM import.
  4. Publication: merge must be followed by a regenerated published pack and a readback of the default-branch bytes before any project binds it.

🤖 Generated with Claude Code

Diff Skip to comments (2)

--- a/tools/generate.py+++ b/tools/generate.py@@ -1,573 +1,633 @@⋯ 364 unchanged lines ⋯  # ---------------------------------------------------------------- KiCad -def gen_kicad_dru(vendor, rec):+def gen_kicad_dru_legacy(vendor, rec):     tw = rec["minTraceWidth"]["mm"]     sp = rec["minSpacing"]["mm"]     dr = rec["minDrill"]["mm"]⋯ 47 unchanged lines ⋯     return "\n".join(out)  -def gen_kicad_board_setup(vendor, rec):+def gen_kicad_board_setup_legacy(vendor, rec):     via_d, via_p = effective_via(rec)     setup = {         "vendor": vendor,⋯ 22 unchanged lines ⋯  # ---------------------------------------------------------------- Altium .RUL ++def _strict_rule_block(out, name, kind, minimum, condition=None, layer=None):+    if minimum is None:+        return+    rows = [f'(rule "{name}"', '  (severity 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 + [""])++def gen_kicad_dru_strict(vendor, rec):+    """Opt-in (record flag kicadStrict). NO unconditional clearance rule: a global custom clearance overrides netclass clearance."""+    via = rec["minVia"]+    out = ["(version 1)", "", "# " + description_text(vendor, rec).replace("\n", "\n# "), ""]+    R = lambda *a, **k: _strict_rule_block(out, *a, **k)+    R("min-track-width", "track_width", rec["minTraceWidth"]["mm"])+    R("min-hole", "hole_size", rec["minDrill"]["mm"])+    R("min-via-hole", "hole_size", via["drillMm"], "A.Type == 'Via'")+    R("min-via-diameter", "via_diameter", via["padMm"], "A.Type == 'Via'")+    R("min-via-annular-width", "annular_width", rec["annularRing"]["viaMm"], "A.Type == 'Via'")+    R("min-pad-annular-width", "annular_width", rec["annularRing"]["padMm"], "A.Type == 'Pad' && A.Pad_Type == 'Through-hole'")+    R("min-edge-clearance", "edge_clearance", g(rec, "copperToBoardEdge", "mm"))+    R("min-hole-to-hole", "hole_to_hole", g(rec, "holeToHole", "mm"))+    R("min-hole-to-inner-copper", "hole_clearance", g(rec, "minHoleToInnerCopper", "mm"))+    R("min-silk-height", "text_height", g(rec, "silkMinHeightMm", "mm"), "A.Type == 'Text' || A.Type == 'Text Box'", "*.SilkS")+    R("min-silk-thickness", "text_thickness", g(rec, "silkLineWidth", "mm"), "A.Type == 'Text' || A.Type == 'Text Box'", "*.SilkS")+    return "\n".join(out)++def gen_kicad_dru(vendor, rec):+    if rec.get("kicadStrict") is True:+        return gen_kicad_dru_strict(vendor, rec)+    return gen_kicad_dru_legacy(vendor, rec)++def gen_kicad_board_setup(vendor, rec):+    if rec.get("kicadStrict") is True:+        via_d, via_p = effective_via(rec)+        setup = {+            "vendor": vendor, "process": rec["process"], "layerCount": rec["layerCount"],+            "boardSetupConstraints": {+                "minTrackWidthMm": rec["minTraceWidth"]["mm"],+                "minClearanceMm": rec["minSpacing"]["mm"],+                "minPadClearanceMm": g(rec, "minPadSpacing"),+                "minViaDrillMm": via_d, "minViaDiameterMm": via_p,+                "minHoleMm": rec["minDrill"]["mm"],+                "copperEdgeClearanceMm": g(rec, "copperToBoardEdge"),+                "holeToHoleMm": g(rec, "holeToHole"),+                "annularRingViaMm": g(rec, "annularRing", "viaMm"),+                "annularRingPadMm": g(rec, "annularRing", "padMm"),+                "silkMinLineWidthMm": g(rec, "silkLineWidth"),+                "maskExpansionMm": g(rec, "maskExpansion"),+            },+            "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", []),+        }+        return json.dumps(setup, indent=2) + "\n"+    return gen_kicad_board_setup_legacy(vendor, rec)+ def gen_rul(vendor, rec):     tw = rec["minTraceWidth"]     sp = rec["minSpacing"]⋯ 117 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/tools/test_legacy_unchanged.py+++ b/tools/test_legacy_unchanged.py@@ -0,0 +1,26 @@+"""Blast-radius guard: every record WITHOUT kicadStrict must produce byte-identical KiCad export to main's legacy functions."""+import unittest, importlib.util, json+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)+RECS = []+for f in sorted((BASE / 'rules' / 'records').glob('*.json')):+    d = json.loads(f.read_text(encoding='utf-8'))+    for r in d['records']:+        RECS.append((d['vendor'], r))+class LegacyUnchanged(unittest.TestCase):+    def test_non_strict_records_use_legacy_kicad_export(self):+        checked = 0+        for vendor, rec in RECS:+            if rec.get('kicadStrict') is True: continue+            if not all(rec.get(k) for k in ('minTraceWidth', 'minSpacing', 'minDrill')): continue+            self.assertEqual(g.gen_kicad_dru(vendor, rec), g.gen_kicad_dru_legacy(vendor, rec), vendor)+            self.assertEqual(g.gen_kicad_board_setup(vendor, rec), g.gen_kicad_board_setup_legacy(vendor, rec), vendor)+            checked += 1+        self.assertGreater(checked, 50)+    def test_only_pcbway_standard_4l_is_strict(self):+        strict = [(v, r['process'], r['layerCount']) for v, r in RECS if r.get('kicadStrict') is True]+        self.assertEqual(strict, [('PCBWay', 'standard', 4)])+if __name__ == '__main__': unittest.main(verbosity=2)+--- a/rules/records/pcbway.json+++ b/rules/records/pcbway.json@@ -1,616 +1,654 @@⋯ 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...)"+      },+      "kicadStrict": true     },     {       "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

Superseded by the complete draft: see the new PR in this repo (global 383/per-page #9). #8 omitted four stale PCBWay 4-layer outputs (Fusion .dru and .edru, Altium .RUL, index and README row). #9 regenerates them and adds a test that every committed 4-layer output equals the generator output. Please close #8 once #9 is reviewed.

John Lauer · 7d ago

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

Log in to comment.