ShiboSoftwareDev/linux-gameboy-advance

A four-layer Linux handheld game-console carrier PCB built around an F1C100S SoC, USB-C and 2×AA power, LCD/microSD interfaces, flash, audio amplifier/speaker, controls, service headers, and RGB status LED.

Version
1.0.124
License
unset
Stars
0

scripts/audit-jlc.mjs

import assert from "node:assert/strict"
import fs from "node:fs"

const delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))

const fetchWithRetry = async (url, options, attempts = 3) => {
  let lastError
  for (let attempt = 1; attempt <= attempts; attempt++) {
    try {
      const response = await fetch(url, {
        ...options,
        signal: AbortSignal.timeout(20_000),
      })
      if (response.ok || response.status < 500) return response
      lastError = new Error(`${url}: HTTP ${response.status}`)
    } catch (error) {
      lastError = error
    }
    if (attempt < attempts) await delay(250 * attempt)
  }
  throw lastError
}

const circuit = JSON.parse(fs.readFileSync("dist/index/circuit.json", "utf8"))
const sourceComponents = circuit.filter((record) => record.type === "source_component")
const sourceById = new Map(sourceComponents.map((component) => [component.source_component_id, component]))
const fitted = circuit
  .filter((record) => record.type === "pcb_component" && !record.do_not_place)
  .map((pcb) => sourceById.get(pcb.source_component_id))
  .filter((source) => source && source.ftype !== "simple_test_point")

const referencesByPart = new Map()
for (const component of fitted) {
  const parts = component.supplier_part_numbers?.jlcpcb ?? []
  assert.ok(parts.length > 0, `${component.name} has no JLCPCB part number`)
  for (const part of parts) {
    assert.match(part, /^C\d+$/, `${component.name}: invalid JLCPCB part number ${part}`)
    if (!referencesByPart.has(part)) referencesByPart.set(part, new Set())
    referencesByPart.get(part).add(component.name)
  }
}

const partNumbers = [...referencesByPart.keys()].sort((a, b) => Number(a.slice(1)) - Number(b.slice(1)))
const results = []
const batchSize = 8
for (let start = 0; start < partNumbers.length; start += batchSize) {
  const batch = partNumbers.slice(start, start + batchSize)
  results.push(...await Promise.all(batch.map(async (partNumber) => {
    const url = `https://jlcsearch.tscircuit.com/api/search?q=${partNumber}`
    let payload = { components: [] }
    try {
      const response = await fetchWithRetry(url, { headers: { accept: "application/json" } })
      if (response.ok) payload = await response.json()
    } catch {
      // Continue to the exact-SKU official inventory fallback below.  The
      // community mirror is useful but must not be a single point of failure.
    }
    const numericId = Number(partNumber.slice(1))
    const match = payload.components?.find((component) => component.lcsc === numericId)
    const detailUrl = `https://jlcpcb.com/partdetail/${partNumber}`
    let officialInStock = false
    let jlcpcbAssemblyListed = false
    let officialStock = null
    let officialManufacturerPartNumber = null
    let officialPackage = null
    if (!match) {
      // The community search mirror can temporarily omit valid parts.  In
      // that case use LCSC's exact-SKU inventory. JLCPCB and LCSC share the
      // C-number. The separate JLCPCB page is recorded when available, but
      // is not required for manual/PTH parts such as the volume control.
      const lcscUrl = `https://www.lcsc.com/product-detail/${partNumber}.html`
      const [detailResult, lcscResult] = await Promise.allSettled([
        fetchWithRetry(detailUrl, { headers: { accept: "text/html" } }),
        fetchWithRetry(lcscUrl, { headers: { accept: "text/html" } }),
      ])
      if (detailResult.status === "fulfilled" && detailResult.value.ok) {
        const detailHtml = await detailResult.value.text()
        jlcpcbAssemblyListed = detailHtml.includes(partNumber) &&
          (detailHtml.includes("SMT Assembly") || detailHtml.includes("PCB assembly"))
      }
      if (lcscResult.status === "fulfilled" && lcscResult.value.ok) {
        const lcscHtml = await lcscResult.value.text()
        const productJsonLd = [...lcscHtml.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)]
          .map((script) => {
            try { return JSON.parse(script[1]) } catch { return null }
          })
          .find((record) => record?.["@type"] === "Product" && record.sku === partNumber)
        officialStock = Number(productJsonLd?.offers?.inventoryLevel ?? 0)
        officialManufacturerPartNumber = productJsonLd?.mpn ?? null
        officialPackage = productJsonLd?.additionalProperty
          ?.find((property) => property.name === "Package")?.value ?? null
        officialInStock = productJsonLd?.offers?.availability === "http://schema.org/InStock" &&
          officialStock >= referencesByPart.get(partNumber).size
      }
    }
    const references = [...referencesByPart.get(partNumber)].sort()
    const requiredQuantity = references.length
    return {
      partNumber,
      references,
      requiredQuantity,
      found: Boolean(match) || officialInStock,
      manufacturerPartNumber: match?.mfr ?? officialManufacturerPartNumber,
      package: match?.package ?? officialPackage,
      stock: match?.stock ?? officialStock,
      sufficientStock: Boolean(match) ? (match?.stock ?? 0) >= requiredQuantity : officialInStock,
      jlcpcbAssemblyListed: Boolean(match) || jlcpcbAssemblyListed,
      availabilitySource: match ? "jlcsearch.tscircuit.com live index" :
        officialInStock ? "official LCSC live exact-SKU Product inventory" : "not found",
      basic: match?.is_basic ?? false,
      preferred: match?.is_preferred ?? false,
      queryUrl: url,
      detailUrl,
    }
  })))
}

const unavailable = results.filter((part) => !part.sufficientStock)
const report = {
  auditedAt: new Date().toISOString(),
  source: "jlcsearch.tscircuit.com live index with official JLCPCB/LCSC exact-SKU fallback",
  note: "Indexed stock is checked against this board's fitted quantity. If the mirror omits a SKU, the audit requires the exact LCSC Product JSON-LD to report InStock inventory sufficient for the board; a JLCPCB assembly-page match is recorded separately because manual/PTH parts need not have one. This is only a point-in-time availability check and not an inventory reservation. Recheck immediately before ordering.",
  fittedComponents: fitted.length,
  uniqueJlcpcbPartNumbers: results.length,
  availablePartNumbers: results.length - unavailable.length,
  unavailablePartNumbers: unavailable.map((part) => part.partNumber),
  parts: results,
}
fs.writeFileSync("jlc-availability.json", `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({
  auditedAt: report.auditedAt,
  fittedComponents: report.fittedComponents,
  uniqueJlcpcbPartNumbers: report.uniqueJlcpcbPartNumbers,
  availablePartNumbers: report.availablePartNumbers,
  unavailablePartNumbers: report.unavailablePartNumbers,
}, null, 2))
assert.deepEqual(unavailable, [], `unavailable JLCPCB parts: ${unavailable.map((part) => part.partNumber).join(", ")}`)