main
John Lauer Component record for mal216099702e3: files batch 2 8841b6d 1d ago
#!/usr/bin/env python3
"""Write the records of adom/mal216099702e3 from the files in the page tree: lod/manifest.json,
provenance/3d-model.json, provenance/symbol.json, provenance/files.json, models.json and verification.json.
Every hash is computed from the file as it sits in the tree. usage: records.py <page dir>"""
import sys, os, json, hashlib
page = sys.argv[1]
S = "mal216099702e3"; MPN = "MAL216099702E3"; TODAY = "2026-10-21"
p_ = lambda f: os.path.join(page, f)
def sha(f): return hashlib.sha256(open(p_(f), "rb").read()).hexdigest()
def blob(f):
    b = open(p_(f), "rb").read(); return hashlib.sha1(b"blob %d\0" % len(b) + b).hexdigest()
size = lambda f: os.path.getsize(p_(f))
P = json.load(open(p_("page.json"))); C = P["component"]
fpc = json.load(open(p_("provenance/footprint-check.json")))
ov = json.load(open(p_("provenance/overlay-proof.json")))
chk = json.load(open(p_(f"docs/proof/{S}-etched.check.json")))
sch = json.load(open(p_("provenance/tools/schlib-pins.json")))
BY = {"name": "Claude (AI agent) for John Lauer, automated checks, not yet reviewed by a person", "handle": "john"}

