0hmX/am3352
This code suite comprises TypeScript scripts that analyze, verify, and assemble complex DDR memory interface hardware, focusing on physical routing, via and pad placement, electrical clearance, and physical constraints, often involving precise geometric calculations and consistent provenance tracking.
- Version
- 1.0.5
- License
- unset
- Stars
- 0
scripts/check-ddr-system-copper.ts
import {readFileSync,writeFileSync} from 'node:fs'
import {auditRouteAngles} from './check-route-angles'
type P={x:number;y:number}
type Polygon={outer:P[];holes:P[][];edges:Array<[P,P]>;bounds:{minX:number;maxX:number;minY:number;maxY:number}}
type Shape={polygon?:Polygon;traceId?:string;id:string;net:string;layer:string;a:P;b:P;r:number;fresh:boolean;rect?:{minX:number;maxX:number;minY:number;maxY:number};join?:string}
class Union {p=new Map<string,string>();find(x:string):string {const p=this.p.get(x);if(!p){this.p.set(x,x);return x}if(p===x)return x;const r=this.find(p);this.p.set(x,r);return r}join(a:string,b:string){this.p.set(this.find(a),this.find(b))}}
const distance=(a:P,b:P)=>Math.hypot(a.x-b.x,a.y-b.y)
function ps(p:P,a:P,b:P){const dx=b.x-a.x,dy=b.y-a.y,t=Math.max(0,Math.min(1,((p.x-a.x)*dx+(p.y-a.y)*dy)/(dx*dx+dy*dy)||0));return distance(p,{x:a.x+t*dx,y:a.y+t*dy})}
function ss(a:P,b:P,c:P,d:P){const cross=(p:P,q:P,r:P)=>(q.x-p.x)*(r.y-p.y)-(q.y-p.y)*(r.x-p.x);if(cross(a,b,c)*cross(a,b,d)<0&&cross(c,d,a)*cross(c,d,b)<0)return 0;return Math.min(ps(a,c,d),ps(b,c,d),ps(c,a,b),ps(d,a,b))}
function sr(a:P,b:P,r:NonNullable<Shape['rect']>){const inside=(p:P)=>p.x>=r.minX&&p.x<=r.maxX&&p.y>=r.minY&&p.y<=r.maxY;if(inside(a)||inside(b))return 0;const p=[{x:r.minX,y:r.minY},{x:r.maxX,y:r.minY},{x:r.maxX,y:r.maxY},{x:r.minX,y:r.maxY}];return Math.min(...p.map((v,i)=>ss(a,b,v,p[(i+1)%4]!)))}
const ringEdges=(ring:P[]):Array<[P,P]>=>ring.map((p,i)=>[p,ring[(i+1)%ring.length]!])
const ringBounds=(ring:P[])=>({minX:Math.min(...ring.map(p=>p.x)),maxX:Math.max(...ring.map(p=>p.x)),minY:Math.min(...ring.map(p=>p.y)),maxY:Math.max(...ring.map(p=>p.y))})
/** -1 outside, 0 boundary, 1 strictly inside; ring winding is immaterial. */
function ringLocation(p:P,ring:P[]):number {
let inside=false
for(const [a,b]of ringEdges(ring)){
if(ps(p,a,b)<=1e-10)return 0
if((a.y>p.y)!==(b.y>p.y)&&p.x<(b.x-a.x)*(p.y-a.y)/(b.y-a.y)+a.x)inside=!inside
}
return inside?1:-1
}
function filled(p:P,polygon:Polygon){return ringLocation(p,polygon.outer)>=0&&!polygon.holes.some(h=>ringLocation(p,h)===1)}
function parsePour(e:any):Polygon {
if(e.shape!=='brep'||!e.brep_shape||Object.keys(e.brep_shape).some(k=>!['outer_ring','inner_rings'].includes(k))||!Array.isArray(e.brep_shape.inner_rings))throw Error('Only BREP outer_ring plus inner_rings is supported')
const ring=(r:any):P[]=>{
if(!r||Object.keys(r).some(k=>k!=='vertices')||!Array.isArray(r.vertices))throw Error('Unsupported BREP ring representation')
const pts=r.vertices.map((p:any)=>{if(Object.keys(p).some(k=>!['x','y'].includes(k)))throw Error('Unsupported pour vertex representation');if(!Number.isFinite(p.x)||!Number.isFinite(p.y))throw Error('Nonfinite pour coordinate');return {x:p.x,y:p.y}})
if(pts.length>1&&distance(pts[0],pts.at(-1))<1e-10)pts.pop()
if(pts.length<3)throw Error('Pour ring needs three vertices')
const edges=ringEdges(pts)
if(edges.some(([a,b])=>distance(a,b)<1e-10))throw Error('Zero length pour edge')
const area=edges.reduce((v,[a,b])=>v+a.x*b.y-b.x*a.y,0)
if(Math.abs(area)<1e-10)throw Error('Degenerate pour ring')
for(let i=0;i<edges.length;i++)for(let j=i+1;j<edges.length;j++){
if(j===i+1||(i===0&&j===edges.length-1))continue
if(ss(...edges[i]!,...edges[j]!)<1e-10)throw Error('Self-intersecting pour ring')
}
return pts
}
const outer=ring(e.brep_shape.outer_ring),holes=e.brep_shape.inner_rings.map(ring) as P[][],outerEdges=ringEdges(outer)
for(const h of holes){
if(h.some(p=>ringLocation(p,outer)!==1))throw Error('Hole not strictly inside pour outer ring')
if(ringEdges(h).some(edge=>outerEdges.some(other=>ss(...edge,...other)<1e-10)))throw Error('Hole crosses outer ring')
}
for(let i=0;i<holes.length;i++)for(let j=i+1;j<holes.length;j++){
const a=holes[i]!,b=holes[j]!,ba=ringBounds(a),bb=ringBounds(b)
if(ba.maxX<bb.minX||bb.maxX<ba.minX||ba.maxY<bb.minY||bb.maxY<ba.minY)continue
if(ringLocation(a[0]!,b)>=0||ringLocation(b[0]!,a)>=0||ringEdges(a).some(edge=>ringEdges(b).some(other=>ss(...edge,...other)<1e-10)))throw Error('Overlapping or nested pour holes')
}
return {outer,holes,edges:[outer,...holes].flatMap(ringEdges),bounds:ringBounds(outer)}
}
function segmentPour(a:P,b:P,p:Polygon){
if(filled(a,p)||filled(b,p))return 0
return Math.min(...p.edges.map(([c,d])=>ss(a,b,c,d)))
}
function polygonGap(a:Polygon,b:Polygon){
if(a.outer.some(p=>filled(p,b))||b.outer.some(p=>filled(p,a)))return 0
let d=Infinity
for(const edge of a.edges)for(const other of b.edges){d=Math.min(d,ss(...edge,...other));if(d===0)return 0}
return d
}
function rectanglePolygon(r:NonNullable<Shape['rect']>):Polygon{
const outer=[{x:r.minX,y:r.minY},{x:r.maxX,y:r.minY},{x:r.maxX,y:r.maxY},{x:r.minX,y:r.maxY}]
return {outer,holes:[],edges:ringEdges(outer),bounds:r}
}
export function ddrCopperShapeGap(a:Shape,b:Shape){
if(a.polygon||b.polygon){
const p=a.polygon?a:b,q=a.polygon?b:a
return q.polygon?polygonGap(p.polygon!,q.polygon):q.rect?polygonGap(p.polygon!,rectanglePolygon(q.rect)):segmentPour(q.a,q.b,p.polygon!)-q.r
}
if(a.rect&&b.rect)return Math.hypot(Math.max(0,a.rect.minX-b.rect.maxX,b.rect.minX-a.rect.maxX),Math.max(0,a.rect.minY-b.rect.maxY,b.rect.minY-a.rect.maxY))
return (a.rect?sr(b.a,b.b,a.rect):b.rect?sr(a.a,a.b,b.rect):ss(a.a,a.b,b.a,b.b))-a.r-b.r
}
const gap=ddrCopperShapeGap
function auditJoinedBendShapes(shapes:Shape[],explicitVias:Array<{net:string;layer:string;p:P}>,includeFixed=false){
// Local junction rule: degree-two continuations turn <=45 degrees; a
// branching junction must admit one incoming ray that turns <=45 degrees
// toward every outgoing ray. This permits Y splits but rejects T/X splits.
// This checks local geometric feasibility, not source-rooted timing topology.
const joinedBends:any[]=[],junctions=new Map<string,{net:string;layer:string;p:P;rays:Array<{angle:number;fresh:boolean;id:string}>}>()
for(const shape of shapes){if(shape.rect||shape.polygon||distance(shape.a,shape.b)<1e-7)continue
for(const [p,q]of [[shape.a,shape.b],[shape.b,shape.a]]){const key=`${shape.net}:${shape.layer}:${p!.x.toFixed(6)}:${p!.y.toFixed(6)}`;let junction=junctions.get(key);if(!junction){junction={net:shape.net,layer:shape.layer,p:p!,rays:[]};junctions.set(key,junction)}
const angle=Math.atan2(q!.y-p!.y,q!.x-p!.x)*180/Math.PI
const old=junction.rays.find(r=>Math.abs(((r.angle-angle+540)%360)-180)<1e-4)
if(old)old.fresh ||= shape.fresh;else junction.rays.push({angle,fresh:shape.fresh,id:shape.id})
}
}
// The all-copper audit additionally resolves centerline intersections and
// endpoint-on-segment joins. Keep the historical fresh-only path unchanged.
if(includeFixed){
const groups=new Map<string,Shape[]>()
for(const shape of shapes)if(!shape.rect&&!shape.polygon&&distance(shape.a,shape.b)>=1e-7){const key=JSON.stringify([shape.net,shape.layer]),group=groups.get(key)??[];group.push(shape);groups.set(key,group)}
for(const group of groups.values()){
for(let i=0;i<group.length;i++)for(let j=i+1;j<group.length;j++){
const a=group[i]!,b=group[j]!,ax=a.b.x-a.a.x,ay=a.b.y-a.a.y,bx=b.b.x-b.a.x,by=b.b.y-b.a.y,den=ax*by-ay*bx
if(Math.abs(den)<1e-12)continue
const dx=b.a.x-a.a.x,dy=b.a.y-a.a.y,t=(dx*by-dy*bx)/den,u=(dx*ay-dy*ax)/den
if(t<-1e-8||t>1+1e-8||u<-1e-8||u>1+1e-8)continue
const p={x:a.a.x+t*ax,y:a.a.y+t*ay},key=`${a.net}:${a.layer}:${p.x.toFixed(6)}:${p.y.toFixed(6)}`
if(!junctions.has(key))junctions.set(key,{net:a.net,layer:a.layer,p,rays:[]})
}
}
for(const junction of junctions.values())for(const shape of groups.get(JSON.stringify([junction.net,junction.layer]))??[]){
if(ps(junction.p,shape.a,shape.b)>1e-7)continue
for(const q of[shape.a,shape.b])if(distance(junction.p,q)>=1e-7){
const angle=Math.atan2(q.y-junction.p.y,q.x-junction.p.x)*180/Math.PI
if(!junction.rays.some(r=>Math.abs(((r.angle-angle+540)%360)-180)<1e-4))junction.rays.push({angle,fresh:shape.fresh,id:shape.id})
}
}
}
// Index via centers for the exact coincidence test; neighboring cells retain
// points straddling a bucket boundary. The final Euclidean test is unchanged.
const viaCenters=new Map<string,P[]>(),cell=1e-6
const viaKey=(n:string,l:string,x:number,y:number)=>JSON.stringify([n,l,x,y])
const addVia=(n:string,l:string,p:P)=>{const key=viaKey(n,l,Math.floor(p.x/cell),Math.floor(p.y/cell)),points=viaCenters.get(key)??[];points.push(p);viaCenters.set(key,points)}
for(const s of shapes)if(s.join?.startsWith('via:')&&distance(s.a,s.b)<1e-7)addVia(s.net,s.layer,s.a)
for(const v of explicitVias)addVia(v.net,v.layer,v.p)
for(const junction of junctions.values()){
const rays=junction.rays;if(rays.length<2||(!includeFixed&&!rays.some(r=>r.fresh)))continue
// A physical same-net via terminates this planar run. A merely nearby via
// must not exempt a sharp bend: its center must coincide with the junction.
let atVia=false
const cellX=Math.floor(junction.p.x/cell),cellY=Math.floor(junction.p.y/cell)
for(let dx=-1;dx<=1&&!atVia;dx++)for(let dy=-1;dy<=1&&!atVia;dy++)atVia=(viaCenters.get(viaKey(junction.net,junction.layer,cellX+dx,cellY+dy))??[]).some(p=>distance(p,junction.p)<1e-6)
if(atVia)continue
const turn=(a:number,b:number)=>180-Math.abs(((a-b+540)%360)-180)
if(!rays.some(incoming=>rays.every(outgoing=>outgoing===incoming||turn(incoming.angle,outgoing.angle)<=45.0001)))joinedBends.push({net:junction.net,layer:junction.layer,x:junction.p.x,y:junction.p.y,degree:rays.length,rays:rays.map(r=>r.angle),kind:rays.length===2?'turn sharper than 45 degrees':'branch has no valid 45-degree incoming direction'})
}
return joinedBends
}
/** Audit every same-net trace junction, including fixed power/support copper.
* Uses identical source identity resolution, exact via-center termination and
* branch rules as the physical checker. All geometry is fixed here, so no
* fresh-signal layer restriction is applied. Does not certify clearance,
* individual route octilinearity, or source-rooted branch topology. */
export function auditDdrTraceJunctions(circuit:any[],connections:any[]=[],aliases:Record<string,string>={}){
const report=verifyDdrSystemCopper(circuit,[],connections,aliases,connections.length,{includeFixedJoinedBends:true})
return{valid:!report.errors.length&&!report.joinedBends.length,errors:report.errors,joinedBends:report.joinedBends,scope:'All fixed and candidate trace centerline junctions, including interior intersections; same-net exact-center vias terminate planar runs. No fresh-signal layer gate.'}
}
// The brute-force mode is retained as a regression oracle for the broad phase.
type CopperCheckOptions={pairSearch?:'sweep'|'brute-force';includeFixedJoinedBends?:boolean;reportSameNetContacts?:boolean}
export function verifyDdrSystemCopper(original:any[],newTraces:any[],connections:any[],aliases:Record<string,string>={},expectedNetCount=50,options:CopperCheckOptions={}){
const electrical=new Union(), physical=new Union(),layers=['top',...Array.from({length:8},(_,i)=>`inner${i+1}`),'bottom'],byId=new Map<string,any>()
for(const e of original)for(const [k,v]of Object.entries(e))if(k===`${e.type}_id`)byId.set(String(v),e)
for(const t of original.filter(e=>e.type==='source_trace')){const ids=[t.source_trace_id,...t.connected_source_port_ids??[],...t.connected_source_net_ids??[]];for(const id of ids)electrical.join(ids[0],id)}
for(const [a,b]of Object.entries(aliases))electrical.join(a,b)
for(const c of connections)for(const id of [c.source_trace_id,...c.mergedConnectionNames??[],c.rootConnectionName].filter(Boolean))electrical.join(c.name,id)
const verifiedViaOwners=new Map<string,string>(),ownershipErrors:string[]=[]
for(const via of original.filter(e=>e.type==='pcb_via'&&e.pcb_trace_id)){
const parent=byId.get(via.pcb_trace_id),owner=parent?.source_trace_id
const matching=parent?.type==='pcb_trace'&&parent.route?.some((p:any)=>p.route_type==='via'&&Number.isFinite(p.x)&&Number.isFinite(p.y)&&Math.hypot(p.x-via.x,p.y-via.y)<=1e-6&&Math.abs(p.via_diameter-via.outer_diameter)<=1e-6&&Math.abs(p.via_hole_diameter-via.hole_diameter)<=1e-6)
if(!matching||!owner||byId.get(owner)?.type!=='source_trace'){
ownershipErrors.push(`Unverified via parent ownership ${via.pcb_via_id}: ${via.pcb_trace_id} needs matching inline via XY, copper/drill dimensions and explicit source trace`)
continue
}
const explicit=[via.source_trace_id,via.source_net_id].filter(Boolean)
if(explicit.some(id=>electrical.find(id)!==electrical.find(owner))){ownershipErrors.push(`Conflicting explicit via ownership ${via.pcb_via_id}: ${via.pcb_trace_id}`);continue}
verifiedViaOwners.set(via.pcb_via_id,owner)
}
const portSource=(id:string)=>byId.get(id)?.source_port_id
const net=(e:any):string=>{
const ids=[e.source_trace_id,e.source_net_id,e.source_port_id,e.type==='pcb_via'?verifiedViaOwners.get(e.pcb_via_id):undefined,e.pcb_port_id?portSource(e.pcb_port_id):undefined,e.connection_name].filter(Boolean)
if(e.connection_name&&!byId.has(e.connection_name)&&!connections.some(c=>c.name===e.connection_name)){
const prefix=connections.filter(c=>e.connection_name.startsWith(c.name+'_')).sort((a,b)=>b.name.length-a.name.length)[0];if(prefix)ids.push(prefix.name)
}
if(!ids.length) return electrical.find(`isolated:${e.pcb_via_id??e.pcb_smtpad_id??e.pcb_trace_id}`)
for(const id of ids)electrical.join(ids[0],id)
return electrical.find(ids[0])
}
// Resolve all identity aliases before freezing conductor net names.
for(const e of [...original,...newTraces])net(e)
const unsupported=original.filter(e=>['pcb_plated_hole','pcb_copperpour','pcb_cutout'].includes(e.type)).map(e=>`Unsupported original copper object ${e.type}`)
const shapes:Shape[]=[],errors:string[]=[...unsupported,...ownershipErrors],add=(s:Omit<Shape,'id'>)=>{const id=`shape${shapes.length}`;shapes.push({...s,id});if(s.join)physical.join(id,s.join)}
const traces=(rows:any[],fresh:boolean)=>{for(const t of rows){const n=net(t);for(let i=0;i<t.route.length;i++){const p=t.route[i];if(!Number.isFinite(Number(p.x))||!Number.isFinite(Number(p.y)))errors.push('Nonfinite copper coordinates');if(fresh&&p.route_type==='wire'&&!['top','inner2','inner4','inner6'].includes(p.layer))errors.push(`New signal on forbidden layer ${p.layer}`);if(fresh&&p.route_type==='wire'&&!(Number(p.width)>=.1016-1e-9))errors.push(`Undersize new trace ${t.pcb_trace_id}`);if(fresh&&p.route_type==='via'&&(!(Number(p.via_diameter)>=.4572-1e-9)||!(Number(p.via_hole_diameter)>=.254-1e-9)))errors.push(`Unqualified new via ${t.pcb_trace_id}`);if(p.route_type==='via'){const join=`via:${fresh}:${t.pcb_trace_id}:${i}`;for(const layer of layers)add({traceId:t.pcb_trace_id,net:n,layer,a:p,b:p,r:Number(p.via_diameter??.4572)/2,fresh,join})}else if(p.route_type==='wire'){const q=t.route[i-1];if(q?.route_type==='wire'&&q.layer===p.layer)add({traceId:t.pcb_trace_id,net:n,layer:p.layer,a:q,b:p,r:Math.max(q.width,p.width)/2,fresh})}else errors.push(`Unsupported route primitive ${p.route_type}`)}}}
traces(original.filter(e=>e.type==='pcb_trace'),false);traces(newTraces,true)
for(const v of original.filter(e=>e.type==='pcb_via'))for(const layer of v.layers)add({net:net(v),layer,a:v,b:v,r:v.outer_diameter/2,fresh:false,join:v.pcb_via_id})
for(const p of original.filter(e=>e.type==='pcb_smtpad')){
if(p.shape==='circle')add({net:net(p),layer:p.layer,a:p,b:p,r:p.radius,fresh:false,join:p.pcb_smtpad_id})
else if(['rect','rotated_rect','pill'].includes(p.shape)&&(!p.ccw_rotation||p.ccw_rotation%90===0)){const swap=p.ccw_rotation%180!==0&&p.ccw_rotation!==undefined,w=swap?p.height:p.width,h=swap?p.width:p.height;add({net:net(p),layer:p.layer,a:p,b:p,r:0,fresh:false,join:p.pcb_smtpad_id,rect:{minX:p.x-w/2,maxX:p.x+w/2,minY:p.y-h/2,maxY:p.y+h/2}})}
else errors.push(`Unsupported pad geometry ${p.pcb_smtpad_id}: ${p.shape}`)
}
for(const pour of original.filter(e=>e.type==='pcb_copper_pour')){
try{
if(!layers.includes(pour.layer)||!pour.source_net_id||!pour.pcb_copper_pour_id)throw Error('Pour needs known layer, source net and ID')
const polygon=parsePour(pour),a=polygon.outer[0]!
add({net:net(pour),layer:pour.layer,a,b:a,r:0,fresh:false,polygon,join:pour.pcb_copper_pour_id})
}catch(error){errors.push(`Unsupported original copper object pcb_copper_pour ${pour.pcb_copper_pour_id}: ${error instanceof Error?error.message:String(error)}`)}
}
const violations:any[]=[];let minClearanceMm=Infinity
const touching:Array<[number,number]>=[],conflicts:Array<{i:number;j:number;g:number}>=[]
const checkPair=(i:number,j:number)=>{
if(i>j)[i,j]=[j,i]
const a=shapes[i]!,b=shapes[j]!;if(a.layer!==b.layer)return
const g=gap(a,b)
if(a.net===b.net){if(g<=1e-7)touching.push([i,j])}
else if(a.fresh||b.fresh){minClearanceMm=Math.min(minClearanceMm,g);if(g<.1016-1e-8)conflicts.push({i,j,g})}
}
const boxes=shapes.map((s,i)=>({i,layer:s.layer,...s.polygon?.bounds??s.rect??{minX:Math.min(s.a.x,s.b.x)-s.r,maxX:Math.max(s.a.x,s.b.x)+s.r,minY:Math.min(s.a.y,s.b.y)-s.r,maxY:Math.max(s.a.y,s.b.y)+s.r}}))
if(options.pairSearch==='brute-force'||boxes.some(b=>![b.minX,b.maxX,b.minY,b.maxY].every(Number.isFinite))){
for(let i=0;i<shapes.length;i++)for(let j=i+1;j<shapes.length;j++)checkPair(i,j)
}else{
// Seed an exact upper bound for nearest foreign copper on each layer. The
// sweep can then discard distant AABBs without changing minClearanceMm.
// Include radii in the AABBs: centerline bounds miss wide tracks/via barrels.
const byLayer=new Map<string,typeof boxes>()
for(const box of boxes){const group=byLayer.get(box.layer)??[];group.push(box);byLayer.set(box.layer,group)}
for(const group of byLayer.values()){
const fresh=group.find(b=>shapes[b.i]!.fresh)
if(fresh){const other=group.find(b=>shapes[b.i]!.net!==shapes[fresh.i]!.net);if(other)minClearanceMm=Math.min(minClearanceMm,gap(shapes[fresh.i]!,shapes[other.i]!))}
group.sort((a,b)=>a.minX-b.minX||a.i-b.i)
}
for(const group of byLayer.values())for(let i=0;i<group.length;i++){
const a=group[i]!
for(let j=i+1;j<group.length;j++){
const b=group[j]!,radius=Math.max(.1016,Number.isFinite(minClearanceMm)?minClearanceMm:.1016)+1e-9
if(b.minX-a.maxX>radius)break
const dx=Math.max(0,b.minX-a.maxX,a.minX-b.maxX),dy=Math.max(0,b.minY-a.maxY,a.minY-b.maxY)
// AABB separation is a lower bound on actual positive copper gap.
// Overlapping boxes always reach the exact test, including negative gaps.
if(Math.hypot(dx,dy)>radius)continue
checkPair(a.i,b.i)
}
}
}
// Preserve the brute-force pair order: physical root identities and violation
// reports remain stable even though candidates are discovered spatially.
touching.sort((a,b)=>a[0]-b[0]||a[1]-b[1])
for(const [i,j]of touching)physical.join(shapes[i]!.id,shapes[j]!.id)
conflicts.sort((a,b)=>a.i-b.i||a.j-b.j)
for(const {i,j,g}of conflicts){const a=shapes[i]!,b=shapes[j]!;violations.push({a:a.id,b:b.id,aNet:a.net,bNet:b.net,layer:a.layer,gapMm:g,aShape:a,bShape:b})}
const connectivity=connections.map(c=>{
const n=electrical.find(c.name),points=c.pointsToConnect??[],terminalResults=points.map((p:any)=>{
const exit=byId.get(p.pointId),source=exit?.source_port_id??portSource(p.pointId),port=original.find(e=>e.type==='pcb_port'&&e.source_port_id===source),pad=port&&original.find(e=>e.type==='pcb_smtpad'&&e.pcb_port_id===port.pcb_port_id)
if(!pad)return {pointId:p.pointId,error:'No actual source pad resolved'}
const sh=shapes.find(s=>s.join===pad.pcb_smtpad_id&&s.net===n)
return sh?{pointId:p.pointId,padId:pad.pcb_smtpad_id,root:physical.find(sh.id)}:{pointId:p.pointId,error:'Pad electrical identity mismatch'}
});return {name:c.name,terminals:terminalResults,connected:terminalResults.length>=2&&terminalResults.every((p:any)=>p.root&&p.root===terminalResults[0]?.root)}
})
const angles=auditRouteAngles(newTraces)
const joinedBends=auditJoinedBendShapes(shapes,original.filter(v=>v.type==='pcb_via').flatMap(v=>v.layers.map((layer:string)=>({net:net(v),layer,p:v}))),options.includeFixedJoinedBends??false)
const valid=connections.length===expectedNetCount&&!errors.length&&!violations.length&&connectivity.every(c=>c.connected)&&angles.valid&&!joinedBends.length
return {...(options.reportSameNetContacts?{sameNetContacts:touching.filter(([i,j])=>shapes[i]!.fresh!==shapes[j]!.fresh).map(([i,j])=>({net:shapes[i]!.net,layer:shapes[i]!.layer,gapMm:gap(shapes[i]!,shapes[j]!),aShape:shapes[i],bShape:shapes[j]}))}:{}),valid,expectedNetCount,netCount:connections.length,shapeCount:shapes.length,minClearanceMm,violations,errors,connectivity,angles,joinedBends,scope:'New copper against all original copper; actual package pads connected per complete supplied connection set. Original-only clearance is independently validated.'}
}
if(import.meta.main){const [original,leads,host,merged,out]=process.argv.slice(2);if(!out)throw Error('Usage: original.json leads.json host-traces.json merged-input.json report.json');const read=(p:string)=>JSON.parse(readFileSync(p,'utf8'));const report=verifyDdrSystemCopper(read(original!),[...read(leads!),...read(host!)],read(merged!).connections);writeFileSync(out,JSON.stringify(report,null,2)+'\n');console.log(JSON.stringify({valid:report.valid,errors:report.errors.length,violations:report.violations.length,connected:report.connectivity.filter(c=>c.connected).length,total:report.netCount}));if(!report.valid)process.exitCode=1}