AnasSarkiz/usb-c-pd-brushed-motor
USB-C PD-powered motor controller with protected input, selectable 5/9/12 V buck supply, MCU qualification, PWM H-bridge drive, overvoltage sensing, regenerative energy-dump resistors, and safety interlocks.
- Version
- 0.0.1-a48-cloud-pipeline9-unrouted-c41d79a-305b1e7a
- License
- unset
- Stars
- 0
scripts/audit-schematic.ts
import { readFile, writeFile } from "node:fs/promises"
import { createHash } from "node:crypto"
import { any_circuit_element } from "circuit-json"
import { z } from "zod"
const auditConfiguration = z
.object({
revision: z.string(),
routingPhase: z.enum(["unrouted", "routed"]).default("unrouted"),
importManifestPath: z.string(),
reportPath: z.string(),
warningReviewPath: z
.string()
.default("evidence/main-warning-review-A7.json"),
})
.parse(
process.argv[2]
? JSON.parse(await readFile(process.argv[2], "utf8"))
: {
revision: "A19",
importManifestPath:
"evidence/active-supplier-inspection-manifest-A19.json",
reportPath: "evidence/schematic-connectivity-audit-A19.json",
},
)
const raw = JSON.parse(await readFile("dist/index/circuit.json", "utf8"))
const circuitJson = z.array(any_circuit_element).parse(raw)
const manifest = z
.array(
z.object({
ref: z.string(),
code: z.string(),
sheet: z.string(),
pins: z.record(z.string(), z.string()),
nc: z.array(z.string()),
}),
)
.parse(JSON.parse(await readFile("docs/design-manifest.json", "utf8")))
const issues: string[] = []
const sources = circuitJson.filter(
(e): e is Extract<typeof e, { type: "source_component" }> =>
e.type === "source_component",
)
if (sources.length !== manifest.length)
issues.push("Source component count differs from manifest")
if (new Set(manifest.map((p) => p.ref)).size !== manifest.length)
issues.push("Duplicate manifest reference")
for (const part of manifest) {
const source = sources.find((e) => e.name === part.ref)
if (!source) {
issues.push(`${part.ref}: missing source`)
continue
}
if (!source.supplier_part_numbers?.jlcpcb?.includes(part.code))
issues.push(`${part.ref}: supplier identity mismatch`)
const sheet = circuitJson.find(
(e) => e.type === "schematic_sheet" && e.name === part.sheet,
)
const schematic = circuitJson.find(
(e) =>
e.type === "schematic_component" &&
e.source_component_id === source.source_component_id,
)
if (
sheet?.type !== "schematic_sheet" ||
sheet.sheet_size !== "a4" ||
schematic?.type !== "schematic_component" ||
schematic.schematic_sheet_id !== sheet.schematic_sheet_id
)
issues.push(
`${part.ref}: expected A4 sheet ${part.sheet} membership missing`,
)
const ports = circuitJson.filter(
(e): e is Extract<typeof e, { type: "source_port" }> =>
e.type === "source_port" &&
e.source_component_id === source.source_component_id,
)
for (const [alias, expectedNet] of Object.entries(part.pins)) {
const matches = ports.filter(
(e) => e.name === alias || e.port_hints?.includes(alias),
)
if (matches.length !== 1) {
issues.push(
`${part.ref}.${alias}: expected one port, found ${matches.length}`,
)
continue
}
const port = matches[0]
const net = circuitJson.find(
(e) =>
e.type === "source_net" && e.name === expectedNet.replace(/^net\./, ""),
)
const traces = circuitJson.filter(
(e): e is Extract<typeof e, { type: "source_trace" }> =>
e.type === "source_trace" &&
e.connected_source_port_ids.includes(port.source_port_id),
)
if (
net?.type !== "source_net" ||
traces.length !== 1 ||
traces[0].connected_source_net_ids.length !== 1 ||
traces[0].connected_source_net_ids[0] !== net.source_net_id ||
traces[0].connected_source_port_ids.length !== 1
)
issues.push(
`${part.ref}.${alias}: wrong, missing or ambiguous ${expectedNet} connection`,
)
const schematicPort = circuitJson.find(
(e) =>
e.type === "schematic_port" && e.source_port_id === port.source_port_id,
)
if (
schematicPort?.type !== "schematic_port" ||
schematic?.type !== "schematic_component" ||
schematicPort.schematic_component_id !== schematic.schematic_component_id
)
issues.push(
`${part.ref}.${alias}: schematic port missing or wrong symbol`,
)
}
for (const alias of part.nc) {
const port = ports.find(
(e) => e.name === alias || e.port_hints?.includes(alias),
)
if (!port) issues.push(`${part.ref}.${alias}: no-connect pin missing`)
else if (
circuitJson.some(
(e) =>
e.type === "source_trace" &&
e.connected_source_port_ids.includes(port.source_port_id),
)
)
issues.push(`${part.ref}.${alias}: no-connect pin has wiring`)
}
}
// Routing interprets internal connection records as conductive links. Check
// them independently of the individual schematic trace declarations so a
// shared symbol alias cannot conceal a short between intended product nets.
const internalConnections = circuitJson.filter(
(element) => element.type === "source_component_internal_connection",
)
for (const internalConnection of internalConnections) {
const source = sources.find(
(source) =>
source.source_component_id === internalConnection.source_component_id,
)
const connectedNetIds = new Set(
circuitJson
.filter((element) => element.type === "source_trace")
.filter((trace) =>
trace.connected_source_port_ids.some((portId) =>
internalConnection.source_port_ids.includes(portId),
),
)
.flatMap((trace) => trace.connected_source_net_ids),
)
if (connectedNetIds.size > 1) {
const netNames = circuitJson
.filter((element) => element.type === "source_net")
.filter((net) => connectedNetIds.has(net.source_net_id))
.map((net) => net.name)
issues.push(
`${source?.name ?? "unnamed component"}: internal connection shorts distinct schematic nets ${netNames.join(", ")}`,
)
}
}
const pcbTraceCount = circuitJson.filter((e) => e.type === "pcb_trace").length
if (auditConfiguration.routingPhase === "unrouted" && pcbTraceCount !== 0)
issues.push("Copper routing was generated while routing is disabled")
if (auditConfiguration.routingPhase === "routed" && pcbTraceCount === 0)
issues.push("Routed connectivity audit requires actual PCB traces")
const diagnostics = circuitJson.filter(
(e) => e.type.includes("warning") || e.type.includes("error"),
)
const warningReview = z
.object({
reviewedAdvisories: z.array(
z.object({
ref: z.string(),
code: z.string(),
type: z.string(),
message: z.string(),
importSha256: z.string(),
wiredPins: z.record(z.string(), z.string()),
}),
),
unreviewedWarnings: z.array(z.unknown()),
})
.parse(
JSON.parse(await readFile(auditConfiguration.warningReviewPath, "utf8")),
)
const importManifest = z
.array(z.object({ code: z.string(), import_path: z.string() }))
.parse(
JSON.parse(await readFile(auditConfiguration.importManifestPath, "utf8")),
)
if (warningReview.unreviewedWarnings.length)
issues.push("Manual diagnostic review has unreviewed warnings")
for (const diagnostic of diagnostics) {
if (diagnostic.type.includes("error")) {
issues.push(`Generated error ${diagnostic.type}`)
continue
}
const warning = z
.object({ source_component_id: z.string(), message: z.string() })
.parse(diagnostic)
const source = sources.find(
(e) => e.source_component_id === warning.source_component_id,
)
const part = manifest.find((p) => p.ref === source?.name)
const imported = importManifest.find((p) => p.code === part?.code)
const sha = imported
? createHash("sha256")
.update(await readFile(imported.import_path))
.digest("hex")
: ""
const reviewed = warningReview.reviewedAdvisories.find(
(r) =>
r.ref === part?.ref &&
r.code === part?.code &&
r.type === diagnostic.type &&
r.message === warning.message &&
r.importSha256 === sha &&
JSON.stringify(r.wiredPins) === JSON.stringify(part?.pins),
)
if (!reviewed)
issues.push(
`${source?.name}: new or stale warning review for ${diagnostic.type}`,
)
}
const report = {
revision: auditConfiguration.revision,
artifactSha256: createHash("sha256")
.update(JSON.stringify(raw))
.digest("hex"),
componentCount: manifest.length,
supplierPartCount: new Set(manifest.map((p) => p.code)).size,
issues,
pcbTraceCount,
diagnostics,
metadataReviewStatus: `Raw diagnostics retained; exact source, message and wiring reviewed in ${auditConfiguration.warningReviewPath}`,
}
await writeFile(
auditConfiguration.reportPath,
JSON.stringify(report, null, 2) + "\n",
)
console.log(
JSON.stringify(
{
componentCount: report.componentCount,
supplierPartCount: report.supplierPartCount,
issues,
pcbTraceCount,
diagnostics: diagnostics.length,
},
null,
2,
),
)
if (issues.length) process.exitCode = 1