imrishabh18/pedometer

This code defines and assembles a simple radio receiver hardware circuit using specific imported capacitors, inductors, RF connectors, and oscillator components with precise footprints and schematic attributes.

Version
1.1.3
License
unset
Stars
0

scripts/check-routing-preserved.ts

import { createHash } from "node:crypto";
import { readFileSync, writeFileSync } from "node:fs";

type Element = Record<string, any> & { type: string };

// Keep numerical values exact: source capacitances can be only 1.5e-12 F,
// so a fixed decimal rounding tolerance would hide meaningful BOM changes.
function stable(value: any): string {
  if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`;
  if (value && typeof value === "object") return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stable(value[key])}`).join(",")}}`;
  return JSON.stringify(value);
}

/** Static constraints are compared independently of generated traces, vias,
 * pour cutouts, IDs, and connectivity-cache labels. Ports are identified by
 * component reference and pin, so renumbered generated IDs cannot hide a
 * changed electrical connection or create a false difference. */
export function routingPreservationSnapshot(circuit: Element[]) {
  const ofType = (type: string) => circuit.filter(e => e.type === type);
  const ids = new Map<string, string>();
  for (const component of ofType("source_component")) ids.set(component.source_component_id, `component:${component.name}`);
  for (const port of ofType("source_port")) ids.set(port.source_port_id, `port:${ids.get(port.source_component_id)}:${port.pin_number ?? port.name}`);
  for (const net of ofType("source_net")) ids.set(net.source_net_id, `net:${net.name}`);
  for (const group of ofType("source_group")) {
    // Group membership itself is retained below; an unnamed root has a stable
    // identity even when its generated counter changes.
    const identity = `group:${group.name ?? (group.is_subcircuit ? "root" : group.source_group_id)}`;
    ids.set(group.source_group_id, identity);
    if (group.subcircuit_id) ids.set(group.subcircuit_id, `subcircuit:${identity}`);
  }
  for (const board of ofType("source_board")) ids.set(board.source_board_id, `board:${board.title ?? "main"}`);
  for (const board of ofType("pcb_board")) ids.set(board.pcb_board_id, `pcb:${ids.get(board.source_board_id) ?? "board:main"}`);
  for (const component of ofType("pcb_component")) ids.set(component.pcb_component_id, `pcb:${ids.get(component.source_component_id)}`);
  for (const port of ofType("pcb_port")) ids.set(port.pcb_port_id, `pcb:${ids.get(port.source_port_id)}`);

  const normalize = (value: any, key = ""): any => {
    if (typeof value === "string") return ids.get(value) ?? value;
    if (Array.isArray(value)) {
      const items = value.map(v => normalize(v));
      // Connection and layer collections are sets; polygon/path vertices are
      // deliberately ordered, since reordering them changes the geometry.
      return /_ids$/.test(key) || key === "layers" || key === "port_hints"
        ? items.sort((a, b) => stable(a).localeCompare(stable(b))) : items;
    }
    if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, normalize(v, k)]));
    return value;
  };
  const record = (element: Element) => normalize(Object.fromEntries(Object.entries(element).filter(([key]) =>
    key !== `${element.type}_id` && key !== "subcircuit_connectivity_map_key" && key !== "was_automatically_named"
  )));
  // Paste elements refer to pads; use the entire pad geometry and pin identity
  // as the stable reference, retaining every mask and land attribute.
  for (const pad of ofType("pcb_smtpad")) ids.set(pad.pcb_smtpad_id, `pad:${stable(record(pad))}`);

  const signatures = (elements: any[]) => elements.map(stable).sort();
  const sourceDefinition = circuit.filter(e => e.type.startsWith("source_") && !["source_trace", "source_project_metadata"].includes(e.type) && !/_(error|warning)$/.test(e.type));
  const physicalLayout = circuit.filter(e => e.type.startsWith("pcb_") && !["pcb_trace", "pcb_via", "pcb_copper_pour", "pcb_debug_object"].includes(e.type) && !/_(error|warning)$/.test(e.type));
  const netlist = ofType("source_trace").map(e => {
    const { display_name, ...definition } = record(e);
    return definition;
  });
  // Routing changes the islands and voids in pours, but cannot change which
  // net, physical layer, or mask policy a pour uses.
  const pourPolicies = ofType("pcb_copper_pour").map(e => {
    const { brep_shape, points, outline, ...policy } = record(e);
    return stable(policy);
  });
  const viaDimensions = ofType("pcb_via").map(e => stable(normalize({
    hole_diameter: e.hole_diameter, outer_diameter: e.outer_diameter,
    layers: e.layers,
  })));
  return {
    sourceDefinition: signatures(sourceDefinition.map(record)),
    netlist: signatures(netlist),
    physicalLayout: signatures(physicalLayout.map(record)),
    simulation: signatures(circuit.filter(e => e.type.startsWith("simulation_")).map(record)),
    pourPolicies: [...new Set(pourPolicies)].sort(),
    viaDimensions: [...new Set(viaDimensions)].sort(),
  };
}

export function compareRoutingPreservation(baseline: Element[], current: Element[]) {
  const before = routingPreservationSnapshot(baseline);
  const after = routingPreservationSnapshot(current);
  const differences = [];
  for (const category of Object.keys(before) as (keyof typeof before)[]) {
    if (stable(before[category]) === stable(after[category])) continue;
    const subtract = (a: string[], b: string[]) => {
      const remaining = [...b];
      return a.filter(item => { const index = remaining.indexOf(item); if (index < 0) return true; remaining.splice(index, 1); return false; });
    };
    const removed = subtract(before[category], after[category]);
    const added = subtract(after[category], before[category]);
    differences.push({ category, removedCount: removed.length, addedCount: added.length, removed: removed.slice(0, 3).map(v => JSON.parse(v)), added: added.slice(0, 3).map(v => JSON.parse(v)) });
  }
  return {
    result: differences.length ? "FAIL" : "PASS",
    compared: Object.fromEntries(Object.keys(before).map(key => [key, before[key as keyof typeof before].length])),
    differences,
  };
}

if (import.meta.main) {
  const baselinePath = process.argv[2] ?? "review/routing-baseline.json";
  const currentPath = process.argv[3] ?? "dist/index/circuit.json";
  const baselineBytes = readFileSync(baselinePath);
  const currentBytes = readFileSync(currentPath);
  const report = {
    baselinePath, currentPath,
    baselineSha256: createHash("sha256").update(baselineBytes).digest("hex"),
    currentSha256: createHash("sha256").update(currentBytes).digest("hex"),
    ...compareRoutingPreservation(JSON.parse(baselineBytes.toString()), JSON.parse(currentBytes.toString())),
  };
  if (process.argv[4]) writeFileSync(process.argv[4], JSON.stringify(report, null, 2) + "\n");
  console.log(JSON.stringify(report, null, 2));
  if (report.result !== "PASS") process.exit(1);
}