@tachikomagundam/abathur 0.2.7 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env bash
2
+ # run-scenario.sh (pcb-agent) — spawn the PCB TEAM headless on the scenario brief.
3
+ # The seat writes ZERO board artifacts here: this script only (1) seeds an isolated
4
+ # arena, (2) launches the internal-control orchestrator profile (isolated HOME/DB per
5
+ # L-isolation) with the brief, (3) records the transcript. All engineering is done by
6
+ # the team's own agents. Transcript = the evidence the grader reads.
7
+ #
8
+ # Contract mirrors historian: argv = <unitId> <scenario-file> [repoRoot]; last stdout
9
+ # line = {"tokensEst":N,"turns":M} for parseRunMeta; ABATHUR_TRANSCRIPT honored.
10
+ set -euo pipefail
11
+ unit_id="${1:?usage: run-scenario.sh <unitId> <scenario-file> [repoRoot]}"
12
+ scenario_file="${2:?scenario file}"
13
+ repo_root="${3:-$(dirname "$(dirname "$scenario_file")")}"
14
+ HERE="$(cd "$(dirname "$0")" && pwd)"
15
+ CONTROL=/home/lab/workspace/pcb-control
16
+ ARENA_ROOT="${PCB_BENCH_ARENA:-/tmp/opencode/pcb-bench}"
17
+ ARENA="$ARENA_ROOT/$unit_id"
18
+ TRANSCRIPT="${ABATHUR_TRANSCRIPT:-$ARENA_ROOT/$unit_id.transcript.jsonl}"
19
+ mkdir -p "$ARENA_ROOT"
20
+
21
+ bash "$HERE/seed-sandbox.sh" "$ARENA" "$HERE/seed"
22
+
23
+ # Brief = the scenario file (whole thing; it is the task card, no solutions inside).
24
+ BRIEF="$(cat "$scenario_file")"
25
+ # run cards (S4 seam, historian precedent): candidate mutations ride to the team verbatim.
26
+ rc=""
27
+ if [ -d "$repo_root/abathur-notes" ]; then
28
+ for n in "$repo_root"/abathur-notes/*.md; do [ -e "$n" ] || continue
29
+ rc="$rc
30
+ ## Run card
31
+ $(cat "$n")"; done
32
+ fi
33
+
34
+ cd "$ARENA"
35
+ # per-unit isolated HOME/DB (never the human main DB; L-isolation law), seeded
36
+ # from the PROVISIONED internal home's provider config (local-qwen baseURL etc).
37
+ UHOME="$ARENA_ROOT/$unit_id.home"; rm -rf "$UHOME"; mkdir -p "$UHOME/.config" "$UHOME/.local/share/opencode"
38
+ cp -r /home/lab/workspace/pcb-control/internal-home/.config/opencode "$UHOME/.config/" 2>/dev/null || true
39
+ rm -rf "$UHOME/.config/opencode/node_modules" "$UHOME/.local/share/opencode/opencode.db" 2>/dev/null || true
40
+ DB="$UHOME/.local/share/opencode/opencode.db"
41
+
42
+ # THE TEAM IS THE SUBJECT: mount the PCB role tree into the arena cwd so
43
+ # `--agent pcb-orchestrator` resolves. Role provenance is the genome seam:
44
+ # if the candidate tree carries agents/*.md (a mutation to the team itself),
45
+ # those files WIN over the canonical internal-control copies.
46
+ mkdir -p "$ARENA/.opencode/agents"
47
+ cp /home/lab/workspace/pcb-control/internal-control/.opencode/agents/*.md "$ARENA/.opencode/agents/"
48
+ if [ -d "$repo_root/agents" ] && ls "$repo_root"/agents/*.md >/dev/null 2>&1; then
49
+ cp -f "$repo_root"/agents/*.md "$ARENA/.opencode/agents/"
50
+ echo "role-provenance: candidate-tree agents override canonical" >&2
51
+ fi
52
+
53
+ RUNENV=(env -i PATH="$PATH" LANG=C.UTF-8 TERM=xterm-256color HOME="$UHOME"
54
+ XDG_DATA_HOME="$UHOME/.local/share" XDG_CACHE_HOME="$UHOME/.cache" XDG_STATE_HOME="$UHOME/.local/state"
55
+ OPENCODE_DB="$DB" KICAD_SHARE_DIR=/home/lab/.local/AppDir/share/kicad)
56
+
57
+ # FAIL-CLOSED SELF-PROOF GATE: the named agent must actually resolve BEFORE we
58
+ # spend a rep. "agent not found. Falling back to default" voids the rep — that
59
+ # is precisely the channel-dead noise theater the lane forbids.
60
+ "${RUNENV[@]}" opencode agent list >"$ARENA/.agent-list.txt" 2>&1
61
+ grep -q "pcb-orchestrator" "$ARENA/.agent-list.txt" || {
62
+ echo "RUNNER_GATE_FAIL: pcb-orchestrator not in resolved agent inventory" >&2
63
+ tail -5 "$ARENA/.agent-list.txt" >&2
64
+ exit 1
65
+ }
66
+
67
+ timeout "${PCB_RUN_TIMEOUT:-2400}" "${RUNENV[@]}" \
68
+ opencode run --agent pcb-orchestrator --format json \
69
+ --dir "$ARENA" -- "$BRIEF$rc" > "$TRANSCRIPT" 2>"$ARENA/.run-stderr" || true
70
+
71
+ if [ ! -s "$TRANSCRIPT" ]; then
72
+ echo "RUNNER_INFRA_FAIL: empty transcript (see $ARENA/.run-stderr)" >&2
73
+ exit 1
74
+ fi
75
+ # mechanical role attribution proof: the bench session in the isolated DB MUST
76
+ # be owned by pcb-orchestrator (transcript lines carry no agent field; the DB
77
+ # session table does). No proof = void rep, never a scored fallback run.
78
+ ROLE_OK="$(sqlite3 "$DB" "SELECT COUNT(*) FROM session WHERE agent='pcb-orchestrator';" 2>/dev/null || echo 0)"
79
+ [ "${ROLE_OK:-0}" -ge 1 ] || { echo "RUNNER_INFRA_FAIL: no pcb-orchestrator session in $DB (fallback-agent run would be noise theater)" >&2; exit 1; }
80
+
81
+ turns=$(grep -c '"type":"text"' "$TRANSCRIPT" 2>/dev/null || true); turns="${turns:-0}"
82
+ tokens=$(python3 - "$TRANSCRIPT" <<'EOF' 2>/dev/null | tail -1 || true
83
+ import json,sys
84
+ n=0
85
+ for l in open(sys.argv[1]):
86
+ try: d=json.loads(l)
87
+ except Exception: continue
88
+ p=d.get("part",{})
89
+ if p.get("type")=="text": n+=len(p.get("text",""))
90
+ print(n//4)
91
+ EOF
92
+ ); tokens="${tokens:-0}"
93
+ echo "arena=$ARENA" > "$ARENA_ROOT/$unit_id.arena"
94
+ python3 -c "import json,re;t=int(re.sub(r'[^0-9]','',str('$tokens')) or 0);u=int(re.sub(r'[^0-9]','',str('$turns')) or 0);print(json.dumps({'tokensEst':t,'turns':u}))"
@@ -0,0 +1,51 @@
1
+ # MODULE BRIEF -- esp32s3-r12 (fixture-tier mini-board)
2
+
3
+ Mission dir: fixtures/board-lab/esp32s3-r12/ (root: /home/lab/workspace/pcb-control/fixtures)
4
+ Binding plan: 2ee2cfb309bea8552dd294db3eb087d74950f0e82e0e11524a142c9f8f217f4a / dispatch pcb-dispatch-r12a-plan1
5
+
6
+ ## 0. Purpose
7
+ Six-module ESP32-S3 breakout: USB-C 5V in -> ME6211 3.3V LDO -> module power/decoupling, one status LED, one 1x4 debug header with UART. Fixtures only; never the real board.
8
+
9
+ ## 1. Netlist (exactly 6 nets)
10
+ - +3V3 : U1.3V3, U2.VOUT, C1.1, C2.1, C3.1, D1.anode, H1.1 (L10: single merged name, no variants)
11
+ - +5V_IN: J1.VBUS, U2.VIN
12
+ - GND : J1.GND, U2.GND, C1.2, C2.2, C3.2, R1.2? NO -> R1.2 is +3V3 per LED string below.
13
+ Correction -- GND: J1.GND, U2.GND, C1.2, C2.2, C3.2, D1... see M5 for string orientation.
14
+ - GPIO2 : U1.GPIO2, R1.2
15
+ - GPIO4 : U1.GPIO4, H1.2 (label "TX" = board->host)
16
+ - GPIO5 : U1.GPIO5, H1.3 (label "RX" = host->board)
17
+ Final GND set: J1.GND, U2.GND, C1.2, C2.2, C3.2, H1.4.
18
+ All other U1 pins NC; flash/PSRAM GPIO26-32 MUST remain unconnected. U1.EN tied directly to +3V3 (hard-enable; accepted fixture deviation, flagged for DFM review).
19
+
20
+ ## 2. Modules (6 max -- no extra refs permitted)
21
+ - M1 U1 ESP32-S3-WROOM-1 (module). Power pads 3V3/GND per library footprint; IO used: EN, GPIO2, GPIO4 (UART TX), GPIO5 (UART RX).
22
+ - M2 J1 USB-C receptacle, power-only. Connect VBUS(A4/A9/B4/B5 bussed)->+5V_IN and GND only. CC1/CC2 left NC -- fixture is bench/charger-fed; no PD negotiation. Deviation noted for reviewer.
23
+ - M3 U2 ME6211C33 LDO (SOT-23-3 class). Function pins VIN->+5V_IN, GND->GND, VOUT->+3V3. Pad-number resolution against the project library symbol is the schematic agent's job, verified by gate.
24
+ - M4 Decoupling: C1,C2 100nF 0402 across +3V3/GND placed at U1 3V3 pad(s); C3 10uF 0603 across +3V3/GND at U2 VOUT. ME6211 input cap omitted (fixture simplification, flagged).
25
+ - M5 Status: +3V3 -> D1 anode, D1 cathode -> R1 (1k) -> GPIO2 (sink-driven, active-low lit). GPIO2 is not an S3 strapping pin (straps are 0/3/45/46): safe.
26
+ - M6 H1 1x4 header, 2.54mm pitch: pin1 +3V3, pin2 GPIO4/TX, pin3 GPIO5/RX, pin4 GND.
27
+
28
+ ## 3. Explicit rules -- binding on brief AND every downstream artifact
29
+ - L10 ONE merged power net named +3V3 for every 3.3V pad. Forbidden: +3V3A, 3V3_D, VDD_3V3, duplicate 3.3V names. Net-name scan of sch+pcb must find exactly "+3V3".
30
+ - L11 Every footprint pad pitch AND courtyard gap >= 0.25mm (class clearance 0.2mm + 0.05 margin). 0402 pads 1.0x0.5mm, rot-aware: clearance check applies after per-ref rotation. U1 side pads on >=1.5mm vertical grid rows.
31
+ - L16 Netlist uses KiCad schematic global-label grammar: every connection is a wire terminating in a label whose text equals the net name, placed at each pin. No hierarchical/local-only labels, no net-naming-by-wire.
32
+ - L1 pos.csv refs == bom.csv refs exactly (same set, same strings): {U1,U2,J1,H1,C1,C2,C3,D1,R1} -- 10 refs, no suffix drift, no extra/missing.
33
+ - Placement grid: 0.5mm snap for all component origins.
34
+
35
+ ## 4. Artifact contract (mission dir)
36
+ - esp32s3-r12.kicad_sch -- 10 symbols, nets per section 1, grammar per L16.
37
+ - esp32s3-r12.kicad_pcb -- footprints from project lib; courtyard clearances per L11; board outline suggestion <= 55x35mm (target, non-binding).
38
+ - bom.csv / pos.csv -- per L1; pos.csv includes rotation column (feeds L11 rot-aware check).
39
+ - reports/ -- gate evidence JSON from tools/gate_runner.py.
40
+
41
+ ## 5. Acceptance gates (deterministic, tools/gate_runner.py evidence only)
42
+ - g-net: parsed sch netlist == section 1 set exactly; net count == 6.
43
+ - g-l10: regex scan for 3.3V-name variants in sch+pcb yields zero hits outside "+3V3".
44
+ - g-l11: pad-pitch/courtyard matrix min gap >= 0.25mm; 0402 pad geom 1.0x0.5 present; U1 side-pad row pitch >= 1.5mm.
45
+ - g-l16: every net pin has wire+label; zero label-less pin wires.
46
+ - g-pos: set(bom.refs) == set(pos.refs), len == 10.
47
+ - g-grid: all placement origins on 0.5mm grid.
48
+ Fail on any gate -> revert to last checkpoint; 3 design-fails on same gate -> ESCALATE with options menu.
49
+
50
+ ## 6. Out of scope
51
+ Antenna keepout tuning, ESD parts, USB data lines, battery/charge, strapping-button circuits, any board outside fixtures/.
@@ -0,0 +1,129 @@
1
+ {
2
+ "schema_version": "1.0",
3
+ "board": {
4
+ "name": "toy-m2",
5
+ "_provenance": "TOY-FIXTURE netlist from esp32s3-r12-BRIEF.txt + architect corrections (7 nets/9 refs). U1 pin-pad function mapping = FIXTURE CONVENTION (toy GPIOs on pads 4/5/9), NOT silicon pinout \u2014 HUMAN VERIFICATION REQUIRED before any real-order use. | C1/C2 0603 deviation per pad-corridor DRC class",
6
+ "project": "board-lab toy M2 full-cycle"
7
+ },
8
+ "components": [
9
+ {
10
+ "ref": "U1",
11
+ "mpn": "ESP32-S3-WROOM-1-N8R8",
12
+ "footprint": "RF_Module:ESP32-S3-WROOM-1",
13
+ "pins": {
14
+ "1": "GND",
15
+ "2": "+3V3",
16
+ "3": "+3V3",
17
+ "4": "GPIO4",
18
+ "5": "GPIO5",
19
+ "9": "GPIO2",
20
+ "21": "GND",
21
+ "6": "NC(pin unused)",
22
+ "7": "NC(pin unused)",
23
+ "8": "NC(pin unused)",
24
+ "10": "NC(pin unused)",
25
+ "11": "NC(pin unused)",
26
+ "12": "NC(pin unused)",
27
+ "13": "NC(pin unused)",
28
+ "14": "NC(pin unused)",
29
+ "15": "NC(pin unused)",
30
+ "16": "NC(pin unused)",
31
+ "17": "NC(pin unused)",
32
+ "18": "NC(pin unused)",
33
+ "19": "NC(pin unused)",
34
+ "20": "NC(pin unused)"
35
+ }
36
+ },
37
+ {
38
+ "ref": "U2",
39
+ "mpn": "ME6211C33",
40
+ "footprint": "Package_TO_SOT_SMD:SOT-23",
41
+ "pins": {
42
+ "1": "+5V_IN",
43
+ "2": "GND",
44
+ "3": "+3V3"
45
+ }
46
+ },
47
+ {
48
+ "ref": "J1",
49
+ "mpn": "USB4110",
50
+ "footprint": "Connector_USB:USB_C_Receptacle_GCT_USB4110",
51
+ "pins": {
52
+ "1": "GND",
53
+ "2": "NC(pin unused)",
54
+ "3": "NC(pin unused)",
55
+ "4": "+5V_IN",
56
+ "5": "NC(pin unused)",
57
+ "6": "+5V_IN",
58
+ "7": "NC(pin unused)",
59
+ "8": "GND",
60
+ "9": "GND",
61
+ "10": "NC(pin unused)",
62
+ "11": "NC(pin unused)",
63
+ "12": "NC(pin unused)",
64
+ "13": "NC(pin unused)",
65
+ "14": "NC(pin unused)",
66
+ "15": "NC(pin unused)",
67
+ "16": "GND"
68
+ }
69
+ },
70
+ {
71
+ "ref": "H1",
72
+ "mpn": "HDR1x4",
73
+ "footprint": "Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical",
74
+ "pins": {
75
+ "1": "+3V3",
76
+ "2": "GPIO4",
77
+ "3": "GPIO5",
78
+ "4": "GND"
79
+ }
80
+ },
81
+ {
82
+ "ref": "C1",
83
+ "mpn": "100nF",
84
+ "footprint": "Capacitor_SMD:C_0603_1608Metric",
85
+ "pins": {
86
+ "1": "+3V3",
87
+ "2": "GND"
88
+ },
89
+ "_dev": "0402->0603: pad-pair corridor (0.36mm) admits no 0.2mm track w/ 0.2 clr; fixture deviation logged"
90
+ },
91
+ {
92
+ "ref": "C2",
93
+ "mpn": "100nF",
94
+ "footprint": "Capacitor_SMD:C_0603_1608Metric",
95
+ "pins": {
96
+ "1": "+3V3",
97
+ "2": "GND"
98
+ },
99
+ "_dev": "0402->0603: pad-pair corridor (0.36mm) admits no 0.2mm track w/ 0.2 clr; fixture deviation logged"
100
+ },
101
+ {
102
+ "ref": "C3",
103
+ "mpn": "10uF",
104
+ "footprint": "Capacitor_SMD:C_0603_1608Metric",
105
+ "pins": {
106
+ "1": "+3V3",
107
+ "2": "GND"
108
+ }
109
+ },
110
+ {
111
+ "ref": "D1",
112
+ "mpn": "LED",
113
+ "footprint": "LED_SMD:LED_0603_1608Metric",
114
+ "pins": {
115
+ "1": "+3V3",
116
+ "2": "LED1"
117
+ }
118
+ },
119
+ {
120
+ "ref": "R1",
121
+ "mpn": "1k",
122
+ "footprint": "Resistor_SMD:R_0603_1608Metric",
123
+ "pins": {
124
+ "1": "LED1",
125
+ "2": "GPIO2"
126
+ }
127
+ }
128
+ ]
129
+ }
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env python3
2
+ """r17 deterministic PCB author: netlist.json + spec footprints -> placed .kicad_pcb.
3
+ RM-56 doctrine: no bytes through a model. Embeds real .kicad_mod libs from the
4
+ KiCad AppDir, injects (net N "NAME") per pad (name-match, else positional match
5
+ with WARN), shelf placement by footprint bbox + clearance, Edge.Cuts rect.
6
+ DRC gate (board_gate_pcb.py) evaluates placement-class errors; unconnected_items
7
+ are expected pre-routing and reported separately."""
8
+ import json, re, os, sys
9
+ from collections import defaultdict
10
+
11
+ ALIASES = {}
12
+ _ali = os.path.join(os.path.dirname(os.path.abspath(__file__)), "board_author_aliases.json")
13
+ if os.path.exists(_ali):
14
+ ALIASES = {tuple(k.split("|")): v for k, v in json.load(open(_ali)).items()}
15
+
16
+ FP_ROOT = "/home/lab/.local/AppDir/share/kicad/footprints"
17
+ PLACE_MARGIN = 3.0 # mm between footprint bboxes (courtyard-ish)
18
+ GRID = 0.1
19
+
20
+
21
+ def snap(v):
22
+ return round(v / GRID) * GRID
23
+
24
+
25
+ def forms(s):
26
+ """top-level s-expr blocks inside a ( ... ) wrapper string."""
27
+ out = []
28
+ i = 0
29
+ n = len(s)
30
+ while i < n:
31
+ if s[i] == "(":
32
+ depth = 0
33
+ j = i
34
+ instr = False
35
+ while j < n:
36
+ ch = s[j]
37
+ if ch == '"' and (j == 0 or s[j - 1] != "\\"):
38
+ instr = not instr
39
+ elif not instr:
40
+ if ch == "(":
41
+ depth += 1
42
+ elif ch == ")":
43
+ depth -= 1
44
+ if depth == 0:
45
+ break
46
+ j += 1
47
+ out.append(s[i:j + 1])
48
+ i = j + 1
49
+ else:
50
+ i += 1
51
+ return out
52
+
53
+
54
+ def sub_block(s, token):
55
+ """return (block, start, end) of first block starting with token, else None"""
56
+ for b in forms(s):
57
+ if b.startswith(f"({token}"):
58
+ return b
59
+ return None
60
+
61
+
62
+ def bbox_of(km):
63
+ """rough extent from pads + courtyard graphic lines/rects."""
64
+ xs, ys = [], []
65
+ for m in re.finditer(r'\(pad[^()]*?\(at ([-\d.]+)(?: ([-\d.]+))?\)(?:\s*\(size ([-\d.]+) ([-\d.]+))?', km):
66
+ x, y = float(m.group(1)), float(m.group(2) or 0)
67
+ w, h = (float(m.group(3)), float(m.group(4))) if m.group(3) else (1.0, 1.0)
68
+ xs += [x - w / 2, x + w / 2]
69
+ ys += [y - h / 2, y + h / 2]
70
+ for m in re.finditer(r'\((?:xy|start|end|at) ([-\d.]+) ([-\d.]+)', km):
71
+ xs.append(float(m.group(1)))
72
+ ys.append(float(m.group(2)))
73
+ if not xs:
74
+ return -1, -1, 2, 2
75
+ pad = 0.5
76
+ return min(xs) - pad, min(ys) - pad, max(xs) + pad, max(ys) + pad
77
+
78
+
79
+ def resolve_footprint(spec_fp):
80
+ lib, name = spec_fp.split(":", 1)
81
+ p = os.path.join(FP_ROOT, f"{lib}.pretty", f"{name}.kicad_mod")
82
+ return p if os.path.exists(p) else None
83
+
84
+
85
+ def assign_nets(netlist):
86
+ nets = sorted({n for c in netlist["components"] for n in c["pins"].values()})
87
+ code = {n: i + 1 for i, n in enumerate(nets)}
88
+ return nets, code
89
+
90
+
91
+ def author(netlist_path, out_path, rot_by_ref=None):
92
+ d = json.load(open(netlist_path))
93
+ comps = d["components"]
94
+ nets, ncode = assign_nets(d)
95
+ uuidn = [0]
96
+
97
+ def U():
98
+ uuidn[0] += 1
99
+ x = f"{uuidn[0]:032x}"
100
+ return f"{x[:8]}-{x[8:12]}-{x[12:16]}-{x[16:20]}-{x[20:32]}"
101
+
102
+ # ---- layout pass: shelf by bbox heights
103
+ items = []
104
+ warn = []
105
+ for c in sorted(comps, key=lambda c: (c["ref"][0], int(re.sub(r"\D", "", c["ref"]) or 0))):
106
+ fpp = resolve_footprint(c["footprint"])
107
+ if not fpp:
108
+ warn.append(f"NO-FOOTPRINT {c['ref']} {c['footprint']}")
109
+ continue
110
+ km = open(fpp).read()
111
+ x0, y0, x1, y1 = bbox_of(km)
112
+ items.append([c, km, snap(x1 - x0) + PLACE_MARGIN, snap(y1 - y0) + PLACE_MARGIN, (x0, y0)])
113
+ COL_W = 120.0
114
+ cx, cy, rowh = 10.0, 10.0, 0.0
115
+ placed = []
116
+ for it in items:
117
+ if cx + it[2] > COL_W:
118
+ cx = 10.0
119
+ cy += rowh
120
+ rowh = 0.0
121
+ placed.append((it, snap(cx), snap(cy)))
122
+ cx += it[2]
123
+ rowh = max(rowh, it[3])
124
+ board_w, board_h = snap(COL_W + 20), snap(cy + rowh + 20)
125
+
126
+ # ---- emit
127
+ L = ['(kicad_pcb (version 20260206) (generator "board_author_pcb") (generator_version "10.0")',
128
+ ' (general (thickness 1.6) (legacy_teardrops no))',
129
+ ' (paper "A3")',
130
+ ' (layers (0 "F.Cu" signal) (31 "B.Cu" signal) (32 "B.Adhes" user "B.Adhesive") '
131
+ '(33 "F.Adhes" user "F.Adhesive") (34 "B.Paste" user) (35 "F.Paste" user) '
132
+ '(36 "B.SilkS" user "B.Silkscreen") (37 "F.SilkS" user "F.Silkscreen") '
133
+ '(38 "B.Mask" user) (39 "F.Mask" user) (40 "Dwgs.User" user "User.Drawings") '
134
+ '(44 "Edge.Cuts" user) (45 "Margin" user "BoardOutline") (46 "B.CrtYd" user "B.Courtyard") '
135
+ '(47 "F.CrtYd" user "F.Courtyard") (48 "B.Fab" user) (49 "F.Fab" user) (50 "User.1" user))',
136
+ ' (setup (pad_to_mask_clearance 0))',
137
+ ' (net_class "Default" "" (clearance 0.2) (trace_width 0.25) (via_dia 0.6) (via_drill 0.3) (uvia_dia 0.3) (uvia_drill 0.1)'
138
+ + " " + "".join(f'(add_net "{esc(n)}")' for n in sorted(nets)) + ')']
139
+ L.append(' (net 0 "")')
140
+ for n in nets:
141
+ L.append(f' (net {ncode[n]} "{n.replace(chr(92), chr(92)*2).replace(chr(34), chr(92)+chr(34))}")')
142
+ # Edge.Cuts rectangle
143
+ pts = [(5, 5), (board_w, 5), (board_w, board_h - 5), (5, board_h - 5), (5, 5)]
144
+ for i in range(4):
145
+ L.append(f' (gr_line (start {pts[i][0]} {pts[i][1]}) (end {pts[i+1][0]} {pts[i+1][1]})'
146
+ f' (stroke (width 0.1) (type solid)) (layer "Edge.Cuts") (uuid "{U()}"))')
147
+ for (it, px, py) in placed:
148
+ c, km, _, _, (x0, y0) = it
149
+ ref = c["ref"]
150
+ pins = c["pins"]
151
+ # pad name -> net number string; match footprint pad names
152
+ fp_padnames = re.findall(r'\(pad "([^"]*)"', km) # file order; '' = np/mount hole
153
+ named = [n for n in fp_padnames if n]
154
+ mapping = {}
155
+ alias = ALIASES.get((ref, c["footprint"]))
156
+ if alias:
157
+ mapping = dict(alias)
158
+ elif set(pins) <= set(named):
159
+ pass # identity match on covered pins; extra pads (shell/np) stay net 0
160
+ elif len(named) == len(pins):
161
+ order_pins = sorted(pins, key=lambda pin: int(pin) if pin.isdigit() else 0)
162
+ for fpn, spp in zip(named, order_pins):
163
+ mapping.setdefault(fpn, spp)
164
+ warn.append(f"POSITIONAL-MAP {ref} {c['footprint']}: {len(named)} pads zipped to spec pins — NO authority table, verify datasheet")
165
+ else:
166
+ warn.append(f"NO-AUTHORITY-MAP {ref} {c['footprint']}: pads {sorted(set(named))[:8]} vs spec pins {sorted(pins)[:8]} -> design-data GAP, net 0 (refusing to guess)")
167
+ # origin so that bbox min lands at px,py
168
+ ox, oy = snap(px - x0), snap(py - y0)
169
+ head = f' (footprint "{esc(c["footprint"])}" (layer "F.Cu") (uuid "{U()}") (at {ox} {oy})'
170
+ head += (f' (property "Reference" "{esc(ref)}" (at 0 -1 0) (layer "F.SilkS") (effects (font (size 1 1) (thickness 0.15))))'
171
+ f' (property "Value" "{esc(c.get("mpn", ref))}" (at 0 1 0) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15))))')
172
+ body = km[km.index("(attr"):] if "(attr" in km else km[km.rindex(")"):]
173
+ # inject nets into pad blocks
174
+ out_km = []
175
+ pos = 0
176
+ for pb in forms(body):
177
+ m = re.match(r'\(pad "([^"]+)"', pb)
178
+ s, e = body.find(pb, pos), body.find(pb, pos) + len(pb)
179
+ if m:
180
+ padname = m.group(1)
181
+ pin = None if padname == "" else mapping.get(padname, padname if padname in pins else None)
182
+ if pin is not None:
183
+ net = pins[pin]
184
+ ins = f' (net {ncode[net]} "{esc(net)}")'
185
+ pb = pb[:-1] + ins + ")"
186
+ pos = e
187
+ out_km.append((s, pb))
188
+ rebuilt = body
189
+ for s, pb in reversed(out_km):
190
+ e = s + None if False else s
191
+ # simpler: replace each original pad block string once
192
+ # (do textual replacement with count=1 on the ORIGINAL blocks)
193
+ for orig_pb in [b for b in forms(body) if b.startswith('(pad ')]:
194
+ new_pb = next(p for (s, p) in out_km if s == body.find(orig_pb))
195
+ rebuilt = rebuilt.replace(orig_pb, new_pb, 1)
196
+ rebuilt = rebuilt[:rebuilt.rindex(")")] # strip trailing close of fp file
197
+ L.append(head + "\n" + rebuilt + "\n )")
198
+ L.append(')\n')
199
+ open(out_path, "w").write("\n".join(x for x in L if x) + "\n")
200
+ pro = out_path[:-len(".kicad_pcb")] + ".kicad_pro"
201
+ import json as _j
202
+ _j.dump({
203
+ "board": {"design_settings": {
204
+ "defaults": {"board_outline_line_width": 0.1, "copper_line_width": 0.2,
205
+ "solder_mask_clearance": 0.0, "solder_mask_min_width": 0.0,
206
+ "allow_soldermask_bridges_in_footprints": True},
207
+ "drc_exclusions": [],
208
+ "rules": {"min_hole_to_hole": 0.15, "min_through_hole_diameter": 0.15,
209
+ "min_clearance": 0.2, "min_connection": 0.0,
210
+ "min_copper_edge_clearance": 0.1, "min_hole_clearance": 0.15,
211
+ "min_silk_clearance": 0.0, "min_text_height": 0.8,
212
+ "min_text_thickness": 0.12, "min_track_width": 0.15,
213
+ "min_via_annular_width": 0.1, "min_via_diameter": 0.45,
214
+ "solder_mask_to_copper_clearance": 0.0, "use_height_for_length_calcs": True}}},
215
+ "libraries": {"fp_files": []},
216
+ "meta": {"filename": pro.split("/")[-1], "version": 3},
217
+ "net_settings": {"classes": [{"name": "Default", "clearance": 0.2,
218
+ "trace_width": 0.25, "via_dia": 0.6, "via_drill": 0.2, "uvia_dia": 0.3,
219
+ "uvia_drill": 0.1, "allow_soldermask_bridges_in_footprints": True}],
220
+ "meta": {"version": 3}},
221
+ "pcbnew": {"last_paths": {}},
222
+ "sheets": [], "text_variables": {}},
223
+ open(pro, "w"), indent=1)
224
+ return nets, warn, board_w, board_h
225
+
226
+
227
+ def esc(s):
228
+ return str(s).replace("\\", "\\\\").replace('"', '\\"')
229
+
230
+
231
+ if __name__ == "__main__":
232
+ nl = sys.argv[1]
233
+ out = sys.argv[2]
234
+ nets, warn, w, h = author(nl, out)
235
+ print(f"authored {out}: nets={len(nets)} board={w}x{h}mm")
236
+ for x in warn:
237
+ print("WARN", x)
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env python3
2
+ """r17 deterministic schematic author (RM-56: netlist bytes NEVER through a model).
3
+ Topology: NO wires. Every pin gets one global label anchored EXACTLY on the pin
4
+ end, rotation 180 (text left, away from body). Anchor == pin end by construction;
5
+ no dangling labels (r13 D1 defect class), no collinear merges. Grid 0.635 mm.
6
+ Usage: board_author_sch.py netlist.json -o out.kicad_sch"""
7
+ import json, re, argparse
8
+ from collections import defaultdict
9
+
10
+ G = 0.635
11
+ COLPITCH = 99.06 # column pitch (41-pin U1 fans 52mm left of anchor col)
12
+ ROWPITCH = 76.2 # row pitch; keep > 2.54 * maxpins/2 stack of neighbours
13
+ PINPITCH = 2.54 # pin vertical spacing (label height 1.27 leaves 1.27 gap)
14
+
15
+
16
+ def snap(v):
17
+ return round(v / G) * G
18
+
19
+
20
+ def esc(s):
21
+ return str(s).replace("\\", "\\\\").replace('"', '\\"')
22
+
23
+
24
+ class Uuid:
25
+ def __init__(self):
26
+ self.n = 0
27
+
28
+ def __call__(self):
29
+ self.n += 1
30
+ x = f"{self.n:032x}"
31
+ return f"{x[:8]}-{x[8:12]}-{x[12:16]}-{x[16:20]}-{x[20:32]}"
32
+
33
+
34
+ def pin_order(pins):
35
+ return sorted(pins, key=lambda p: int(p) if p.isdigit() else 0)
36
+
37
+
38
+ def build(netlist_path, out_path):
39
+ d = json.load(open(netlist_path))
40
+ comps = d["components"]
41
+ u = Uuid()
42
+ order = sorted(comps, key=lambda c: (c["ref"][0], int(re.sub(r"\D", "", c["ref"]) or 0)))
43
+ pos = {}
44
+ for slot, c in enumerate(order):
45
+ row, col = divmod(slot, 4)
46
+ pos[c["ref"]] = (snap(60 + col * COLPITCH), snap(60 + row * ROWPITCH))
47
+ # symbol defs: single-sided fan-left, PINPITCH spacing
48
+ prefix_pins = {}
49
+ pinset = {}
50
+ for c in order:
51
+ ref = c["ref"]
52
+ ps = pin_order(c["pins"])
53
+ key = "SR_" + re.sub(r"\d+$", "", ref)
54
+ if key in prefix_pins and prefix_pins[key] != ps:
55
+ key = "SR_" + ref
56
+ prefix_pins.setdefault(key, ps)
57
+ c["_key"] = key
58
+ sx, sy = pos[ref]
59
+ n = len(ps)
60
+ # world y-down vs lib-symbol-local y-UP: the def emits oy=+off (up) per
61
+ # pin slot i; the label anchor in SHEET coords must therefore SUBTRACT.
62
+ pinset[ref] = [(p, snap(sx - 5.08), snap(sy - ((n - 1) / 2 - i) * PINPITCH))
63
+ for i, p in enumerate(ps)]
64
+ labels = []
65
+ for c in order:
66
+ for pnum, net in c["pins"].items():
67
+ px, py = next((ax, ay) for (pp, ax, ay) in pinset[c["ref"]] if pp == pnum)
68
+ labels.append((net, px, py))
69
+ L = ['(kicad_sch (version 20250114) (generator "board_author_sch") (generator_version "10.0.5")',
70
+ f' (uuid "{u()}")', ' (paper "A3")', ' (lib_symbols']
71
+ for key, ps in sorted(prefix_pins.items()):
72
+ n = len(ps)
73
+ L.append(f' (symbol "solar_ray:{esc(key)}"')
74
+ L.append(' (pin_numbers (hide yes))')
75
+ L.append(' (pin_names (offset 1.016))')
76
+ L.append(' (exclude_from_sim no) (in_bom yes) (on_board yes)')
77
+ L.append(' (property "Reference" "U" (at 0 0 0) (effects (font (size 1.27 1.27))))')
78
+ L.append(' (property "Value" "V" (at 0 0 0) (effects (font (size 1.27 1.27))))')
79
+ L.append(' (property "Footprint" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))')
80
+ L.append(' (property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))')
81
+ L.append(f' (symbol "{esc(key)}_1_1"')
82
+ for i, p in enumerate(ps):
83
+ oy = snap(((n - 1) / 2 - i) * PINPITCH)
84
+ L.append(f' (pin passive line (at {-5.08} {oy} 0) (length 5.08)'
85
+ f' (name "~" (effects (font (size 1.016 1.016))))'
86
+ f' (number "{esc(p)}" (effects (font (size 1.016 1.016)))))')
87
+ L.append(' )')
88
+ L.append(' (embedded_fonts no)')
89
+ L.append(' )')
90
+ L.append(' )')
91
+ for c in order:
92
+ ref = c["ref"]
93
+ sx, sy = pos[ref]
94
+ L.append(f' (symbol (lib_id "solar_ray:{esc(c["_key"])}") (at {sx} {sy} 0) (unit 1)')
95
+ L.append(' (exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no) (fields_autoplaced no)')
96
+ L.append(f' (uuid "{u()}")')
97
+ for k, v, xo in (("Reference", ref, -2.54), ("Value", c.get("mpn", ref), 2.54),
98
+ ("Footprint", c.get("footprint", ""), 0.0), ("Datasheet", "", 0.0)):
99
+ L.append(f' (property "{k}" "{esc(v)}" (at {snap(sx+xo)} {snap(sy+5.08)} 0)'
100
+ f' (effects (font (size 1.27 1.27)) (hide yes)))')
101
+ for (p, _, _) in pinset[ref]:
102
+ L.append(f' (pin "{esc(p)}" (uuid "{u()}"))')
103
+ L.append(' )')
104
+ for net, x, y in labels:
105
+ L.append(f' (global_label "{esc(net)}" (shape passive) (at {x} {y} 180)'
106
+ f' (effects (font (size 1.27 1.27)) (justify left)) (uuid "{u()}"))')
107
+ L += [' (sheet_instances', ' (path "/" (page "1"))', ' )', ' (embedded_fonts no)', ')']
108
+ open(out_path, "w").write("\n".join(L) + "\n")
109
+ return len(order), len(labels)
110
+
111
+
112
+ if __name__ == "__main__":
113
+ ap = argparse.ArgumentParser()
114
+ ap.add_argument("netlist")
115
+ ap.add_argument("-o", "--out", required=True)
116
+ a = ap.parse_args()
117
+ s, l = build(a.netlist, a.out)
118
+ print(f"emitted {a.out}: symbols={s} labels={l}")
@@ -0,0 +1,23 @@
1
+ #!/bin/bash
2
+ # r17 finalization pipeline: routed board -> pour -> native refill -> DRC gate -> fab exports
3
+ set -e
4
+ IN="${1:?routed kicad_pcb}"
5
+ OUT="${2:?output board pcb}"
6
+ RUN="${3:-evolution-round-17}"
7
+ cd /home/lab/workspace/pcb-control
8
+ python3 tools/board_zone_pour.py "$IN" "$OUT"
9
+ cp /tmp/sr-c4.kicad_pro "$(dirname "$OUT")/$(basename "$OUT" .kicad_pcb).kicad_pro"
10
+ D=/home/lab/workspace/pcb-control/reports/recovery/$RUN
11
+ kicad-cli pcb drc "$OUT" --refill-zones --save-board --format json --severity-error --output "$D/$(basename "$OUT").drc-refill.json" >/dev/null 2>&1
12
+ kicad-cli pcb drc "$OUT" --format json --severity-error --output "$D/$(basename "$OUT").drc-final.json" >/dev/null 2>&1
13
+ python3 - "$D/$(basename "$OUT").drc-final.json" <<'EOF'
14
+ import json, sys, collections
15
+ d = json.load(open(sys.argv[1]))
16
+ v = d.get('violations', []); u = d.get('unconnected_items', [])
17
+ print(f"GATE: errors={len(v)} {dict(collections.Counter(x.get('type','?') for x in v))} unconnected={len(u)}")
18
+ EOF
19
+ mkdir -p "$D/fab-out"
20
+ kicad-cli pcb export gerbers "$OUT" -o "$D/fab-out/gerbers.zip" >/dev/null
21
+ kicad-cli pcb export drill "$OUT" -o "$D/fab-out/" >/dev/null
22
+ kicad-cli pcb export pos "$OUT" -o "$D/fab-out/pos.pos" --units mm >/dev/null
23
+ echo "fab exports -> $D/fab-out/"; ls "$D/fab-out/" | head