RFC-015's partner-handoff artifact: cohdl build --emit ipc2581 writes
out/<package-name>.xml, an IPC-2581 revision B1 document conforming to
the IPC 2581 Consortium's published IPC-2581B1.xsd (vendored for the test
gate at tests/schema/IPC-2581B1.xsd; validated with xmllint --schema in
tests/ipc2581.rs). It is a fourth output alongside the KiCad .net, BOM
CSV, and layout.json — additive, never a replacement, and emitted only
when --emit ipc2581 is requested (build --json then lists it under
build.ipc2581, the same only-when-emitted pattern as build.layout).
The document carries the real physical geometry a layout tool routes — a
DictionaryStandard of primitive shapes, PadStackDefs (copper/mask/paste +
a plated PadstackHoleDef for through-hole pads), real layers (F.Cu/B.Cu/
masks/paste) + a stackup, and side-aware LayerFeatures for every PLACED
copper/mask/enabled-paste feature, each at its absolute board position and tied
to its component pin (PinRef) and net (Set/@net). Component and pin mount
types are real (SMT
vs THMT; SURFACE_MOUNT_PAD / THROUGH_HOLE_PIN). Without this physical
layer, an XSD-valid document that carried only abstract Package/Pin land
patterns showed no copper/holes/ratsnest in Quilter (finding:
.co/invalid-ipc2581.xml) — so it is emitted.
What is still NOT done, and the markers say so (never overclaiming): final
component placement (unlocked components are STAGED just outside the
outline — the "please place me" idiom — not placed) and routing (no copper
traces). The document therefore carries the marker
logical-complete,placement-staged,unrouted in three
machine-or-human-visible places:
FunctionMode/@comment—logical-complete,placement-staged,unrouted — layout not yet performed.@modeisASSEMBLY: the mode names the data function the document serves (assembly-level: BOM + components + nets + land patterns + padstacks), not a completeness claim — and real consumers key their fabrication-layer rendering off a standard mode (under the earlierUSERDEFa consumer legitimately skipped everyLayerFeature, drawing components without pads). The honest completeness disclosure lives in this comment and the machine-readable marker below;HistoryRecord/FileRevision/@comment— same marker;- a machine-readable
NonstandardAttributeon the Step:name="COHDL_COMPLETENESS" value="logical-complete,placement-staged,unrouted".
A consumer must check the marker before assuming physical data is present.
The Step Datum is a degenerate (0,0). Component placement depends on
whether a board outline exists: a layout tool such as Quilter treats every
component inside the outline as pre-placed/locked and only places the ones
left outside it (docs.quilter.ai). So when the design declares a
board_outline, components are staged in a deterministic, non-overlapping
grid just OUTSIDE the outline (to its right) — the standard "unplaced,
please place me" signal — rather than piled at (0,0) (which is inside the
outline and would read as N components locked at the board centre). A
component named by a place <inst> at (x, y) [rotate ANGLE] statement
(RFC-020) is instead emitted at that fixed position (inside the outline =
the layout tool treats it as pre-placed/locked) and is NOT staged — this is
how board-edge/mechanical parts (the Pico's 40-pin castellated header, USB,
SWD headers) are fixed while the rest are placed. An optional rotate (a
closed set {0, 90, 180, 270}) rides Component/Xform/@rotation (schema order:
NonstandardAttribute*, Xform?, Location); an unrotated component emits no
Xform. Staging and placement positions are exact over the femto integers and
byte-stable.
Coordinate frame. IPC-2581 is a +y-up (screen-up) frame; CoHDL authors
geometry +y-down (as KiCad does internally, and as the design's place/DXF
coordinates read). Every emitted y is therefore NEGATED into the IPC frame —
component Locations, placed-copper/hole Locations, Package land-pattern
Pin and courtyard Outline corners, and the board Profile (whose arc
winding also flips). Rotation values are UNCHANGED: this reproduces KiCad's own
kicad-cli pcb export ipc2581 --version B placement of rotated, y-offset pads
exactly (verified component-for-component and pad-for-pad against a KiCad
reference export), so the document renders in a consumer (Quilter) the same way
up as the design and as KiCad's board. The pad computation negates the local
offset BEFORE rotation and the component position, not the final absolute — a
naive reflection of the absolute position mis-places rotated pads. (The
.kicad_mod files stay in KiCad's native +y-down frame, so their y is the
negation of the IPC land pattern's — same magnitude, exact.)
With no outline there is nothing to
stage against, so Component/Location keeps the (0,0) placeholder. Either
way the staged positions are NOT a real placement — they are the input a
placement engine consumes, and the marker still governs.
A Package whose footprint has RFC-018 pad placements carries its land
pattern — its courtyard (or, when a footprint deliberately omits the
courtyard, a non-degenerate Outline derived from the pad extents —
a degenerate (0,0)-(0,0) polygon made such packages invisible) and one
logical Pin element per distinct electrical pad number. Its Location
and StandardPrimitiveRef into the shared DictionaryStandard come from
a deterministic representative placement (footprint-local top SMD before
bottom SMD before PTH, then the largest land), so a thermal via authored before
its exposed-pad land cannot turn that logical pin into a through-hole pin. The
dictionary form is the encoding real importers resolve; an inline primitive
under <Pin> is schema-valid via the StandardShape substitution group but was
invisible to a real importer. type SURFACE/THRU, electricalType="ELECTRICAL", and
mountType SURFACE_MOUNT_PAD/THROUGH_HOLE_PIN describe that logical
terminal. Every repeated physical placement is retained separately below.
The placed copper — with layer + drill — is in the PadStackDef/
LayerFeature structures below; a footprint still at the RFC-017 placeholder
stage (empty body) keeps the zero-size-polygon idiom.
Board outline (Step/Profile, RFC-020). When the design declares a
board_outline: "path.dxf" in its layout {} block, cohdl build opens the
referenced DXF and extracts exactly one closed outline entity — a closed
LWPOLYLINE/POLYLINE on the Edge.Cuts layer (the documented
convention; everything else in the file is ignored). Its real geometry is
embedded in a Step/Profile/Polygon between Datum and the first Package:
straight edges become PolyStepSegment, DXF arc bulges become PolyStepCurve
(end point + center + clockwise). Vertex coordinates are parsed to exact
femto-mm integers; only an arc's center (which a bulge implies but does not
state) is computed in f64 and rounded to femto ONCE at extraction, so both
emitters read identical integers. A missing / unparseable / non-closed / no-
entity-on-Edge.Cuts DXF is a named E1006 build error — never a silent wrong
Profile. A design with no board_outline emits no Profile. The outline
also rides layout.json ("board_outline": { "source", "start", "segments" }
/ null). This is a pragmatic PCB-partner extension beyond RFC-015's original
scope, formalized by RFC-020 (docs/compliance-report.md).
Physical padstacks + placed copper (closes review R5-8). The copper
layer and drill the abstract Package/Pin subset can't carry are now
emitted properly: a PadStackDef per unique copper/mask/paste geometry,
plating, drill, and physical side. A through-hole padstack has copper/mask on
both outer faces plus a plated PadstackHoleDef carrying its real drill. An
SMD padstack has copper/mask and, unless suppressed, paste only on the physical
front or back face selected by the footprint pad's local layer and the
component-side flip. A LayerFeature per layer the padstack lands on places
each padstack at its absolute board position, tied to the component pin
(PinRef) and net (Set/@net). Bottom-side placement mirrors the pad's local
x, reflects an asymmetric chamfer, and reverses the pad-local rotation before
the component rotation is applied; this is the same mirror-then-rotate model
used for the position. The primitives preserve the bounded per-pad chamfer,
four-corner RectRound radius, mask_expansion, and
paste: none | (width, height) | circle(diameter) overrides from the
provisional syntax, including a circular aperture either smaller or larger
than its copper land. When one electrical pad number has multiple physical
placements, every placement remains in these side-aware layer features with
the same PinRef; the package-level logical list contains one Pin per
distinct number. Annular copper is an exact Contour with a circular
Cutout; segmented annular paste becomes four separate filled contour
primitives on the one paste layer while retaining a single
logical/electrical pin identity. A consumer
compositing "visible pad = copper through
a mask aperture" sees no pads at all when only the copper is instanced. A
drill LayerFeature (F.Cu_B.Cu, a DRILL-function Layer spanning
F.Cu→B.Cu) carries one located, plated <Hole> per through-hole pad
(Set/@geometry names the padstack) — the board-level form hole-rendering
consumers key on; the padstack-level PadstackHoleDef alone stays invisible.
Every fabrication layer is declared as a Content/LayerRef and a Layer,
and the Stackup lists the full 9-row top→bottom fab sequence
(silk/paste/mask/F.Cu(0.035)/DIELECTRIC_1(1.51)/B.Cu(0.035)/mask/
paste/silk, masks 0.01 — summing to the declared 1.6mm; the single
FAB_LAYERS table feeds all three). Listing the mask/paste rows and the
dielectric matters: a consumer builds its renderable-layer model from the
StackupLayer sequence, and a copper-only stack (two directly-adjacent
conductors, no mask rows) never composites a visible pad. This is what makes
the copper, holes, and ratsnest visible/routable
in a consumer (Quilter) — the finding in .co/invalid-ipc2581.xml. (The
earlier "moot for an all-SMD board" note was stale: rpi-pico2's USB +
castellations are through-hole with real 0.44/1.0/1.35 mm drills.) Still not
projected into the IPC document: the footprint's silkscreen_ref (present
in the .kicad_mod).
| Section | Content | Source of truth it must agree with |
|---|---|---|
Ecad/CadData/Step/LogicalNet |
every merged net; one PinRef (refdes, pin number) per device physical pin number. Repeated footprint placements reuse that reference in physical layer features rather than duplicating the logical net member |
the .net's node list, exactly |
LogicalNet/@netClass |
GROUND for [gnd] nets, POWER for voltage-annotated nets, else SIGNAL |
the net annotations |
Step/Component |
one per instance, designator order; @part = MPN, @packageRef = footprint-derived package |
the .net comps |
Component/NonstandardAttribute |
COHDL_DEVICE, COHDL_PATH, one COHDL_SPEC_<field> per resolved spec value (RFC-001 literal text), COHDL_PLACEMENT_HINT when present |
the IR / layout.json hints |
Bom + Avl |
one BomItem per MPN group (quantity, RefDes children, MPN/MFR/VALUE Textual characteristics), backed by one AvlItem per group (the XSD's keyrefs require the AVL). AvlMpn/@name is the group's collision-free key; the TRUE MPN rides AvlMpn/@other — two distinct MPNs never collapse to one AVL identity |
the BOM CSV rows, exactly — one item per row, quantity = the Designator cell's count, MPN/MFR Textuals = the Comment/Manufacturer cells; VALUE is XML-only (the assembly-style CSV carries no value column) and is pinned to the IR's principal value instead |
CadHeader/Spec |
RFC-013 constraints as CoHDL vendor extensions (NOT native IPC constraint semantics — IPC-2581B1 has no user-named net-class element, and there is no SpecRef linking these to a LogicalNet): cohdl:net_class:<name>, cohdl:diff_pair:<p>:<n>, cohdl:length_match:<i>, each with General/Property members (net, positive/negative, tolerance). Recoverable only by a CoHDL-aware decoder; a generic IPC/Quilter consumer will not honor them. See review F2 in docs/compliance-report.md (deviation pending note-side amendment). |
layout.json |
Fidelity is enforced by tests/ipc2581.rs over the fixture corpus AND both
repo examples: nets/components against the .net, BOM items against the
CSV, constraints/hints generically against the parsed layout.json, and
every COHDL_SPEC_* map against the IR's resolved spec values — the
emitters are independent consumers of the same DesignIr, and the tests
fail the moment any of them disagrees for the same design.
Same source + same std → same bytes (Constitution hard constraint). Two consequences visible in the document:
- every schema-required
xsd:dateTime(HistoryRecord,AvlHeader) is the fixed epoch instant1970-01-01T00:00:00Z— the wall clock never enters an artifact; - every list is explicitly ordered (components by designator, nets by name, BOM groups by MPN, packages by footprint name).
The XSD restricts key/reference names (qualifiedNameType, shortName) to
an identifier charset. Names CoHDL controls (designators, net names) already
fit. Free-text part fields are handled as:
- Package names (from the footprint symbol): the CoHDL fully-qualified
separator
::is collapsed to a single-(rpi_pico2-CHIP_0805, KiCad's own convention) and any other character outside[A-Za-z0-9_\-.+]becomes_. Although the XSD'squalifiedNameTypepermits:, it is the XML QName/namespace delimiter — a consumer whose pin/pad resolution treats a package name as an NCName can fail to bind pins to their land pattern, so the emitted name is colon-free. The same string is written verbatim as bothPackage/@nameand everyComponent/@packageRef, so they stay matched; when sanitizing changes the name the raw footprint symbol is preserved inPackage/@comment. Post-sanitize collisions get_suffixes (deterministic). - BOM/AVL keys (from the MPN): same rule (no
:), raw MPN always in theMPNTextual characteristic. - Designators (
Component/@refDes,RefDes/@name,PinRef/@componentRef): the document's XSD-enforcedcomponentKey. All three spellings come from one collision-free table (same rule as above); identity for every ordinary designator, and only reachable at all via a hostiledesignator_prefixstring. - MPN in
Component/@part, MFR, hints, specs: plainxsd:stringattributes — raw values, XML-escaped.
Character fidelity in attribute values: tab and CR are emitted as
character references (	/ ) so conforming parsers preserve them
(a literal tab would be normalized to a space, silently diverging from
layout.json/the BOM CSV). Characters XML 1.0 forbids outright (C0
controls other than tab/LF/CR) are replaced with U+FFFD — the emitter's one
non-value-preserving projection; the alternative is a document no parser
will open. Everything else is value-preserving.
No board outline/stackup (footprint geometry itself landed with RFC-018 —
Package/Outline + Pin above — but component placement stays degenerate:
layout has not been performed), no ECO or
incremental re-route story (a regenerated document is a fresh full handoff),
and no claim of a usable Quilter handoff: a Quilter starter board needs
a valid board outline and PLACED footprints, and this document has neither
(no Profile; every Component/Location is (0,0) — layout has not been
performed). The artifact is a schema-valid logical-interchange document with
real footprint geometry, and its real-partner gate (an actual import/
job-creation pass against a consuming tool) is open — tracked as an
RFC-015 acceptance blocker in docs/compliance-report.md (review F1).