shibosoftwaredev/f1c100s-linux-nema17-stepper-controller
A four-layer NEMA 17 motor-controller PCB combining an Allwinner F1C100S Linux computer, USB‑PD/buck power system, W5500 Ethernet, STM32 real-time motion control, DRV8825 stepper driver, protection/temperature/current telemetry, USB interfaces, and local LED/buzzer diagnostics.
- Version
- 0.4.56
- License
- unset
- Stars
- 0
scripts/check-round12-regression.mjs
import fs from "node:fs"
import path from "node:path"
import crypto from "node:crypto"
import assert from "node:assert/strict"
const baseline = process.argv[2]
assert.ok(baseline, "Pass the restore-verified pre-fix board directory")
const read = p => JSON.parse(fs.readFileSync(p, "utf8"))
const hash = p => crypto.createHash("sha256").update(fs.readFileSync(p)).digest("hex")
const before = read(`${baseline}/dist/index/circuit.json`), after = read("dist/index/circuit.json")
const sha = hash("dist/index/circuit.json"), of = (c,t) => c.filter(e => e.type === t)
const normalize = value => {
if (Array.isArray(value)) return value.map(normalize)
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value)
.filter(([key]) => key !== "subcircuit_connectivity_map_key").map(([key,v]) => [key,normalize(v)]))
return value
}
for (const type of ["source_net","source_component_internal_connection",
"pcb_board","pcb_component","pcb_port","pcb_smtpad","pcb_plated_hole","pcb_hole",
"pcb_keepout","pcb_courtyard_rect","pcb_courtyard_outline","pcb_courtyard_circle",
"pcb_silkscreen_text","pcb_silkscreen_circle",...new Set(before.filter(e=>e.type.startsWith("cad_")).map(e=>e.type))])
assert.deepEqual(normalize(of(after,type)), normalize(of(before,type)), `unchanged ${type}`)
const oldParts = new Map(of(before,"source_component").map(e => [e.name,e]))
assert.equal(of(after,"source_component").length,oldParts.size)
for (const part of of(after,"source_component")) {
const old = oldParts.get(part.name)
assert.ok(old, `no added part ${part.name}`)
const expected = {...old}
if (part.name === "Q_PD_ENABLE") {
assert.equal(old.manufacturer_part_number,"BSS84-7-F")
expected.manufacturer_part_number = "BSS84_R1_00001"
expected.supplier_part_numbers = {jlcpcb:["C391434"]}
}
assert.deepEqual(normalize(part), normalize(expected), `only approved part metadata changes: ${part.name}`)
}
const oldTraces = of(before,"source_trace").filter(t=>t.name!=="BUCK_ENABLE")
const newTraces = of(after,"source_trace")
assert.equal(oldTraces.length,newTraces.length)
const remapId = id => {
const number = Number(id.split("_").at(-1))
assert.notEqual(number,104)
return `source_trace_${number > 104 ? number-1 : number}`
}
for (const [i,trace] of oldTraces.entries())
assert.deepEqual(normalize(newTraces[i]),normalize({...trace,source_trace_id:remapId(trace.source_trace_id)}),
`connection and routing constraints preserved: ${trace.name ?? trace.source_trace_id}`)
const oldPorts = of(before,"source_port"), newPorts = of(after,"source_port")
assert.equal(oldPorts.length,newPorts.length)
for(const [i,port] of oldPorts.entries()) {
const expected={...port}
if(port.source_port_id==="source_port_255") expected.do_not_connect=true
assert.deepEqual(normalize(newPorts[i]),normalize(expected),"only U_BUCK.EN gets an NC flag")
}
const oldReplay=read(`${baseline}/routing/complete-routes.json`)
const expectedReplay=oldReplay.filter(t=>t.source_trace_id!=="source_trace_104")
.map(t=>({...t,source_trace_id:remapId(t.source_trace_id)}))
assert.deepEqual(read("routing/complete-routes.json"),expectedReplay,
"only unsafe EN branch removed; every other explicit route byte retained, with trace IDs rebound")
const errors=after.filter(e=>e.type.endsWith("_error"))
assert.deepEqual(errors,[])
const warnings=after.filter(e=>e.type.endsWith("_warning"))
assert.equal(after.filter(e=>e.type==="schematic_element_outside_sheet_warning").length,0)
assert.ok(warnings.length<=before.filter(e=>e.type.endsWith("_warning")).length)
const warningKey=e=>`${e.type}:${(e.message??"").replace(/#\d+\b/g,"#ID").replace(/actual: [0-9.]+mm/g,"actual: WIDTH")}`
const remainingWarnings=before.filter(e=>e.type.endsWith("_warning"))
for(const warning of warnings) {
const index=remainingWarnings.findIndex(old=>warningKey(old)===warningKey(warning))
assert.ok(index>=0,`no new warning kind/source: ${warning.message}`)
const [old]=remainingWarnings.splice(index,1)
const actual=warning.message?.match(/actual: ([0-9.]+)mm/)
const oldActual=old.message?.match(/actual: ([0-9.]+)mm/)
if(actual&&oldActual)assert.ok(Number(actual[1])>=Number(oldActual[1])-1e-9,"no worse reported routing width")
}
const vias=of(after,"pcb_via")
assert.equal(vias.length,352)
assert.ok(vias.every(v=>v.hole_diameter===.3&&v.outer_diameter===.45&&
["top","inner1","inner2","bottom"].every(l=>v.layers.includes(l))&&v.layers.length===4))
const log = name => fs.readFileSync(`reports/round12-${name}.log`,"utf8")
assert.match(log("build"),/Circuits\s+1 passed/)
assert.match(log("shorts"),/No shorts detected/)
assert.doesNotMatch(log("style"),/<SchematicPlacementIssues>/)
assert.match(log("enable-tests"),/PASS: open EN/)
assert.match(log("board-tests"),/6 pass[\s\S]*0 fail/)
assert.match(log("module-tests"),/32 pass[\s\S]*0 fail/)
assert.match(log("motion-tests"),/PASS: signed coil conversion/)
assert.match(log("host-tests"),/Ran 6 tests[\s\S]*OK/)
assert.doesNotMatch(log("typecheck"),/error TS\d+|Error:|not found/)
assert.match(log("mechanical"),/0 cross-layer\/mechanical violations/)
const kelvin=JSON.parse(log("kelvin").trim())
assert.equal(kelvin.length,4)
assert.ok(kelvin.every(k=>k.connected&&k.joinsOnlyAtShuntLand))
assert.deepEqual(read("reports/round5-legend-audit.json").findings,[])
let unchangedSourceFiles=0
function unchanged(directory) {
for(const entry of fs.readdirSync(`${baseline}/${directory}`,{withFileTypes:true})) {
if(["node_modules","build","dist","reports",".git"].includes(entry.name))continue
const p=path.join(directory,entry.name)
if(entry.isDirectory())unchanged(p)
else if(/\.(tsx?|mjs|json|c|h|py|md)$/.test(p)||entry.name==="Makefile") {
assert.equal(hash(p),hash(`${baseline}/${p}`),`unchanged firmware/module source ${p}`)
unchangedSourceFiles++
}
}
}
unchanged("modules");unchanged("firmware")
fs.writeFileSync("reports/module-test-regression.json",JSON.stringify({
checkedAt:new Date().toISOString(),baselinePassed:32,baselineFailed:0,currentPassed:32,currentFailed:0,
addedFailures:0,unchangedModuleAndFirmwareSources:unchangedSourceFiles,
baselineCircuitSha256:hash(`${baseline}/dist/index/circuit.json`),
currentLogSha256:hash("reports/round12-module-tests.log")},null,2)+"\n")
for(const filename of ["buck-enable-audit.json","connectivity-audit.json","coil-all-copper-preflight-meta.json","solder-process-audit.json",
"drill-clearance-regression.json","additional-dfm-review.json","bom-validation.json","jlcpcb-stock-audit.json",
"round12-width-proof.json","round12-release-gates.json"])
assert.equal(read(`reports/${filename}`).circuitSha256,sha,`fresh evidence: ${filename}`)
const continuity=read("reports/connectivity-audit.json")
assert.equal(continuity.sourceFailures,0);assert.equal(continuity.disconnectedTerminalIslands,0)
assert.equal(continuity.emittedCopperOnly,true)
assert.equal(read("reports/coil-all-copper-preflight-meta.json").failureCount,0)
assert.equal(read("reports/coil-all-copper-preflight-meta.json").physicalCopperSha256,hash("routing/physical-copper.json"))
assert.equal(read("reports/drill-clearance-regression.json").currentFindings,0)
const widths=read("reports/round12-width-proof.json")
assert.equal(widths.vmBridgeMinimumMm,.8)
assert.equal(widths.vmBridgeOuterOnly,true)
assert.equal(widths.phaseBReturnMinimumMm,.3)
assert.equal(widths.reducedHalfMmVmSegmentsRemaining,0)
assert.deepEqual(read("reports/additional-dfm-review.json").findings,[])
assert.deepEqual(read("reports/bom-validation.json").findings,[])
const solder=read("reports/solder-process-audit.json")
assert.deepEqual(solder.ordinaryPadDrillIntersections,[])
assert.equal(solder.thermalPadDrillIntersections.length,16)
assert.equal(solder.approvedForAssembly,false)
const stock=read("reports/jlcpcb-stock-audit.json")
assert.deepEqual(stock.parts.filter(p=>p.status!=="in-stock").map(p=>p.code),["C32843"])
assert.equal(stock.parts.find(p=>p.code==="C391434").status,"in-stock")
const release=read("reports/round12-release-gates.json")
assert.equal(release.assemblyExportBlocked,true)
assert.equal(release.thermalProcessStillUnqualified,true)
assert.match(log("verify"),/all JLCPCB parts cover board quantity/)
const report={checkedAt:new Date().toISOString(),circuitSha256:sha,baselineCircuitSha256:hash(`${baseline}/dist/index/circuit.json`),
localChangesNotPushed:true,fabricationReady:false,
fixed:["TPS54302 EN isolated from 15 V VIN in both source and emitted copper; TI open-pin enable option used",
"Q_PD_ENABLE changed to stocked PANJIT C391434 with checked G/S/D and compatible retained SOT-23 lands",
"regulator capacitor sides, buck schematic, bypass grouping and factory wiring presentation improved",
"contradictory README replaced; adapter and unconfirmed rear screw fit disclosed; history retained",
"power audit source identities follow net names, not brittle render-order numbers"],
invariants:{sizeMm:42.3,layers:4,thicknessMm:1.6,mountingHoles:4,mountPatternMm:31,
placementsAndLandsUnchanged:true,cadAssemblyUnchanged:true,allNonEnCopperRoutesUnchanged:true,
throughVias:352,viaDrillMm:.3,viaPadMm:.45,firmwareAndModuleSourcesUnchanged:unchangedSourceFiles},
checks:{nativeErrors:0,nativeWarnings:warnings.length,baselineWarnings:153,localSchematicPlacementIssues:0,
newWarningKindsOrWorseWidths:false,
outsideSheetWarnings:0,shorts:"pass",emittedContinuity:"pass",independentCopper:"pass",drill:"pass",
ordinaryPadDrillIntersections:0,thermalPadRecords:16,kelvin:"four pass",powerWidths:"pass",
typecheck:"pass",boardTests:6,moduleTests:32,firmwareAndHost:"pass",bomCatalog:"pass",
fullVerify:"blocked on W5500 orderable stock; no gate relaxed",assemblyExport:"blocked; negative release-gate tests pass"},
remaining:["W5500 C32843 orderable-stock shortage; no unqualified network-chip substitution",
"thermal via/stencil/opposite-face assembly-process approval",
"actual motor rear endbell/bolt engagement/spacer/harness confirmation",
"controlled USB/Ethernet impedance and return paths; buck feedback-loop placement review",
"A-current output remains 86.72 mm: two bounded reroute candidates failed; neither installed",
"complete Linux image, electrical first-article qualification and current/thermal calibration",
"fresh website AI schematic analysis not run; local CLI and rendered sheets checked"],
stockIssues:stock.parts.filter(p=>p.status!=="in-stock"),
evidence:Object.fromEntries(["dist/index/circuit.json","routing/complete-routes.json","routing/physical-copper.json",
...["build","shorts","style","continuity","copper","drill","solder","kelvin","mechanical","dfm",
"width-proof","typecheck","board-tests","module-tests","motion-tests","host-tests","enable-tests",
"stock","bom","verify","gate-tests"].map(n=>`reports/round12-${n}.log`)].map(p=>[p,hash(p)]))}
fs.writeFileSync("reports/round12-fix-status.json",JSON.stringify(report,null,2)+"\n")
console.log(JSON.stringify({fixed:report.fixed,nativeErrors:0,nativeWarnings:warnings.length,
schematicIssues:0,noUnintendedPhysicalChanges:true,remaining:report.remaining,localChangesNotPushed:true,fabricationReady:false}))