def glbstats(f):
    b = open(p_(f), "rb").read(); n = int.from_bytes(b[12:16], "little"); j = json.loads(b[20:20 + n])
    tris = sum(j["accessors"][pr["indices"]]["count"] // 3 for m in j["meshes"] for pr in m["primitives"])
    return {"triangles": tris, "primitives": sum(len(m["primitives"]) for m in j["meshes"]),
            "nodes": [x.get("name") for x in j["nodes"] if "mesh" in x], "extensions": j.get("extensionsUsed", []),
            "materials": sorted({m.get("name") for m in j.get("materials", [])})}
LV = [("high", f"{S}-etched.step", True, "the etched GLB itself: etched STEP through step2glb --compress none, physical materials (provenance/tools/materials_cap.py)"),
      ("medium", f"{S}-etched.step", True, "the etched STEP through step2glb with its default KHR_mesh_quantization; virtual mark kept"),
      ("low", "provenance/tools/build_cla1216.py --simple", False, "the builder's simple variant (no can neck, no edge rounds; cathode bar, terminals and dummy pins kept), step2glb quantized; mark omitted"),
      ("super-low", "provenance/tools/build_cla1216.py --simple", False, "the simple variant re-meshed by OCCT at 0.35 mm / 1.0 rad (provenance/tools/coarse_glb.py): a polygonal can on the plate, cathode bar and terminals kept; mark omitted")]
levels = []
for name, src, mark, how in LV:
    f = f"lod/{S}-{name}.glb"
    levels.append({"level": name, "path": f, "sha256": sha(f), "sizeBytes": size(f), **glbstats(f), "derivedFrom": src, "markingIncluded": mark,
                   "helperNodes": [], "derivation": how, "review": "candidate: inspected as cached images (depth-buffered render); not qualified in a streaming LOD viewer",
                   "preview": {"path": f"lod/{S}-{name}.png", "sha256": sha(f"lod/{S}-{name}.png"), "view": "iso, same camera on every level"}})
json.dump({"schema": "adom/component-lod@1", "mpn": MPN, "units": "metres (glTF)", "upAxis": "Z (seated on z = 0, centred on the footprint origin)",
           "status": "candidate", "purpose": "clean board-use models: no footprint pads, outlines, labels or silkscreen (those are only in the page hero)",
           "source": {"path": f"{S}-etched.step", "sha256": sha(f"{S}-etched.step")},
           "sourceToDisplay": "uniform scale 0.001 (mm to m); no rotation, no translation", "generator": "step2glb (service) + provenance/tools/materials_cap.py, coarse_glb.py; previews render_glb.py",
           "removedHelpers": [], "levels": levels,
           "hints": ["All four levels share pose and units; every level keeps the cathode bar so polarity stays visible."]},
          open(p_("lod/manifest.json"), "w"), indent=1)

m3d = {"schema": "adom/3d-model-provenance@1", "mpn": MPN, "date": TODAY,
 "classification": "Datasheet-built (method 3 of the wiki-component ladder): CadQuery model generated from Vishay doc 28405 Fig. 2 and Table 1 (case 1216), undimensioned features measured off Fig. 2, colours from the maker's product photo; produced by Claude (AI) for Adom. Not manufacturer CAD.",
 "manufacturer_step": {"tried": ["https://www.vishay.com/en/product/28405/ (CAD models served through the Ultra Librarian embedded portal, vendor.ultralibrarian.com/vishay/embedded)"],
                       "result": "the portal is a script page that needs a signed-in session to download; no browser session was used, so Vishay's model was not obtained (2026-10-21)", "status": "not reachable"},
 "kicad_library": "KiCad has no 12.5 mm CP_Elec model or footprint and no 4-pin (dummy pin) V-chip variant, so method 1 and 2 do not apply",
 "builder": "provenance/tools/build_cla1216.py (CadQuery 2.x, OpenCascade); the docstring lists every printed, chosen and measured value",
 "printed_vs_model": [
   {"dim": "L, W (base plate)", "printed": "12.9 max", "model": 12.8, "pass": True},
   {"dim": "H (total height)", "printed": "16.5 max (nominal case 16)", "model": 16.0, "pass": True},
   {"dim": "D (can)", "printed": "12.5", "model": 12.5, "pass": True},
   {"dim": "B (terminal width)", "printed": "1.3 max", "model": 1.2, "pass": True},
   {"dim": "S (dummy pin length)", "printed": "3.6", "model": 3.6, "pass": True},
   {"dim": "L1 (over the terminals)", "printed": "14.9 max; terminals min 0.4 / max 1.0 beyond the plate", "model": 14.3, "pass": True}],
 "measured_off_drawing": "plate thickness 2.2, anode-side bevels 2.6 x 2.6, cathode-side corner radius 0.6, can neck 1.4 at D 11.6, terminal inner end 2.7 from the centre, dummy pins 2.6 deep, cathode bar about 6.0 x 2.5 (drawn 5.0 x 2.3 to stay on the flat top); Fig. 2 at 32.2 px/mm (plate) and 31.2 px/mm (D) in a 600 dpi render",
 "not_from_drawings": "terminal thickness 0.3 and the 0.2 mm plate lift above the seating plane (Fig. 3 allows 0.3 coplanarity); the plate is a plain slab, while the product photo shows raised corner blocks around a lower middle section",
 "photo": "DigiKey product image 'CLA Series 12.50mm' (mm.digikey.com, fetched 2026-10-21): bare aluminium can, black rectangular cathode bar on the top, black base plate, tinned terminals",
 "frame": "Z up, millimetres, seated on z = 0, centred on the footprint origin; anode terminal toward -X over pad 1, cathode toward +X over pad 2, dummy pins at +-Y over the MP pads; binds with zero offset and rotation",
 "overlay": {"image": f"docs/proof/{S}-overlay.png", "record": "provenance/overlay-proof.json", "scale": ov},
 "files": {"plain": {"file": f"{S}.step", "sha256": sha(f"{S}.step")}, "etched": {"file": f"{S}-etched.step", "sha256": sha(f"{S}-etched.step")},
           "glb_plain": {"file": f"{S}.glb", "sha256": sha(f"{S}.glb")}, "glb_etched": {"file": f"{S}-etched.glb", "sha256": sha(f"{S}-etched.glb")}},
 "etch": {"tool": "adom-aiflow-etch --up z --face top --over-vendor-mark --keep-old-mark --lines 'MAL2160|99702E3' --along y --mark dark",
          "why": "the planner's default (two lines along X) ran across the cathode bar, which it reads as an existing marking; along Y the block sits beside the bar. Dark mark on the light aluminium can.",
          "gate": chk.get("status"), "warnings": ["text runs across the long axis (the top is round, both axes 11.3 mm)", "dark mark on a light body (aluminium can)"],
          "text": ["MAL2160", "99702E3"], "cap_height_mm": 0.97, "virtual": True,
          "note": "Virtual identification mark, not Vishay's marking (capacitance, voltage, date code, group code and cathode bar; doc 28405 p.1)."},
 "hero": {"file": f"{S}-hero.glb", "sha256": sha(f"{S}-hero.glb"), "generator": "adom/component-hero 0.1.2 scripts/hero_glb.py",
          "result": "pads 4, silk items 12, pin1 added dot (AI), labels 3; align OK (offset 0.0 mm)",
          "note": "labels: '+' at pad 1, '-' at pad 2, 'MP' once for the two mechanical pads; the generator added a dot beside pad 1 in addition to the footprint's '+'"}}
json.dump(m3d, open(p_("provenance/3d-model.json"), "w"), indent=1)

json.dump({"schema": "adom/symbol-provenance@1", "mpn": MPN, "date": TODAY,
           "source": "graphics of KiCad's Device:C_Polarized (kicad-symbols master, CC-BY-SA 4.0 with the library exception), written by provenance/tools/make_symbol.py",
           "grid_fix": "KiCad's C_Polarized has its pin ends at +-3.81 mm (on the 1.27 mm grid only); moved to +-5.08 mm with pin length 4.064 so both ends are on the 2.54 mm grid; plates unchanged",
           "pins": {"1": "+ (anode), passive", "2": "- (cathode), passive"},
           "names_and_numbers": "hidden on the symbol (pin_names hide, pin_numbers hide), the usual polarized-capacitor convention; the '+' graphic marks pin 1",
           "fusion_lbr": {"file": f"{S}.lbr", "sha256": sha(f"{S}.lbr"), "check": "2 pins typed pas, visible='pad' (adom-lbr hides names on a glyph symbol); pads 1, 2, MP and MP@2 (adom-lbr's @-suffix for the duplicate mechanical pad name); connects + -> 1, - -> 2; MP pads unconnected by design; lint PASS"},
           "altium": {"schlib_sha256": sha(f"altium/{MPN}.SchLib"), "pcblib_sha256": sha(f"altium/{MPN}.PcbLib"),
                      "check": "pin records read from the SchLib binary (schlib_pins.py): pin 1 '+' and pin 2 '-', both passive, names and designators hidden (as in KiCad), PcbLib with the etched STEP embedded", "pins": sch}},
          open(p_("provenance/symbol.json"), "w"), indent=1)

mark = {"text": "MAL2160 / 99702E3 (two lines)", "surface": "top, beside the cathode bar, reading along Y", "colour": "dark on aluminium", "virtual": True,
        "note": "virtual identification mark by adom-chip-laser; Vishay prints capacitance, voltage, date code and group code"}
def rec(path, fmt, title, slot, variant, use, marking, derived, unique, source, thumb=None):
    r = {"path": path, "format": fmt, "sha256": sha(path), "git_blob": blob(path), "bytes": size(path), "title": title, "status": "current",
         "slot": slot, "variant": variant, "role": "datasheet-built" if variant != "hero" else "derived", "use": use, "marking": marking, "colours": True,
         "derived_from": derived, "unique": unique, "source": source}
    if thumb: r["thumb"] = thumb
    return r
json.dump({"schema": "adom/models@1", "part": MPN, "updated": TODAY, "models": [
  rec(f"{S}-etched.step", "step", f"{MPN}, virtual MPN etch (library model)", "model_3d_step", "etched", ["board-cad"], mark, f"{S}.step",
      "The board-CAD model with the MPN on the can top (Adom.LaserEtch); bound by the KiCad footprint, the Fusion .lbr and the Altium PcbLib",
      "provenance/tools/build_cla1216.py + adom-chip-laser (service-occt etch, placed by adom-aiflow-etch)", f"docs/models/{S}-etched-iso.png"),
  rec(f"{S}.step", "step", f"{MPN}, unmarked (cathode bar only)", "model_3d_step_plain", "plain", ["board-cad", "archive"], None,
      "Vishay doc 28405 p.2 Fig. 2, Table 1 (case 1216)", "The same model without the virtual mark; the cathode bar is the part's own polarity mark", "provenance/tools/build_cla1216.py"),
  rec(f"{S}-hero.glb", "glb", f"{MPN} hero (page 3D viewer)", "model_3d", "hero", ["wiki-hero"], mark, f"{S}-etched.glb",
      "Adds the footprint's teal dashed pad outlines, the +, - and MP labels and the silkscreen under the part; for the page only, never for boards", "adom/component-hero hero_glb.py"),
  rec(f"{S}-etched.glb", "glb", f"{MPN}, virtual MPN etch", "model_3d_plain", "etched", ["render", "board-insertion"], mark, f"{S}-etched.step",
      "Clean GLB of the library model with physical materials (aluminium, black plastic, tin); what boards and digital twins should place", "step2glb + materials_cap.py", f"docs/models/{S}-etched-top.png"),
  rec(f"{S}.glb", "glb", f"{MPN}, unmarked", None, "plain", ["render"], None, f"{S}.step", "Unmarked GLB with physical materials", "step2glb + materials_cap.py", f"docs/models/{S}-glb-top.png")]},
  open(p_("models.json"), "w"), indent=1)

def ent(title, scope, checks, evidence, files, status="verified"):
    return {"date": TODAY, "by": BY, "status": status, "title": title, "scope": scope, "variants": ["*"],
            "checks": [{"text": t, "result": r} for t, r in checks], "evidence": [{"label": os.path.basename(e), "href": e} for e in evidence], "files": {f: sha(f) for f in files}}
json.dump({"schema": "adom/verification@1", "part": MPN,
 "variants": [{"mpn": MPN, "note": "100 uF, 80 V, case 1216, life code L1, taped on reel (200 per 380 mm reel; doc 28405 p.3, p.5)", "stocked": False}],
 "entries": [
  ent("Value, rating and case match the datasheet", ["symbol", "page"], [
      ("Doc 28405 Table 5 (p.5), UR 80 V row: 100 uF, case 12.5 x 12.5 x 16, IR 320 mA (100 kHz, 150 C), IL2 80 uA, tan d 0.12, Z 0.650 Ohm (20 C) / 5.20 Ohm (-40 C), life code L1, ordering code MAL2160 99702E3", "pass"),
      ("Symbol: 2 passive pins on the 2.54 mm grid, pin 1 '+', pin 2 '-'; Fusion and Altium libraries carry the same pins and the MPN / distributor fields", "pass")],
      [f"docs/figures/{S}-ds-p5.png", f"docs/figures/{S}-ds-p1.png"], [f"{S}.kicad_sym", f"{S}.lbr", f"altium/{MPN}.SchLib"]),
  ent("Footprint equals Vishay's recommended soldering pads", ["footprint"], [
      ("Pads equal doc 28405 Fig. 4 / Table 3, case 1216: main pads 6.3 x 2.5 mm with a 4.0 mm gap, mechanical pads 5.0 x 4.2 mm with a 5.6 mm gap (pattern turned 180 deg so pad 1 = anode is on the left)", "pass" if fpc["all_pads_match"] else "fail"),
      ("Every model contact (terminals and dummy pins, section at z = 0.01 mm) lies inside its pad", "pass" if fpc["model_contact"]["all_inside"] else "fail"),
      ("KiCad has no footprint for this case; no comparison possible", "note")],
      [f"docs/figures/{S}-ds-p3-fig4-table3.png", f"docs/proof/{S}-footprint-check.png"], [f"{S}.kicad_mod", f"altium/{MPN}.PcbLib"]),
  ent("Polarity agrees across datasheet, footprint, 3D model and symbol", ["footprint", "model", "symbol"], [
      ("Datasheet: cathode = black bar (or '-') on the case, anode = bevelled edges of the base plate (doc 28405 p.1, Marking); Fig. 2 (p.2) shows the bar and the bevels on opposite ends; Fig. 4 (p.3) draws the plate outline with its bevels at one end and does not label the pads; the pad at the bevelled end takes the anode terminal (Fig. 2)", "pass"),
      ("Footprint: pad 1 = anode on the left (-X); F.Fab and silkscreen outlines bevelled on the pad-1 side; '+' printed beside pad 1", "pass"),
      ("3D model: anode terminal over pad 1 and cathode terminal over pad 2 (contact section), bevelled plate corners at -X, cathode bar on the can top at +X", "pass" if (fpc["polarity"]["anode_terminal_over_pad"] == "1" and fpc["polarity"]["cathode_terminal_over_pad"] == "2") else "fail"),
      ("Symbol: pin 1 is '+' (anode), pin 2 '-' (cathode); pin n -> pad n in the Fusion connects and the Altium hub JSON", "pass"),
      ("Tape orientation (pick-and-place zero rotation) not checked: the 1216 blister-tape drawing in doc 28359 Fig. 2 is too small to read the polarity marks", "open")],
      [f"docs/figures/{S}-ds-p1.png", f"docs/figures/{S}-ds-p2-fig2-4pin.png", f"docs/figures/{S}-ds-p3-fig4-table3.png", f"docs/proof/{S}-hero-preview.png", f"docs/proof/{S}-overlay.png"],
      [f"{S}.kicad_mod", f"{S}-etched.step", f"{S}.kicad_sym"]),
  ent("3D model inside Vishay's drawing", ["model"], [
      ("Printed limits met: plate 12.8 (12.9 max), height 16.0 (16.5 max), D 12.5, terminal width 1.2 (1.3 max), dummy pin 3.6 (S), over the terminals 14.3 (14.9 max)", "pass"),
      ("Overlay on Fig. 2 top and bottom views (each calibrated on the drawn plate edges as 12.9 mm): terminals, dummy pins, bevels and cathode bar line up", "pass"),
      ("Undimensioned features measured off the drawing (plate thickness, bevels, neck) and colours from the product photo; plate simplified to a slab", "note")],
      [f"docs/proof/{S}-overlay.png", f"docs/figures/{S}-ds-p2.png"], [f"{S}.step", f"{S}-etched.step"]),
  ent("Still to do", ["*"], [("Not yet reviewed by a person", "open"), ("Vishay's own 3D model not compared (portal needs a session)", "open"),
      ("Tape polarity / pick-and-place rotation not confirmed", "open"), ("Libraries checked by parsing, not opened in KiCad, Fusion or Altium", "open")], [], [], status="open")]},
  open(p_("verification.json"), "w"), indent=1)
allf = sorted(os.path.relpath(os.path.join(d, f), page) for d, _, fs in os.walk(page) for f in fs if f != "page.json")
json.dump({"schema": "adom/files@1", "date": TODAY, "files": {f: sha(f) for f in allf if f != "provenance/files.json"}}, open(p_("provenance/files.json"), "w"), indent=1)
print("OK records")