← Commit history
docs/figures/kemet-ds-p4-electrical.pngadded
⋯ 1 unchanged line ⋯
docs/figures/kemet-ds-p8-waterfall-1812.pngadded
⋯ 1 unchanged line ⋯
docs/hero.pngadded
⋯ 1 unchanged line ⋯
docs/models/step-plain.pngadded
⋯ 1 unchanged line ⋯
docs/verify/c1812c124j1gactu-check.jsonadded+51
@@ -0,0 +1,51 @@+{+ "step": "C1812C124J1GACTU.step",+ "bbox_mm": [+  -2.4,+  2.4,+  -1.75,+  1.75,+  0.0,+  1.55+ ],+ "checks": [+  {+   "dim": "L",+   "model_mm": 4.8,+   "datasheet_min": 4.2,+   "datasheet_max": 4.8,+   "pass": true+  },+  {+   "dim": "W",+   "model_mm": 3.5,+   "datasheet_min": 2.9000000000000004,+   "datasheet_max": 3.5,+   "pass": true+  },+  {+   "dim": "T/H",+   "model_mm": 1.55,+   "datasheet_min": 1.25,+   "datasheet_max": 1.5499999999999998,+   "pass": true+  },+  {+   "dim": "B (band width), top",+   "model_mm": 0.877,+   "datasheet_min": 0.25,+   "datasheet_max": 0.95,+   "pass": true+  },+  {+   "dim": "B (band width), bottom",+   "model_mm": 0.877,+   "datasheet_min": 0.25,+   "datasheet_max": 0.95,+   "pass": true+  }+ ],+ "z_up_seated_centred": true,+ "long_axis_x": true,+ "all_pass": true+}\ No newline at end of file
docs/verify/c1812c124j1gactu-overlay.pngadded
⋯ 1 unchanged line ⋯
docs/verify/hero-preview.pngadded
⋯ 1 unchanged line ⋯
land-pattern/README.mdadded+41
@@ -0,0 +1,41 @@+# Land pattern check: C1812C124J1GACTU++The footprint on this page is KiCad's standard library footprint `Capacitor_SMD:C_1812_4532Metric`, renamed to+`C_1812_4532Metric_C1812C124J1GACTU` so it can sit beside other parts in one library folder. The copper is unchanged; only the name and+the 3D model path (`${KIPRJMOD}/C1812C124J1GACTU.step`) differ from upstream. This page compares it with the land pattern KEMET+recommends for 1812 parts in datasheet C1003_C0G (2/20/2025), page 12, Table 3 (IPC-7351).++![KiCad pads (teal) over KEMET's density level B land (amber dashed), true scale](land-pattern.png)++## KEMET's three density levels for 1812 (mm)++| Level | C (centre to land centre) | Y (land length) | X (land width) | V1 x V2 (courtyard) |+|---|---|---|---|---|+| A, maximum protrusion | 2.15 | 1.60 | 3.60 | 6.90 x 4.60 |+| B, median (nominal) | 2.05 | 1.40 | 3.50 | 6.00 x 4.00 |+| C, minimum protrusion | 1.95 | 1.20 | 3.40 | 5.30 x 3.70 |++KEMET's drawing (p.12) gives C from the part centre to each land centre, so the gap between the lands is 2C - Y and the+overall length is 2C + Y. KEMET describes level B as "a robust solder attachment condition for reflow solder processes".++## KiCad pads against level B++| Dimension | KiCad C_1812_4532Metric (used) | KEMET density level B | Difference |+|---|---|---|---|+| gap between lands | 2.70 mm | 2.70 mm | +0.00 mm |+| land length | 1.40 mm | 1.40 mm | +0.00 mm |+| land width | 3.40 mm | 3.50 mm | -0.10 mm |+| overall length | 5.50 mm | 5.50 mm | +0.00 mm |++## Result++KiCad's footprint is IPC-7351 nominal, the same rule as KEMET's level B: land centres, land length, gap and overall length+are identical. The only difference is the land width: KiCad's pads are 3.40 mm wide, KEMET's level B lands+3.50 mm (0.10 mm narrower, 0.05 mm per side; level C uses 3.40 mm). The KiCad pads have rounded corners+(roundrect ratio 0.18); KEMET draws plain rectangles. KiCad's courtyard is 6.00 x 3.90 mm, KEMET's V1 x V2 6.00 x 4.00 mm.+Both cover the full printed body width (3.20 +/-0.30 mm). This page keeps the KiCad pads. A design that wants KEMET's+level B exactly should widen each pad to 3.50 mm.++KEMET lists the 1812 case as reflow only (datasheet p.3 and p.13).++The drawing and [land-check.json](land-check.json) come from `provenance/tools/landcheck.py`.
land-pattern/land-check.jsonadded+45
@@ -0,0 +1,45 @@+{+ "footprint": "C_1812_4532Metric_C1812C124J1GACTU.kicad_mod",+ "kicad_library_footprint": "C_1812_4532Metric",+ "kicad": {+  "gap": 2.7,+  "len": 1.4,+  "wid": 3.4,+  "overall": 5.5+ },+ "maker_reference": {+  "gap": 2.7,+  "len": 1.4,+  "wid": 3.5,+  "overall": 5.5+ },+ "maker_source": "KEMET C1003_C0G 2/20/2025 p.12, Table 3, 1812, density level B (median, nominal): C 2.05 (centre to pad centre), Y 1.40, X 3.50, V1 6.00, V2 4.00 mm",+ "maker_letters": "gap = 2C - Y, land length = Y, land width = X, overall = 2C + Y (KEMET's letters C, Y, X)",+ "rows": [+  {+   "dim": "gap between lands",+   "kicad_mm": 2.7,+   "maker_nominal": 2.7,+   "difference_mm": 0.0+  },+  {+   "dim": "land length",+   "kicad_mm": 1.4,+   "maker_nominal": 1.4,+   "difference_mm": 0.0+  },+  {+   "dim": "land width",+   "kicad_mm": 3.4,+   "maker_nominal": 3.5,+   "difference_mm": -0.1+  },+  {+   "dim": "overall length",+   "kicad_mm": 5.5,+   "maker_nominal": 5.5,+   "difference_mm": 0.0+  }+ ],+ "all_within": null+}\ No newline at end of file
land-pattern/land-pattern-icon.pngadded
⋯ 1 unchanged line ⋯
land-pattern/land-pattern.pngadded
⋯ 1 unchanged line ⋯
lod/high.glbadded
⋯ 1 unchanged line ⋯
lod/high.pngadded
⋯ 1 unchanged line ⋯
lod/low.glbadded
⋯ 1 unchanged line ⋯
lod/low.pngadded
⋯ 1 unchanged line ⋯
lod/manifest.jsonadded+178
@@ -0,0 +1,178 @@+{+ "schema": "adom/component-lod@1",+ "mpn": "C1812C124J1GACTU",+ "upAxis": "Z",+ "units": "metres",+ "status": "candidate (AI-checked, not human-reviewed)",+ "purpose": "clean board-use component models: no footprint pads, outlines, labels or silkscreen; no marking (KEMET supplies C0G parts unmarked)",+ "source": {+  "path": "C1812C124J1GACTU.step",+  "sha256": "88e36c2337e5f1e514a7c5ad920e2abc2b0f399373a5eb1023a2c2af80c528ed"+ },+ "sourceToDisplay": "uniform scale 0.001 (mm to m); no rotation, no translation; origin = footprint origin, seat z = 0",+ "generator": "lod.py (OCCT via cadquery-ocp)",+ "removedHelpers": [],+ "levels": [+  {+   "level": "high",+   "path": "lod/high.glb",+   "sha256": "bef7ba8cde5577a9e4c18ae18557c1bfebfe35c9b3fbf5c380de2fc3504ccb20",+   "sizeBytes": 1497944,+   "decodedBufferBytes": 1496040,+   "triangles": 19676,+   "primitives": 2,+   "bounds": {+    "min": [+     -0.0024,+     -0.00175,+     0.0+    ],+    "max": [+     0.0024,+     0.00175,+     0.00155+    ]+   },+   "approximation": {+    "method": "OCCT BRepMesh_IncrementalMesh",+    "linearDeflectionMm": 0.0005,+    "angularDeflectionRad": 0.1+   },+   "detailVisibility": {+    "virtualMpn": false,+    "note": "no marking: the datasheet specifies none"+   },+   "review": "Candidate: rendered preview checked by AI (silhouette, terminals, seat z=0); not human-reviewed",+   "preview": {+    "path": "lod/high.png",+    "sha256": "6b6d873316fc536ba705988edb8cc15c0d875351d45fb158101c0e624de6cc20",+    "sizeBytes": 10872,+    "width": 400,+    "height": 300,+    "view": "iso",+    "sourceLevelSha256": "bef7ba8cde5577a9e4c18ae18557c1bfebfe35c9b3fbf5c380de2fc3504ccb20"+   }+  },+  {+   "level": "medium",+   "path": "lod/medium.glb",+   "sha256": "909900100576c0fb2203fd429a28f45080d64983bfdfbb5f1999927ad87428a0",+   "sizeBytes": 177448,+   "decodedBufferBytes": 175560,+   "triangles": 2412,+   "primitives": 2,+   "bounds": {+    "min": [+     -0.0024,+     -0.00175,+     0.0+    ],+    "max": [+     0.0024,+     0.00175,+     0.00155+    ]+   },+   "approximation": {+    "method": "OCCT BRepMesh_IncrementalMesh",+    "linearDeflectionMm": 0.004,+    "angularDeflectionRad": 0.35+   },+   "detailVisibility": {+    "virtualMpn": false,+    "note": "no marking: the datasheet specifies none"+   },+   "review": "Candidate: rendered preview checked by AI (silhouette, terminals, seat z=0); not human-reviewed",+   "preview": {+    "path": "lod/medium.png",+    "sha256": "2b833b2f1a2532901bf91e3c56027ce097bf29cf0199127f88c315ac05a60954",+    "sizeBytes": 10735,+    "width": 400,+    "height": 300,+    "view": "iso",+    "sourceLevelSha256": "909900100576c0fb2203fd429a28f45080d64983bfdfbb5f1999927ad87428a0"+   }+  },+  {+   "level": "low",+   "path": "lod/low.glb",+   "sha256": "8f1ad4771992ec4f9a9f75476d5e37ee50729a5b284172effcb6610a08462776",+   "sizeBytes": 47552,+   "decodedBufferBytes": 45672,+   "triangles": 668,+   "primitives": 2,+   "bounds": {+    "min": [+     -0.0024,+     -0.00175,+     0.0+    ],+    "max": [+     0.0024,+     0.00175,+     0.00155+    ]+   },+   "approximation": {+    "method": "OCCT BRepMesh_IncrementalMesh",+    "linearDeflectionMm": 0.02,+    "angularDeflectionRad": 0.9+   },+   "detailVisibility": {+    "virtualMpn": false,+    "note": "no marking: the datasheet specifies none"+   },+   "review": "Candidate: rendered preview checked by AI (silhouette, terminals, seat z=0); not human-reviewed",+   "preview": {+    "path": "lod/low.png",+    "sha256": "e9b5214c286b495842543087e64b79329e34eda4b1d5a8ea843f039252dd0784",+    "sizeBytes": 10883,+    "width": 400,+    "height": 300,+    "view": "iso",+    "sourceLevelSha256": "8f1ad4771992ec4f9a9f75476d5e37ee50729a5b284172effcb6610a08462776"+   }+  },+  {+   "level": "super-low",+   "path": "lod/super-low.glb",+   "sha256": "bce035912fe7694cf06a5e4d1947efe8cd8a4863a376e2df1070d2febda0ecb0",+   "sizeBytes": 3800,+   "decodedBufferBytes": 1944,+   "triangles": 36,+   "primitives": 2,+   "bounds": {+    "min": [+     -0.0024,+     -0.00175,+     0.0+    ],+    "max": [+     0.0024,+     0.00175,+     0.00155+    ]+   },+   "approximation": {+    "method": "per-material bounding boxes split at the part centre (terminals, body, overcoat)"+   },+   "detailVisibility": {+    "virtualMpn": false,+    "note": "no marking: the datasheet specifies none"+   },+   "review": "Candidate: rendered preview checked by AI (silhouette, terminals, seat z=0); not human-reviewed",+   "preview": {+    "path": "lod/super-low.png",+    "sha256": "175038a12e92cc626a29228a20d9821f6cdff3393608b7fd475f0172efacaa8c",+    "sizeBytes": 7960,+    "width": 400,+    "height": 300,+    "view": "iso",+    "sourceLevelSha256": "bce035912fe7694cf06a5e4d1947efe8cd8a4863a376e2df1070d2febda0ecb0"+   }+  }+ ],+ "hints": [+  "All four levels share pose and units; super-low is boxes per material group, chosen for board overviews."+ ]+}\ No newline at end of file
lod/medium.glbadded
⋯ 1 unchanged line ⋯
lod/medium.pngadded
⋯ 1 unchanged line ⋯
lod/super-low.glbadded
⋯ 1 unchanged line ⋯
lod/super-low.pngadded
⋯ 1 unchanged line ⋯
models.jsonadded+168
@@ -0,0 +1,168 @@+{+ "schema": "adom/models@1",+ "part": "C1812C124J1GACTU",+ "models": [+  {+   "path": "C1812C124J1GACTU.step",+   "format": "step",+   "title": "Package model (STEP, KEMET, Z-up)",+   "status": "current",+   "slot": "step",+   "variant": "plain",+   "role": "manufacturer",+   "use": [+    "board-cad"+   ],+   "marking": null,+   "derived_from": "vendor/C1812C124J1GACTU-kemet-original.step",+   "unique": "The board CAD model: KEMET's package solid re-posed to Z-up, seated on z = 0 and centred; KEMET's CAD logo glyphs dropped (the real part is unmarked)",+   "source": "KEMET 3D model (STEP) for C1812C124J1GACTU, https://search.kemet.com/component-documentation/download/step/C1812C124J1GACTU.step",+   "sha256": "88e36c2337e5f1e514a7c5ad920e2abc2b0f399373a5eb1023a2c2af80c528ed",+   "git_blob": "e4e7f1300c735efbd84ad2ae8bb9547feb87e368",+   "bytes": 376434,+   "thumb": "docs/models/step-plain.png"+  },+  {+   "path": "vendor/C1812C124J1GACTU-kemet-original.step",+   "format": "step",+   "title": "KEMET original download (STEP)",+   "status": "current",+   "variant": "original",+   "role": "manufacturer",+   "use": [+    "archive"+   ],+   "marking": {+    "text": "KEMET",+    "surface": "top of the ceramic",+    "colour": "SolidWorks default lavender (no colour set by KEMET)"+   },+   "unique": "The file exactly as downloaded: Y-up, corner at the origin, with KEMET's logo as three 5 um solids on top",+   "source": "KEMET 3D model (STEP) for C1812C124J1GACTU, https://search.kemet.com/component-documentation/download/step/C1812C124J1GACTU.step",+   "sha256": "b5d45febdee8defce1c3da229b540ed0a72760aeaa02501c728f7f8143eb6de7",+   "git_blob": "00a103d438e2cdf3caa51e8badb8cde33033cf51",+   "bytes": 361304+  },+  {+   "path": "package-reference.glb",+   "format": "glb",+   "title": "Package model (GLB)",+   "status": "current",+   "slot": "model_3d_plain",+   "variant": "plain",+   "role": "derived",+   "use": [+    "render",+    "board-insertion"+   ],+   "marking": null,+   "derived_from": "C1812C124J1GACTU.step",+   "unique": "Clean board-use model for viewers (step2glb service), no footprint artwork",+   "source": "KEMET 3D model (STEP) for C1812C124J1GACTU, https://search.kemet.com/component-documentation/download/step/C1812C124J1GACTU.step",+   "sha256": "53804863334397774437d2712467f7d99cf6a28381b8ca78c498bbbfedcf5aa2",+   "git_blob": "b8ef29926a372264231c35943dea1ffd0f0285ac",+   "bytes": 57820+  },+  {+   "path": "c1812c124j1gactu-hero.glb",+   "format": "glb",+   "title": "Hero model (GLB)",+   "status": "current",+   "slot": "model_3d",+   "variant": "hero",+   "role": "derived",+   "use": [+    "wiki-hero"+   ],+   "marking": null,+   "derived_from": "package-reference.glb",+   "unique": "The page's 3D view: the package plus teal dashed pad outlines, silkscreen and pad-number labels from the footprint (adom-hero standard)",+   "source": "adom/component-hero hero_glb.py",+   "sha256": "fd98debd3272da2303ea2af1a7af804b750ed9887356b33cdcff92680a7d21e2",+   "git_blob": "188e365829ba914437a8e46a8dd77a557ae4b6a1",+   "bytes": 93916,+   "thumb": "docs/verify/hero-preview.png"+  },+  {+   "path": "lod/high.glb",+   "format": "glb",+   "title": "Board LOD, high",+   "status": "current",+   "variant": "lod",+   "role": "derived",+   "use": [+    "board-insertion",+    "render"+   ],+   "marking": null,+   "derived_from": "C1812C124J1GACTU.step",+   "unique": "high detail for board and scaffold viewers: 19676 triangles, 2 primitives",+   "thumb": "lod/high.png",+   "source": "KEMET 3D model (STEP) for C1812C124J1GACTU, https://search.kemet.com/component-documentation/download/step/C1812C124J1GACTU.step",+   "sha256": "bef7ba8cde5577a9e4c18ae18557c1bfebfe35c9b3fbf5c380de2fc3504ccb20",+   "git_blob": "f358797688ddf3159914ca2297f8380c4f5266af",+   "bytes": 1497944+  },+  {+   "path": "lod/medium.glb",+   "format": "glb",+   "title": "Board LOD, medium",+   "status": "current",+   "variant": "lod",+   "role": "derived",+   "use": [+    "board-insertion",+    "render"+   ],+   "marking": null,+   "derived_from": "C1812C124J1GACTU.step",+   "unique": "medium detail for board and scaffold viewers: 2412 triangles, 2 primitives",+   "thumb": "lod/medium.png",+   "source": "KEMET 3D model (STEP) for C1812C124J1GACTU, https://search.kemet.com/component-documentation/download/step/C1812C124J1GACTU.step",+   "sha256": "909900100576c0fb2203fd429a28f45080d64983bfdfbb5f1999927ad87428a0",+   "git_blob": "2138efa5614c279fcd74d2f66e3147ec0da208fe",+   "bytes": 177448+  },+  {+   "path": "lod/low.glb",+   "format": "glb",+   "title": "Board LOD, low",+   "status": "current",+   "variant": "lod",+   "role": "derived",+   "use": [+    "board-insertion",+    "render"+   ],+   "marking": null,+   "derived_from": "C1812C124J1GACTU.step",+   "unique": "low detail for board and scaffold viewers: 668 triangles, 2 primitives",+   "thumb": "lod/low.png",+   "source": "KEMET 3D model (STEP) for C1812C124J1GACTU, https://search.kemet.com/component-documentation/download/step/C1812C124J1GACTU.step",+   "sha256": "8f1ad4771992ec4f9a9f75476d5e37ee50729a5b284172effcb6610a08462776",+   "git_blob": "8db1e6c840de09cfb4998b54acc9b87452bb2f0e",+   "bytes": 47552+  },+  {+   "path": "lod/super-low.glb",+   "format": "glb",+   "title": "Board LOD, super-low",+   "status": "current",+   "variant": "lod",+   "role": "derived",+   "use": [+    "board-insertion",+    "render"+   ],+   "marking": null,+   "derived_from": "C1812C124J1GACTU.step",+   "unique": "super-low detail for board and scaffold viewers: 36 triangles, 2 primitives",+   "thumb": "lod/super-low.png",+   "source": "KEMET 3D model (STEP) for C1812C124J1GACTU, https://search.kemet.com/component-documentation/download/step/C1812C124J1GACTU.step",+   "sha256": "bce035912fe7694cf06a5e4d1947efe8cd8a4863a376e2df1070d2febda0ecb0",+   "git_blob": "0f7896c1711e2f5c678e3d3065ce779c49c8c3b1",+   "bytes": 3800+  }+ ],+ "updated": "2026-10-21"+}\ No newline at end of file
package-reference.glbadded
⋯ 1 unchanged line ⋯
package.jsonadded+10
@@ -0,0 +1,10 @@+{+ "name": "c1812c124j1gactu",+ "title": "C1812C124J1GACTU - KEMET",+ "version": "0.1.0",+ "description": "MLCC 0.12 uF +/-5 %, 100 V, C0G/NP0, 1812 (4532 metric)",+ "type": "hardware",+ "kind": "component",+ "license": "CC-BY-4.0",+ "files": []+}\ No newline at end of file
provenance.jsonadded+460
@@ -0,0 +1,460 @@+{+ "manufacturer_reference": "https://yageogroup.com/en/us/capacitors/ceramic/mlccs-smd/C1812C124J1GACTU.html",+ "datasheet": {+  "file": "docs/KEMET-C1003-C0G-2025-02-20.pdf",+  "sha256": "02d179914aeb9585eb2229ba8e18ef9d6b01c77c056de2af295d6950a2a5cc0d",+  "url": "https://content.kemet.com/datasheets/KEM_C1003_C0G_SMD.pdf",+  "same_bytes_at": "https://yageogroup.com/content/datasheet/asset/file/KEM_C1003_C0G_SMD",+  "title": "KEMET, Surface Mount Multilayer Ceramic Chip Capacitors (SMD MLCCs), C0G Dielectric, 10 to 250 VDC (Commercial Grade), C1003_C0G, 2/20/2025 (22 pages)",+  "revision": "C1003_C0G 2/20/2025",+  "checked": "2026-10-21"+ },+ "specs_read_from_datasheet": {+  "capacitance": "0.12 uF +/-5 % (124, J), p.1",+  "rated_voltage": "100 V (code 1), p.1",+  "dielectric": "C0G, p.1",+  "tcc": "+/-30 ppm/C, -55 to +125 C, p.1 and p.4",+  "voltage": "no capacitance change with applied rated DC voltage, p.2",+  "aging": "0 %, p.4",+  "df": "0.1 %, p.4",+  "ir": "1,000 MOhm uF or 100 GOhm, p.4",+  "dwv": "250 % rated, p.4",+  "dimensions_mm": {+   "L": "4.50 +/-0.30",+   "W": "3.20 +/-0.30",+   "T": "1.40 +/-0.15 (GH, p.8 and p.10)",+   "B": "0.60 +/-0.35",+   "S_min": "2.30"+  },+  "packaging": "TU: 7 inch reel, unmarked, 1,000 pcs plastic, 12 mm tape, 8 mm pitch (p.2, p.10, p.16)",+  "marking": "C0G supplied unmarked only, p.15",+  "soldering": "reflow only (1812), p.3 and p.13"+ },+ "symbol_source": "Original, written for this page (standard capacitor graphic, non-polar, pin ends on the 2.54 mm grid). CC-BY-4.0.",+ "footprint_source": "KiCad standard library Capacitor_SMD:C_1812_4532Metric (CC-BY-SA 4.0 with the KiCad library exception); renamed C_1812_4532Metric_C1812C124J1GACTU, model path ${KIPRJMOD}/C1812C124J1GACTU.step; copper unchanged",+ "footprint_upstream_url": "https://gitlab.com/kicad/libraries/kicad-footprints/-/raw/master/Capacitor_SMD.pretty/C_1812_4532Metric.kicad_mod",+ "footprint_upstream_sha256": "55049cf1f23070ef96b5a248163237009cf6f89b4690a42b422ce69a5832911d",+ "manufacturer_step": {+  "found": true,+  "url": "https://search.kemet.com/component-documentation/download/step/C1812C124J1GACTU.step",+  "also_served_at": "https://connect.kemet.com:7667/gateway/IntelliData-ComponentDocumentation/1.0/download/step/C1812C124J1GACTU (same bytes)",+  "method": "plain HTTP GET, no browser, no session",+  "retrieved": "2026-10-21",+  "file": "vendor/C1812C124J1GACTU-kemet-original.step",+  "sha256": "b5d45febdee8defce1c3da229b540ed0a72760aeaa02501c728f7f8143eb6de7",+  "header_name": "CCS_CCSTD_1812_L4.8W3.5T1.55B0.6.STEP",+  "authoring": "SolidWorks 2021, 2024-09-06",+  "units": "mm",+  "up_axis": "+Y",+  "repose": "provenance/C1812C124J1GACTU.step.json"+ },+ "board_step": {+  "file": "C1812C124J1GACTU.step",+  "sha256": "88e36c2337e5f1e514a7c5ad920e2abc2b0f399373a5eb1023a2c2af80c528ed",+  "transform": [+   [+    1.0,+    0.0,+    0.0,+    -2.4+   ],+   [+    0.0,+    0.0,+    -1.0,+    -1.75+   ],+   [+    0.0,+    1.0,+    0.0,+    0.0+   ]+  ],+  "dropped": {+   "what": "KEMET logo glyph solids on the ceramic top (SolidWorks default lavender, no own colour); the real C0G part is unmarked (C1003_C0G p.15)",+   "solids": [+    {+     "faces": 25,+     "volume_mm3": 0.0019,+     "bbox_mm": [+      2.0775,+      1.4772,+      -2.1768,+      2.592,+      1.4822,+      -0.99+     ],+     "thickness_mm": 0.005+    },+    {+     "faces": 19,+     "volume_mm3": 0.00124,+     "bbox_mm": [+      2.0775,+      1.4772,+      -3.0648,+      2.592,+      1.4822,+      -2.0908+     ],+     "thickness_mm": 0.005+    },+    {+     "faces": 13,+     "volume_mm3": 0.00079,+     "bbox_mm": [+      2.0775,+      1.4772,+      -1.057,+      2.592,+      1.4822,+      -0.4375+     ],+     "thickness_mm": 0.005+    }+   ]+  },+  "kept": {+   "solid": "package (terminals and ceramic)",+   "faces": 72,+   "volume_mm3": 23.90241,+   "volume_after_mm3": 23.90241,+   "face_colours_srgb": [+    [+     0.376,+     0.231,+     0.0+    ],+    [+     0.552,+     0.552,+     0.552+    ]+   ]+  }+ },+ "geometry_status": "Manufacturer-issued CAD for this part number, every printed dimension checked for thickness code GH (docs/verify).",+ "marking": {+  "real_marking": null,+  "statement": "KEMET supplies C0G devices unmarked only (datasheet p.15), and the ordering code TU means 7 inch reel, unmarked (p.2)",+  "source": "KEMET C1003_C0G 2/20/2025 p.2 and p.15"+ },+ "frame": {+  "units": "mm (STEP), metres (GLB)",+  "up_axis": "Z",+  "seat_z": 0,+  "origin": "footprint origin"+ },+ "glb": {+  "tool": "step2glb convert (service)",+  "source": "C1812C124J1GACTU.step",+  "exit": 0+ },+ "hero_image": {+  "tool": "step2glb thumbnail --width 1600 --height 1000 (iso, 16:10)",+  "source": "C1812C124J1GACTU.step",+  "path": "docs/hero.png"+ },+ "hero_glb": {+  "file": "c1812c124j1gactu-hero.glb",+  "tool": "adom/component-hero skills/component-hero/scripts/hero_glb.py",+  "tool_sha256": "429e437a3f2e5f1dc3544fe65843ea4e019637f0a6163060cda2ae210f3af7bc",+  "inputs": [+   "package-reference.glb",+   "C_1812_4532Metric_C1812C124J1GACTU.kicad_mod",+   "page.json"+  ],+  "result": "OK c1812c124j1gactu-hero.glb: pads 2, silk items 2, pin1 none, labels 2; align OK (offset 0.0 mm)",+  "sha256": "fd98debd3272da2303ea2af1a7af804b750ed9887356b33cdcff92680a7d21e2"+ },+ "fusion_lbr": {+  "file": "C1812C124J1GACTU.lbr",+  "tool": "adom-lbr 2.47.7",+  "command": "adom-lbr generate --sym C1812C124J1GACTU.kicad_sym --fp C_1812_4532Metric_C1812C124J1GACTU.kicad_mod --part-metadata page.json -o C1812C124J1GACTU.lbr",+  "lint": {+   "passed": true,+   "errors": 0,+   "warnings": 0+  },+  "sha256": "8ab0ff6c67078aa179707b321443019cc088c6da010378e7dbff10433302ef1d"+ },+ "altium": {+  "files": [+   "altium/C1812C124J1GACTU.SchLib",+   "altium/C1812C124J1GACTU.PcbLib",+   "C1812C124J1GACTU.adom-lbr.json"+  ],+  "commands": [+   "adom-lbr import-kicad --sym C1812C124J1GACTU.kicad_sym --fp C_1812_4532Metric_C1812C124J1GACTU.kicad_mod --mpn C1812C124J1GACTU --out C1812C124J1GACTU.adom-lbr.json",+   "adom-lbr assemble-altium --no-dedupe --out altium/C1812C124J1GACTU C1812C124J1GACTU.adom-lbr.json"+  ],+  "report": {+   "_hint": "components are named by MPN; identical land patterns share one footprint. skipped[] is the complete drop list, never silent. lostFeatures[] names what the Altium lane could not carry (warn and ship); under --strict those parts move to refused[] and skipped[], the rest still assemble, status is partial and the exit code 2 while the files written stay valid.",+   "components": 1,+   "deduped": [],+   "footprints": 1,+   "lostFeatures": [],+   "models": {+    "embedded": 1,+    "missing": []+   },+   "pcblib": "altium/C1812C124J1GACTU.PcbLib",+   "refused": [],+   "renamed": [],+   "schlib": "altium/C1812C124J1GACTU.SchLib",+   "skipped": [],+   "status": "ok"+  },+  "hints": [+   "HINT: identity complete (MPN, Manufacturer, Datasheet, Description, Mouser, DigiKey, LCSC, Adom Wiki) - they become Altium parameters + Supplier links and EAGLE attributes."+  ],+  "sha256": {+   "altium/C1812C124J1GACTU.SchLib": "b84eb045fe23f2260ddda06777a2bd7a494691ab4be41a466bf73a9dc526b79f",+   "altium/C1812C124J1GACTU.PcbLib": "8fbecca79504564d582770b912d34fc9c62341bbd948d94df8d51f9174d0a356",+   "C1812C124J1GACTU.adom-lbr.json": "febc9b75092f87c62b3ac30d9e63593a14fecd36ffb4ecc7fd02b61e3575725d"+  }+ },+ "document_figures": [+  {+   "file": "docs/figures/kemet-ds-p1-overview-ordering.png",+   "document": "KEMET, Surface Mount Multilayer Ceramic Chip Capacitors (SMD MLCCs), C0G Dielectric, 10 to 250 VDC (Commercial Grade), C1003_C0G, 2/20/2025 (22 pages)",+   "page": 1,+   "crop_px_at_150dpi": [+    60,+    180,+    1215,+    1515+   ],+   "caption": "p.1 overview (no change in capacitance with time and voltage, +/-30 ppm/C) and ordering information (C 1812 C 124 J 1 G A C TU)",+   "sha256": "b24fcc4cfcf3ee41c7a2a3fcc681658b12e9bec39249a544b4d5e95967b935f0"+  },+  {+   "file": "docs/figures/kemet-ds-p2-benefits-packaging.png",+   "document": "KEMET, Surface Mount Multilayer Ceramic Chip Capacitors (SMD MLCCs), C0G Dielectric, 10 to 250 VDC (Commercial Grade), C1003_C0G, 2/20/2025 (22 pages)",+   "page": 2,+   "crop_px_at_150dpi": [+    60,+    150,+    1215,+    810+   ],+   "caption": "p.2 packaging C-spec (TU = 7 inch reel, unmarked) and benefits (no capacitance change with applied rated DC voltage, no decay with time)",+   "sha256": "66fc63e1866b22ed7f1d358d9a1c40f1bbc5d8ad848afd1050926927df1b4689"+  },+  {+   "file": "docs/figures/kemet-ds-p3-dimensions.png",+   "document": "KEMET, Surface Mount Multilayer Ceramic Chip Capacitors (SMD MLCCs), C0G Dielectric, 10 to 250 VDC (Commercial Grade), C1003_C0G, 2/20/2025 (22 pages)",+   "page": 3,+   "crop_px_at_150dpi": [+    60,+    150,+    1215,+    1185+   ],+   "caption": "p.3 dimensions: 1812 L 4.50 +/-0.30, W 3.20 +/-0.30, B 0.60 +/-0.35, S 2.30 min (mm)",+   "sha256": "4ca075141bbbf7977223b2d8b9aa1f377722b5d1fe7a1bd77ca96d311743b1d9"+  },+  {+   "file": "docs/figures/kemet-ds-p4-electrical.png",+   "document": "KEMET, Surface Mount Multilayer Ceramic Chip Capacitors (SMD MLCCs), C0G Dielectric, 10 to 250 VDC (Commercial Grade), C1003_C0G, 2/20/2025 (22 pages)",+   "page": 4,+   "crop_px_at_150dpi": [+    60,+    150,+    1215,+    1035+   ],+   "caption": "p.4 electrical parameters: -55 to +125 C, +/-30 ppm/C, 0 % aging, DWV 250 %, DF 0.1 %, IR",+   "sha256": "bf8972f6aa1ad36e02fd95f6c1e77fe98055ead639904713c4e03a46227c3f7b"+  },+  {+   "file": "docs/figures/kemet-ds-p8-waterfall-1812.png",+   "document": "KEMET, Surface Mount Multilayer Ceramic Chip Capacitors (SMD MLCCs), C0G Dielectric, 10 to 250 VDC (Commercial Grade), C1003_C0G, 2/20/2025 (22 pages)",+   "page": 8,+   "crop_px_at_150dpi": [+    60,+    150,+    1215,+    1080+   ],+   "caption": "p.8 Table 1B: 0.12 uF (124) in C1812C at 100 V (voltage code 1) has chip thickness code GH",+   "sha256": "b558affcc15fd4dc980a348155b4f2217d6dc4a26d8151121f1a4e0b156a9b39"+  },+  {+   "file": "docs/figures/kemet-ds-p10-thickness.png",+   "document": "KEMET, Surface Mount Multilayer Ceramic Chip Capacitors (SMD MLCCs), C0G Dielectric, 10 to 250 VDC (Commercial Grade), C1003_C0G, 2/20/2025 (22 pages)",+   "page": 10,+   "crop_px_at_150dpi": [+    60,+    150,+    915,+    1440+   ],+   "caption": "p.10 Table 2A: thickness code GH = 1.40 +/-0.15 mm; 1,000 pcs per 7 inch plastic reel",+   "sha256": "7211050566dcb2acafd592c9491b533f7c6c894bf6fbacecd2631484d3bd25e3"+  },+  {+   "file": "docs/figures/kemet-ds-p12-land-pattern.png",+   "document": "KEMET, Surface Mount Multilayer Ceramic Chip Capacitors (SMD MLCCs), C0G Dielectric, 10 to 250 VDC (Commercial Grade), C1003_C0G, 2/20/2025 (22 pages)",+   "page": 12,+   "crop_px_at_150dpi": [+    60,+    150,+    1215,+    1350+   ],+   "caption": "p.12 Table 3: IPC-7351 land pattern for 1812 at density levels A, B and C",+   "sha256": "c6245d9166539a8b215114c2364e169762830081943bd3258e8ee1974288b821"+  },+  {+   "file": "docs/figures/kemet-ds-p13-soldering.png",+   "document": "KEMET, Surface Mount Multilayer Ceramic Chip Capacitors (SMD MLCCs), C0G Dielectric, 10 to 250 VDC (Commercial Grade), C1003_C0G, 2/20/2025 (22 pages)",+   "page": 13,+   "crop_px_at_150dpi": [+    60,+    150,+    1215,+    1095+   ],+   "caption": "p.13 soldering: reflow only for 1812, profile for 100 % matte Sn (260 C peak)",+   "sha256": "3c337c99956fb0ba4e6ed1d7acaa5671618b2b59d1b793764926fc69f6442db7"+  },+  {+   "file": "docs/figures/kemet-ds-p15-marking.png",+   "document": "KEMET, Surface Mount Multilayer Ceramic Chip Capacitors (SMD MLCCs), C0G Dielectric, 10 to 250 VDC (Commercial Grade), C1003_C0G, 2/20/2025 (22 pages)",+   "page": 15,+   "crop_px_at_150dpi": [+    60,+    150,+    1215,+    1170+   ],+   "caption": "p.15 construction and marking: C0G devices are supplied unmarked only",+   "sha256": "0af48889b2a288a2fe612c5f01403017b43105619b7e2acc11406be113083466"+  }+ ],+ "datasheet_check": {+  "overlay": "docs/verify/c1812c124j1gactu-overlay.png",+  "numbers": "docs/verify/c1812c124j1gactu-check.json",+  "result": "PASS"+ },+ "land_pattern_check": {+  "readme": "land-pattern/README.md",+  "numbers": "land-pattern/land-check.json",+  "source": "KEMET C1003_C0G 2/20/2025 p.12, Table 3, 1812, density level B (median, nominal): C 2.05 (centre to pad centre), Y 1.40, X 3.50, V1 6.00, V2 4.00 mm",+  "levels": {+   "A": {+    "C": 2.15,+    "Y": 1.6,+    "X": 3.6,+    "V1": 6.9,+    "V2": 4.6+   },+   "B": {+    "C": 2.05,+    "Y": 1.4,+    "X": 3.5,+    "V1": 6.0,+    "V2": 4.0+   },+   "C": {+    "C": 1.95,+    "Y": 1.2,+    "X": 3.4,+    "V1": 5.3,+    "V2": 3.7+   }+  },+  "result": "KiCad matches KEMET level B except land width (3.40 vs 3.50 mm); KiCad kept"+ },+ "distributors": {+  "tool": "adom-parts-search search C1812C124J1GACTU",+  "date": "2026-10-21",+  "mouser": {+   "part": "80-C1812C124J1G",+   "url": "https://www.mouser.com/ProductDetail/KEMET/C1812C124J1GACTU?qs=Fn3VdHBzkcnCFZmhvW0Uag%3D%3D",+   "stock": 1701,+   "price": "$2.98 (1), $1.79 (100), lead time 266 days"+  },+  "digikey": {+   "part": "399-C1812C124J1GACTUTR-ND",+   "url": "https://www.digikey.com/en/products/detail/kemet/C1812C124J1GACTU/1874760",+   "stock": 1761,+   "price": "$3.53 (1)"+  },+  "lcsc": null,+  "lcsc_note": "JLCPCB/LCSC search for C1812C124J1GACTU returned no part on 2026-10-21"+ },+ "lod": {+  "manifest": "lod/manifest.json",+  "generator": "provenance/tools/lod.py",+  "source": "C1812C124J1GACTU.step",+  "source_sha256": "88e36c2337e5f1e514a7c5ad920e2abc2b0f399373a5eb1023a2c2af80c528ed"+ },+ "tools_note": "Scripts adapted from adom/grm188r72a103ka01d and adom/msash168sc7224ktca01; the hero generator is adom/component-hero hero_glb.py, unmodified.",+ "files": {+  "C1812C124J1GACTU.adom-lbr.json": "febc9b75092f87c62b3ac30d9e63593a14fecd36ffb4ecc7fd02b61e3575725d",+  "verification.json": "cd0cda2c7c786caa107147ba51c8de7946fd72dee65a1bd470000e113e013840",+  "C1812C124J1GACTU.kicad_sym": "83532d32c732fd73bc8966c290816fdb4d6c6a73870d65a187f82be83f90fe5c",+  "C1812C124J1GACTU.lbr": "8ab0ff6c67078aa179707b321443019cc088c6da010378e7dbff10433302ef1d",+  "package-reference.glb": "53804863334397774437d2712467f7d99cf6a28381b8ca78c498bbbfedcf5aa2",+  "c1812c124j1gactu-hero.overlay.json": "c2158a453c8646d4f82bc3e70447f284263c78413d9e48a86e9c0c47f7c731c7",+  "package.json": "a9bbcffea5ac512b12e930d6e183e3259bfbb125d5489916dee0d88a2e08a3f5",+  "c1812c124j1gactu-hero.glb": "fd98debd3272da2303ea2af1a7af804b750ed9887356b33cdcff92680a7d21e2",+  "C_1812_4532Metric_C1812C124J1GACTU.kicad_mod": "3a3d40605d16f4d89e01f0719afb667d0669a0b53ec5e4bad052f8eafab3a29d",+  "models.json": "f4133a569e0b1844aea3330a80acb6ae60518c1c9ebe50571ea7c8388cedc58d",+  "LICENSE": "f5f8a7678131996d5edd47f8fb283ecbd6e8850986dbbdabce1344315814e729",+  "page.json": "87c34a7227ebf54c859ae97f59de63cb09a2ae72b6a675c0a2b4313ce36597b7",+  "C1812C124J1GACTU.step": "88e36c2337e5f1e514a7c5ad920e2abc2b0f399373a5eb1023a2c2af80c528ed",+  "README.md": "6940a37cfdb964da30a0019ad1c8f68efba51718885c7b9f45be2c51665b4235",+  "provenance/C1812C124J1GACTU.step.json": "ee8035f1d805071413942a565258aaafeca4d924e378f50e9bb1ea1a5897bcd4",+  "provenance/tools/finish.py": "8aabfb27e56fd725b8c7e328b7314be41578cd39e0a30b76b596cdd259667402",+  "provenance/tools/parts.py": "857d73d0b15c362893e7907a4111954ce3b4be9d8cd63913da1e7063533b0e66",+  "provenance/tools/records.py": "3a83a0f00d402e8a1fb51179768c87449c0a1e0b35ba750d13e160a9595c836a",+  "provenance/tools/landcheck.py": "d3624535cee3fd54fb87602c767529c44f2d52b781c5acb73da682ce822a6bb7",+  "provenance/tools/overlay_chip.py": "52617a63c9504e020e574ec63f39f518734032fcda7b14ad158e3f8907b31723",+  "provenance/tools/ortho.py": "15f0e98e0a62384682a3129119c5d8e4a7a63e5cd10b356a10e12f4244020d29",+  "provenance/tools/lod.py": "98ac419e109fd403ef9522048530bc2f71147c196bab1dc9d5cda12fc2cd8b06",+  "provenance/tools/push.sh": "9730987a7d277b7e5386319767b97407da89ab20317beceb5a4f914160f82694",+  "provenance/tools/verify_live.py": "6b95fdc89dcc61cebd5295fc1c146060217084abc83312996158294843c71e03",+  "provenance/tools/repose_step.py": "13ba7954c9b42256840aba51529e1ae9548b0c056151249af4860645e0df9852",+  "provenance/tools/c1812c124j1gactu-overlay-spec.json": "fff878e0e736748817c6d4de2c94aa5161da4732e8245ecf5f2ccbd54ca377fe",+  "provenance/tools/build.py": "13f031b2e5480ee1ac759e3026de0ef584628cc7320914483c8e20ec4ce308e9",+  "lod/low.png": "e9b5214c286b495842543087e64b79329e34eda4b1d5a8ea843f039252dd0784",+  "lod/super-low.png": "175038a12e92cc626a29228a20d9821f6cdff3393608b7fd475f0172efacaa8c",+  "lod/low.glb": "8f1ad4771992ec4f9a9f75476d5e37ee50729a5b284172effcb6610a08462776",+  "lod/high.png": "6b6d873316fc536ba705988edb8cc15c0d875351d45fb158101c0e624de6cc20",+  "lod/manifest.json": "dd9359989e3c3775e0f2c266d92f9ec00abe4662315dd0ab420554c29af4be8d",+  "lod/super-low.glb": "bce035912fe7694cf06a5e4d1947efe8cd8a4863a376e2df1070d2febda0ecb0",+  "lod/medium.glb": "909900100576c0fb2203fd429a28f45080d64983bfdfbb5f1999927ad87428a0",+  "lod/high.glb": "bef7ba8cde5577a9e4c18ae18557c1bfebfe35c9b3fbf5c380de2fc3504ccb20",+  "lod/medium.png": "2b833b2f1a2532901bf91e3c56027ce097bf29cf0199127f88c315ac05a60954",+  "docs/KEMET-C1003-C0G-2025-02-20.pdf": "02d179914aeb9585eb2229ba8e18ef9d6b01c77c056de2af295d6950a2a5cc0d",+  "docs/hero.png": "a6ec189d403cd66fd838836392e2649745dd15daa5fa81d0399a54f504df1e45",+  "docs/models/step-plain.png": "c0ecdc20ca30682f90ffcf00ac366607240ad07db54bced23f404d34037d2950",+  "docs/verify/c1812c124j1gactu-check.json": "07f7bac8d13516dfcfe384cee27c8b70df2483a7aa669a1e0d840769daa47c8e",+  "docs/verify/hero-preview.png": "cad91eef07080746d55716f77cb7efdc8c176c504c5715c038635de9aed840dd",+  "docs/verify/c1812c124j1gactu-overlay.png": "30ae1c993d3d0dfbc34a91f4cbcc239c6a040d392beaa9f6066bd92e292d4ec2",+  "docs/figures/kemet-ds-p8-waterfall-1812.png": "b558affcc15fd4dc980a348155b4f2217d6dc4a26d8151121f1a4e0b156a9b39",+  "docs/figures/kemet-ds-p13-soldering.png": "3c337c99956fb0ba4e6ed1d7acaa5671618b2b59d1b793764926fc69f6442db7",+  "docs/figures/kemet-ds-p2-benefits-packaging.png": "66fc63e1866b22ed7f1d358d9a1c40f1bbc5d8ad848afd1050926927df1b4689",+  "docs/figures/kemet-ds-p12-land-pattern.png": "c6245d9166539a8b215114c2364e169762830081943bd3258e8ee1974288b821",+  "docs/figures/kemet-ds-p4-electrical.png": "bf8972f6aa1ad36e02fd95f6c1e77fe98055ead639904713c4e03a46227c3f7b",+  "docs/figures/figures.json": "b9d44071a97be2046a361d2602c948f7d21c22f777b71d4095368f6c2596ddbb",+  "docs/figures/kemet-ds-p15-marking.png": "0af48889b2a288a2fe612c5f01403017b43105619b7e2acc11406be113083466",+  "docs/figures/kemet-ds-p1-overview-ordering.png": "b24fcc4cfcf3ee41c7a2a3fcc681658b12e9bec39249a544b4d5e95967b935f0",+  "docs/figures/kemet-ds-p3-dimensions.png": "4ca075141bbbf7977223b2d8b9aa1f377722b5d1fe7a1bd77ca96d311743b1d9",+  "docs/figures/kemet-ds-p10-thickness.png": "7211050566dcb2acafd592c9491b533f7c6c894bf6fbacecd2631484d3bd25e3",+  "land-pattern/land-check.json": "807a04ca14e1aafb57f1b1b7c3903274c584b429309e2abd4615737a66c62cfc",+  "land-pattern/land-pattern-icon.png": "b7ea9d3ee69bf1624f511156c8a2318285537e0f7976d8c2421451c6ee6fb7da",+  "land-pattern/land-pattern.png": "b0d2226eb261a22ee847ef302abf24e3d304ca5f6a8a488e7503bcfbe9150ac0",+  "land-pattern/README.md": "c24e33ad743d4efc7836e5142afd68ca03d54221dff8a56c335b5d49370ff742",+  "vendor/C1812C124J1GACTU-kemet-original.step": "b5d45febdee8defce1c3da229b540ed0a72760aeaa02501c728f7f8143eb6de7",+  "altium/C1812C124J1GACTU.PcbLib": "8fbecca79504564d582770b912d34fc9c62341bbd948d94df8d51f9174d0a356",+  "altium/C1812C124J1GACTU.SchLib": "b84eb045fe23f2260ddda06777a2bd7a494691ab4be41a466bf73a9dc526b79f"+ },+ "files_note": "provenance.json itself is not listed.",+ "built_by": "Claude (AI agent) for John Lauer",+ "date": "2026-10-21"+}\ No newline at end of file
provenance/C1812C124J1GACTU.step.jsonadded+118
@@ -0,0 +1,118 @@+{+ "source": "C1812C124J1GACTU-kemet-original.step",+ "source_sha256": "b5d45febdee8defce1c3da229b540ed0a72760aeaa02501c728f7f8143eb6de7",+ "source_bbox_mm": [+  -0.0,+  -0.0,+  -3.5,+  4.8,+  1.55,+  0.0+ ],+ "source_units": "mm",+ "source_up_axis": "+Y",+ "source_solids": 4,+ "kept": {+  "solid": "package (terminals and ceramic)",+  "faces": 72,+  "volume_mm3": 23.90241,+  "volume_after_mm3": 23.90241,+  "face_colours_srgb": [+   [+    0.376,+    0.231,+    0.0+   ],+   [+    0.552,+    0.552,+    0.552+   ]+  ]+ },+ "dropped": {+  "what": "KEMET logo glyph solids on the ceramic top (SolidWorks default lavender, no own colour); the real C0G part is unmarked (C1003_C0G p.15)",+  "solids": [+   {+    "faces": 25,+    "volume_mm3": 0.0019,+    "bbox_mm": [+     2.0775,+     1.4772,+     -2.1768,+     2.592,+     1.4822,+     -0.99+    ],+    "thickness_mm": 0.005+   },+   {+    "faces": 19,+    "volume_mm3": 0.00124,+    "bbox_mm": [+     2.0775,+     1.4772,+     -3.0648,+     2.592,+     1.4822,+     -2.0908+    ],+    "thickness_mm": 0.005+   },+   {+    "faces": 13,+    "volume_mm3": 0.00079,+    "bbox_mm": [+     2.0775,+     1.4772,+     -1.057,+     2.592,+     1.4822,+     -0.4375+    ],+    "thickness_mm": 0.005+   }+  ]+ },+ "rotation": "+90 deg about X (y -> z, z -> -y)",+ "translation_mm": [+  -2.4,+  -1.75,+  0.0+ ],+ "matrix_3x4": [+  [+   1.0,+   0.0,+   0.0,+   -2.4+  ],+  [+   0.0,+   0.0,+   -1.0,+   -1.75+  ],+  [+   0.0,+   1.0,+   0.0,+   0.0+  ]+ ],+ "output": "C1812C124J1GACTU.step",+ "output_sha256": "88e36c2337e5f1e514a7c5ad920e2abc2b0f399373a5eb1023a2c2af80c528ed",+ "output_bbox_mm": [+  -2.4,+  -1.75,+  -0.0,+  2.4,+  1.75,+  1.55+ ],+ "LWT_mm": [+  4.8,+  3.5,+  1.55+ ]+}\ No newline at end of file
provenance/tools/build.pyadded+134
@@ -0,0 +1,134 @@+"""Build the C1812C124J1GACTU page tree (KEMET STEP re-posed, KiCad footprint renamed, symbol, checks, GLB, LODs, figures).++usage: build.py            (cadquery venv python, run with -I under tools/capped)+Writes /home/adom/project/uta-dynos/work/pages/c1812c124j1gactu/. Downloads are read from+/home/adom/project/uta-dynos/work/pages/_build/dl (never executed); scripts live in this folder.+"""+import sys, os, json, shutil, subprocess, hashlib, re+from PIL import Image+TOOLS = os.path.dirname(os.path.abspath(__file__))+sys.path.insert(0, TOOLS)+from parts import P, ROOT, FIGS, DOCS, TODAY, DS_URL+DL = os.path.join(ROOT, "dl"); WORK = os.path.join(ROOT, "work", "kemet"); os.makedirs(WORK, exist_ok=True)+PY = "/home/adom/.venvs/cq/bin/python"+slug, MPN = P["slug"], P["mpn"]+OUT = f"/home/adom/project/uta-dynos/work/pages/{slug}"+sha = lambda f: hashlib.sha256(open(f, "rb").read()).hexdigest()+def run(cmd, **kw):+    r = subprocess.run(cmd, capture_output=True, text=True, **kw)+    if r.returncode != 0:+        print("FAILED", cmd, r.stdout[-2000:], r.stderr[-2000:]); sys.exit(1)+    return r+for d in ("docs/verify", "docs/models", "docs/figures", "lod", "provenance/tools", "land-pattern", "altium", "vendor"):+    os.makedirs(os.path.join(OUT, d), exist_ok=True)++# ---------- STEP: KEMET's own model, original kept; board model re-posed to Z-up, logo glyphs dropped ----------+src_step = os.path.join(DL, "kemet", "step", f"{MPN}-kemet-original.step")+orig_name = f"vendor/{MPN}-kemet-original.step"+shutil.copyfile(src_step, os.path.join(OUT, orig_name))+step_name = f"{MPN}.step"; step_path = os.path.join(OUT, step_name)+r = run([PY, "-I", os.path.join(TOOLS, "repose_step.py"), src_step, step_path], cwd=TOOLS)+repose = json.loads(r.stdout.strip().splitlines()[-1]); repose["output"] = step_name+assert repose["source_sha256"] == sha(os.path.join(OUT, orig_name))+json.dump(repose, open(os.path.join(OUT, f"provenance/{step_name}.json"), "w"), indent=1)++# ---------- footprint: KiCad standard C_1812_4532Metric, renamed, model path set ----------+fp_name = f"{P['fp']}_{MPN}"+src_fp = open(os.path.join(DL, "kicad", P["fp"] + ".kicad_mod")).read()+fp = src_fp.replace(f'(footprint "{P["fp"]}"', f'(footprint "{fp_name}"', 1)+fp = re.sub(r'\(model "[^"]+"', f'(model "${{KIPRJMOD}}/{step_name}"', fp)+assert fp.count(fp_name) == 1 and "${KIPRJMOD}/" + step_name in fp+fp_file = f"{fp_name}.kicad_mod"+open(os.path.join(OUT, fp_file), "w").write(fp)++# ---------- symbol (standard capacitor graphic, pin ends on the 2.54 mm grid) ----------+d = P["dist"]+def prop(name, val, y, hide=True):+    h = " hide" if hide else ""+    return f'    (property "{name}" "{val}" (at 0 {y} 0) (effects (font (size 1.27 1.27)){h}))\n'+props = (prop("Reference", "C", 0, False).replace("(at 0 0 0)", "(at 2.54 1.27 0)").replace("(font (size 1.27 1.27))", "(font (size 1.27 1.27)) (justify left)")+         + prop("Value", P["value_sym"], 0, False).replace("(at 0 0 0)", "(at 2.54 -1.27 0)").replace("(font (size 1.27 1.27))", "(font (size 1.27 1.27)) (justify left)")+         + prop("Footprint", fp_name, -7.62) + prop("Datasheet", DS_URL, -10.16) + prop("Description", P["brief"], -12.7)+         + prop("MPN", MPN, -15.24) + prop("Manufacturer", P["mfr"], -17.78)+         + prop("Mouser", d["mouser"][0], -20.32) + prop("DigiKey", d["digikey"][0], -22.86) + prop("LCSC", "", -25.4)+         + prop("Adom Wiki", f"https://wiki.adom.inc/adom/{slug}", -27.94))+sym = f'''(kicad_symbol_lib (version 20241209) (generator "adom_passive_symbol")+  (symbol "{MPN}" (pin_numbers (hide yes)) (pin_names (offset 0.254) (hide yes)) (exclude_from_sim no) (in_bom yes) (on_board yes)+{props}    (property "ki_keywords" "capacitor mlcc 1812 4532 100V C0G NP0 120nF {MPN}" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))+    (symbol "{MPN}_0_1"+      (polyline (pts (xy -2.032 -0.762) (xy 2.032 -0.762)) (stroke (width 0.508) (type default)) (fill (type none)))+      (polyline (pts (xy -2.032 0.762) (xy 2.032 0.762)) (stroke (width 0.508) (type default)) (fill (type none)))+    )+    (symbol "{MPN}_1_1"+      (pin passive line (at 0 2.54 270) (length 1.778) (name "~" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))))+      (pin passive line (at 0 -2.54 90) (length 1.778) (name "~" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))))+    )+  )+)+'''+sym_file = f"{MPN}.kicad_sym"+open(os.path.join(OUT, sym_file), "w").write(sym)++# ---------- documents + figure crops (150 dpi) ----------+pdfs = {}+for key, local, pub, title, url in DOCS:+    shutil.copyfile(os.path.join(DL, local), os.path.join(OUT, pub)); pdfs[key] = os.path.join(DL, local)+figs = []+for name, doc, page, box, caption in FIGS:+    png = os.path.join(WORK, f"fig150-{doc}-{page}.png")+    if not os.path.exists(png):+        run(["pdftoppm", "-r", "150", "-png", "-singlefile", "-f", str(page), "-l", str(page), pdfs[doc], png[:-4]])+    b = [int(round(v * 1.5)) for v in box]+    out = os.path.join(OUT, "docs/figures", name)+    Image.open(png).convert("RGB").crop(b).save(out, optimize=True)+    figs.append({"file": f"docs/figures/{name}", "document": next(t for k, _, _, t, _ in DOCS if k == doc), "page": page,+                 "crop_px_at_150dpi": b, "caption": caption, "sha256": sha(out)})+json.dump(figs, open(os.path.join(OUT, "docs/figures/figures.json"), "w"), indent=1)++# ---------- overlay proof of the maker's STEP against the printed limits ----------+crops100 = []+for pg, box in ((3, [40, 330, 810, 770]), (10, [40, 100, 610, 960])):+    png = os.path.join(WORK, f"ds100-p{pg}.png")+    if not os.path.exists(png):+        run(["pdftoppm", "-r", "100", "-png", "-singlefile", "-f", str(pg), "-l", str(pg), pdfs["ds"], png[:-4]])+    crops100.append([png, box])+spec = {"label": MPN, "mpn": MPN, **P["overlay"], "crops": crops100, "marking": "none (C0G is supplied unmarked, p.15)"}+sp = os.path.join(WORK, "overlay-spec.json"); json.dump(spec, open(sp, "w"))+r = run([PY, "-I", os.path.join(TOOLS, "overlay_chip.py"), sp, step_path, os.path.join(OUT, f"docs/verify/{slug}-overlay.png")], cwd=TOOLS)+check = json.loads(r.stdout.strip().splitlines()[-1]); check["step"] = step_name+json.dump(check, open(os.path.join(OUT, f"docs/verify/{slug}-check.json"), "w"), indent=1)+pub = dict(spec); pub["crops"] = [["datasheet p.3 render at 100 dpi", crops100[0][1]], ["datasheet p.10 render at 100 dpi", crops100[1][1]]]+json.dump(pub, open(os.path.join(OUT, f"provenance/tools/{slug}-overlay-spec.json"), "w"), indent=1)+print("overlay all_pass:", check["all_pass"], [(c["dim"], c["model_mm"], c["pass"]) for c in check["checks"]])+if not check["all_pass"]:+    sys.exit(2)++# ---------- land: KiCad pads (used) vs KEMET Table 3 density level B ----------+r = run([PY, "-I", os.path.join(TOOLS, "landcheck.py"), slug, os.path.join(OUT, "land-pattern"), os.path.join(OUT, fp_file)], cwd=TOOLS)+land = json.loads(r.stdout.strip().splitlines()[-1])+json.dump(land, open(os.path.join(OUT, "land-pattern/land-check.json"), "w"), indent=1)++# ---------- GLB + thumbnails (step2glb service) ----------+glb_path = os.path.join(OUT, "package-reference.glb")+g = subprocess.run(["step2glb", "convert", step_path, "-o", glb_path], capture_output=True, text=True)+print("step2glb exit", g.returncode)+assert g.returncode == 0 and os.path.getsize(glb_path) > 1000+def thumb(src, out, wh=None):+    extra = ["--width", str(wh[0]), "--height", str(wh[1])] if wh else []+    return subprocess.run(["step2glb", "thumbnail", src, "-o", out] + extra, capture_output=True, text=True).returncode+th = [thumb(step_path, os.path.join(OUT, "docs/hero.png"), (1600, 1000)), thumb(step_path, os.path.join(OUT, "docs/models/step-plain.png"))]+print("thumbs", th)++# ---------- LODs ----------+run([PY, "-I", os.path.join(TOOLS, "lod.py"), step_path, MPN, OUT, step_name], cwd=TOOLS)+man = json.load(open(os.path.join(OUT, "lod/manifest.json")))+man["purpose"] = "clean board-use component models: no footprint pads, outlines, labels or silkscreen; no marking (KEMET supplies C0G parts unmarked)"+man["generator"] = "lod.py (OCCT via cadquery-ocp)"+json.dump(man, open(os.path.join(OUT, "lod/manifest.json"), "w"), indent=1)++for t in ("build.py", "parts.py", "repose_step.py", "ortho.py", "overlay_chip.py", "landcheck.py", "lod.py", "records.py", "finish.py", "push.sh", "verify_live.py"):+    if os.path.exists(os.path.join(TOOLS, t)):+        shutil.copyfile(os.path.join(TOOLS, t), os.path.join(OUT, "provenance/tools", t))+json.dump({"step": step_name, "orig": orig_name, "fp": fp_file, "sym": sym_file, "check": check, "land": land, "thumbs": th, "date": TODAY},+          open(os.path.join(ROOT, "work", f"{slug}-build.json"), "w"), indent=1)+print("built", OUT)
provenance/tools/c1812c124j1gactu-overlay-spec.jsonadded+49
@@ -0,0 +1,49 @@+{+ "label": "C1812C124J1GACTU",+ "mpn": "C1812C124J1GACTU",+ "L": [+  4.5,+  0.3+ ],+ "W": [+  3.2,+  0.3+ ],+ "T": [+  1.4,+  0.15+ ],+ "band_top": [+  0.25,+  0.95+ ],+ "band_bot": [+  0.25,+  0.95+ ],+ "band_top_name": "B (band width), top",+ "band_bot_name": "B (band width), bottom",+ "ppmm": 100,+ "source": "KEMET C1003_C0G 2/20/2025 p.3 (1812) and p.10 (thickness code GH)",+ "crops": [+  [+   "datasheet p.3 render at 100 dpi",+   [+    40,+    330,+    810,+    770+   ]+  ],+  [+   "datasheet p.10 render at 100 dpi",+   [+    40,+    100,+    610,+    960+   ]+  ]+ ],+ "marking": "none (C0G is supplied unmarked, p.15)"+}\ No newline at end of file
provenance/tools/finish.pyadded+49
@@ -0,0 +1,49 @@+"""Fusion .lbr, Altium SchLib/PcbLib, hero GLB and the plain-STEP thumbnail for one page.++usage: finish.py <slug>   (after build.py and a first records.py run, which writes page.json)+"""+import sys, os, json, subprocess, hashlib, shutil+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))+from parts import PARTS+ROOT = "/home/adom/project/uta-dynos/work/pages/_build"+HERO_PY = os.path.join(ROOT, "component-hero/skills/component-hero/scripts/hero_glb.py")+slug = sys.argv[1]+P = next(p for p in PARTS if p["slug"] == slug)+OUT = os.path.join(ROOT, "pages", slug); MPN = P["mpn"]+B = json.load(open(os.path.join(ROOT, "work", f"{slug}-build.json")))+sha = lambda f: hashlib.sha256(open(os.path.join(OUT, f), "rb").read()).hexdigest()+def run(cmd, cwd=OUT):+    r = subprocess.run(cmd, capture_output=True, text=True, cwd=cwd)+    if r.returncode != 0:+        print("FAILED", cmd, r.stdout[-1500:], r.stderr[-1500:]); sys.exit(1)+    return (r.stdout + r.stderr).strip()+rec = {}+# plain STEP thumbnail for models.json+run(["step2glb", "thumbnail", B["step"], "-o", "docs/models/step-plain.png"])+# Fusion / EAGLE+o = run(["adom-lbr", "generate", "--sym", B["sym"], "--fp", B["fp"], "--part-metadata", "page.json", "-o", f"{MPN}.lbr"])+_l = run(["adom-lbr", "lint", f"{MPN}.lbr"]); lint = json.loads(_l[_l.index("{"):_l.rindex("}") + 1])+assert lint["passed"], lint+rec["lbr"] = {"file": f"{MPN}.lbr", "tool": "adom-lbr " + run(["adom-lbr", "--version"]).split()[-1],+              "command": f"adom-lbr generate --sym {B['sym']} --fp {B['fp']} --part-metadata page.json -o {MPN}.lbr",+              "lint": {"passed": lint["passed"], "errors": len(lint.get("errors", [])), "warnings": len(lint.get("warnings", []))}, "sha256": sha(f"{MPN}.lbr")}+# Altium: hub JSON beside the STEP so the PcbLib embeds the body+os.makedirs(os.path.join(OUT, "altium"), exist_ok=True)+o1 = run(["adom-lbr", "import-kicad", "--sym", B["sym"], "--fp", B["fp"], "--mpn", MPN, "--out", f"{MPN}.adom-lbr.json"])+o2 = run(["adom-lbr", "assemble-altium", "--no-dedupe", "--out", f"altium/{MPN}", f"{MPN}.adom-lbr.json"])+rep = json.loads(o2[o2.index("{"):o2.rindex("}") + 1])+assert rep["status"] == "ok" and rep["models"]["embedded"] == 1, rep+rec["altium"] = {"files": [f"altium/{MPN}.SchLib", f"altium/{MPN}.PcbLib", f"{MPN}.adom-lbr.json"],+                 "commands": [f"adom-lbr import-kicad --sym {B['sym']} --fp {B['fp']} --mpn {MPN} --out {MPN}.adom-lbr.json",+                              f"adom-lbr assemble-altium --no-dedupe --out altium/{MPN} {MPN}.adom-lbr.json"],+                 "report": rep, "hints": [l for l in o1.splitlines() if l.startswith("HINT")],+                 "sha256": {f: sha(f) for f in (f"altium/{MPN}.SchLib", f"altium/{MPN}.PcbLib", f"{MPN}.adom-lbr.json")}}+# hero GLB (component-hero standard)+o = run(["/home/adom/.venvs/cq/bin/python", "-I", HERO_PY, "package-reference.glb", B["fp"], "page.json", f"{slug}-hero.glb", "--png", "docs/verify/hero-preview.png"])+print(o)+assert "align OK" in o, o+rec["hero"] = {"file": f"{slug}-hero.glb", "tool": "adom/component-hero skills/component-hero/scripts/hero_glb.py",+               "tool_sha256": hashlib.sha256(open(HERO_PY, "rb").read()).hexdigest(),+               "inputs": ["package-reference.glb", B["fp"], "page.json"], "result": o.splitlines()[-1], "sha256": sha(f"{slug}-hero.glb")}+json.dump(rec, open(os.path.join(ROOT, "work", f"{slug}-finish.json"), "w"), indent=1)+print("finished", slug)
provenance/tools/landcheck.pyadded+117
@@ -0,0 +1,117 @@+"""Compare the KiCad footprint pads with the manufacturer's recommended land pattern.++usage: landcheck.py <slug> <outdir>+Reads parts.PARTS[slug]["land"] and the page's footprint (KiCad library geometry).+Writes <outdir>/land-pattern.png (true-scale drawing + table) and+<outdir>/land-pattern-icon.png (256 px square thumbnail), prints a JSON result.++Drawing: grey = part body at datasheet nominal, teal = KiCad pads (copper),+amber dashed = the maker's land at the middle of its printed range (or its+printed nominal), amber band = the range the maker allows for the inner and+outer pad edges where the maker prints a range.+"""+import sys, os, json, re, glob+from PIL import Image, ImageDraw, ImageFont+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))+from parts import PARTS++slug, outdir = sys.argv[1], sys.argv[2]+P = next(p for p in PARTS if p["slug"] == slug)+L = P["land"]+page = os.path.dirname(outdir.rstrip("/"))+fpf = sys.argv[3] if len(sys.argv) > 3 else glob.glob(os.path.join(page, "*.kicad_mod"))[0]+fp = open(fpf).read()+pads = [tuple(map(float, m)) for m in re.findall(r'\(pad "\d" smd (?:roundrect|rect)\s*\(at ([-\d.]+) ([-\d.]+)\)\s*\(size ([\d.]+) ([\d.]+)\)', fp)]+_m = re.search(r"roundrect_rratio ([\d.]+)", fp); RR = float(_m.group(1)) if _m else 0.0+x, _, w, h = pads[1]+k = {"gap": round(2 * x - w, 3), "len": round(w, 3), "wid": round(h, 3), "overall": round(2 * x + w, 3)}++def mid(v): return (v[0] + v[1]) / 2 if isinstance(v, list) else v+rows = []+if L["kind"] in ("range", "tol"):+    rng = {"gap": L["gap_a"], "len": L["len_b"], "wid": L["wid_c"]}+    rng["overall"] = L.get("overall") or [round(L["gap_a"][0] + 2 * L["len_b"][0], 3), round(L["gap_a"][1] + 2 * L["len_b"][1], 3)]+    derived_overall = not L.get("overall")+    for key, name in (("gap", "gap between lands"), ("len", "land length"), ("wid", "land width"),+                      ("overall", "overall length (a + 2b, derived)" if derived_overall else "overall length")):+        lo, hi = rng[key]; v = k[key]+        ok = lo - 1e-6 <= v <= hi + 1e-6+        dev = 0 if ok else round(v - hi if v > hi else v - lo, 3)+        rows.append({"dim": name, "kicad_mm": v, "maker_min": lo, "maker_max": hi, "within": ok, "outside_by_mm": dev})+    ref = {kk: mid(rng[kk]) for kk in rng}+    if L["kind"] == "tol":+        ref = {"gap": L["nominal"]["gap_a"], "len": L["nominal"]["len_b"], "wid": L["nominal"]["wid_c"], "overall": L["nominal"]["overall"]}+else:+    ref = {"gap": L["gap_a"], "len": L["len_b"], "wid": L["wid_c"], "overall": L["overall"]}+    for key, name in (("gap", "gap between lands"), ("len", "land length"), ("wid", "land width"), ("overall", "overall length")):+        rows.append({"dim": name, "kicad_mm": k[key], "maker_nominal": ref[key], "difference_mm": round(k[key] - ref[key], 3)})++# ---------- drawing ----------+body = P["body_mm"]+span = max(k["overall"], ref["overall"] if isinstance(ref["overall"], float) else 0) + 0.4+PP = 560 / span+W_, H_ = 1000, int(max(k["wid"], ref["wid"], body["y_mm"]) * PP + 120)+img = Image.new("RGBA", (W_, H_), (255, 255, 255, 255))+d = ImageDraw.Draw(img, "RGBA")+cx, cy = W_ / 2, H_ / 2 + 10+def R(x0, y0, x1, y1): return [cx + x0 * PP, cy - y1 * PP, cx + x1 * PP, cy - y0 * PP]+# allowed edge bands (range kinds)+if L["kind"] in ("range", "tol"):+    g0, g1 = L["gap_a"]; b0, b1 = L["len_b"]; c1 = L["wid_c"][1]+    for s in (-1, 1):+        for a, b in ((g0 / 2, g1 / 2), (g0 / 2 + b0, g1 / 2 + b1)):+            xa, xb = sorted((s * a, s * b))+            d.rectangle(R(xa, -c1 / 2, xb, c1 / 2), fill=(255, 190, 70, 70))+# body+d.rectangle(R(-body["x_mm"] / 2, -body["y_mm"] / 2, body["x_mm"] / 2, body["y_mm"] / 2), outline=(110, 110, 110, 255), width=2)+# KiCad pads, rounded rect (the footprint's own roundrect_rratio)+for px, py, pw, ph in pads:+    rr = RR * min(pw, ph) * PP+    d.rounded_rectangle(R(px - pw / 2, py - ph / 2, px + pw / 2, py + ph / 2), radius=rr, fill=(0, 150, 150, 110), outline=(0, 120, 120, 255), width=2)+# maker land, dashed amber+def dashed(box, col, wdt=3):+    xa, ya, xb, yb = box+    for (p, q) in (((xa, ya), (xb, ya)), ((xb, ya), (xb, yb)), ((xb, yb), (xa, yb)), ((xa, yb), (xa, ya))):+        n = int(max(abs(q[0] - p[0]), abs(q[1] - p[1])) / 7) or 1+        for i in range(0, n, 2):+            t0, t1 = i / n, min(1, (i + 1) / n)+            d.line([(p[0] + (q[0] - p[0]) * t0, p[1] + (q[1] - p[1]) * t0), (p[0] + (q[0] - p[0]) * t1, p[1] + (q[1] - p[1]) * t1)], fill=col, width=wdt)+for s in (-1, 1):+    a, b = ref["gap"] / 2, ref["gap"] / 2 + ref["len"]+    xa, xb = sorted((s * a, s * b))+    dashed(R(xa, -ref["wid"] / 2, xb, ref["wid"] / 2), (200, 110, 0, 255))+icon_box = (int(cx - 0.5 * (k["overall"] + 0.25) * PP), int(cy - 0.5 * (k["overall"] + 0.25) * PP))+icon_side = int((k["overall"] + 0.25) * PP)+padded = Image.new("RGBA", (W_ + 2 * icon_side, H_ + 2 * icon_side), (255, 255, 255, 255)); padded.paste(img, (icon_side, icon_side))+icon = padded.crop((icon_box[0] + icon_side, icon_box[1] + icon_side, icon_box[0] + 2 * icon_side, icon_box[1] + 2 * icon_side)).convert("RGB").resize((256, 256), Image.LANCZOS)+mask = Image.new("L", (256, 256), 0); ImageDraw.Draw(mask).rounded_rectangle([0, 0, 255, 255], radius=28, fill=255)+ic = Image.new("RGBA", (256, 256), (0, 0, 0, 0)); ic.paste(icon, (0, 0), mask)+ic.save(os.path.join(outdir, "land-pattern-icon.png"))++f = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 15)+fb = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 17)+lines = []+for r in rows:+    if "within" in r:+        lines.append((f"{r['dim']}:  KiCad {r['kicad_mm']:.2f}   maker {r['maker_min']:.2f} to {r['maker_max']:.2f}   "+                      + ("inside" if r["within"] else f"outside by {r['outside_by_mm']:+.2f}"), (0, 120, 40) if r["within"] else (190, 90, 0)))+    else:+        lines.append((f"{r['dim']}:  KiCad {r['kicad_mm']:.2f}   maker {r['maker_nominal']:.2f}   difference {r['difference_mm']:+.2f}", (40, 40, 40)))+sheet = Image.new("RGB", (W_, H_ + 100 + 24 * len(lines) + 70), (255, 255, 255))+sd = ImageDraw.Draw(sheet)+sd.text((14, 10), f"{P['mpn']}: KiCad {P['fp']} pads vs maker's land, true scale", fill=(0, 0, 0), font=fb)+sheet.paste(img.convert("RGB"), (0, 40))+y = H_ + 46+sd.text((14, y), "grey = body (nominal)   teal = KiCad pads   amber dashed = maker's land (middle of range or nominal)", fill=(60, 60, 60), font=f); y += 22+sd.text((14, y), "amber band = where the maker allows the inner and outer pad edges (range sheets only)", fill=(60, 60, 60), font=f); y += 26+for t, c in lines:+    sd.text((14, y), t, fill=c, font=f); y += 24+import textwrap+for ln in textwrap.wrap("Source: " + L["source"], 115):+    sd.text((14, y + 4), ln, fill=(90, 90, 90), font=f); y += 20+sheet.save(os.path.join(outdir, "land-pattern.png"))++res = {"footprint": os.path.basename(fpf), "kicad_library_footprint": P["fp"], "kicad": k, "maker_reference": ref,+       "maker_source": L["source"], "maker_letters": L["letters"], "rows": rows,+       "all_within": all(r.get("within", True) for r in rows) if L["kind"] != "nominal" else None}+print(json.dumps(res))
provenance/tools/lod.pyadded+199
@@ -0,0 +1,199 @@+"""Board-use LOD GLBs (high, medium, low, super-low) for a small two-terminal chip.++Input: the page's board-use STEP (Z-up, mm, seated on z=0, centred).+Output (in <outdir>/lod/): high.glb medium.glb low.glb super-low.glb, a 400x300+PNG preview of each rendered from that GLB's own triangles, manifest.json+(schema adom/component-lod@1).++Every level: Z-up, metres, the same origin and pose as the STEP (no recentring,+no rotation), primitives batched by material (one per material), the STEP's own+colours.  No footprint, pad outline, label or silkscreen helpers exist in the+source, so nothing is removed.  high/medium/low are OCCT tessellations of the+exact B-rep at decreasing deflection; super-low replaces each material group by+its bounding box per side of the part (terminals, body, overcoat), which keeps+the silhouette, the terminal bands and the seating plane.++usage: lod.py part.step MPN outdir [source_label]+"""+import sys, os, json, struct, hashlib+import numpy as np+from PIL import Image, ImageDraw+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))+import ortho+from OCP.BRepMesh import BRepMesh_IncrementalMesh+from OCP.BRepTools import BRepTools++step, mpn, outdir = sys.argv[1], sys.argv[2], sys.argv[3]+src_label = sys.argv[4] if len(sys.argv) > 4 else os.path.basename(step)+# optional: the unmarked STEP; colours present only in the marked source are the marking,+# kept in high and medium, dropped in low and super-low (too small to read there)+plain_step = sys.argv[5] if len(sys.argv) > 5 else None+MARK_COLS = set()+lod_dir = os.path.join(outdir, "lod"); os.makedirs(lod_dir, exist_ok=True)++LEVELS = [("high", 0.0005, 0.1), ("medium", 0.004, 0.35), ("low", 0.02, 0.9)]++def tris_at(lin, ang):+    # ortho.load_tris meshes with fixed params; clean and remesh by patching BRepMesh call+    orig = ortho.BRepMesh_IncrementalMesh+    def mesh(shp, _l, rel, _a, par):+        BRepTools.Clean_s(shp)+        return orig(shp, lin, False, ang, True)+    ortho.BRepMesh_IncrementalMesh = mesh+    try:+        return ortho.load_tris(step)+    finally:+        ortho.BRepMesh_IncrementalMesh = orig++def boxes_from(tris):+    groups = {}+    for p, c in tris:+        groups.setdefault(c, []).append(p)+    allp = np.concatenate([p for p, _ in tris]); xmax = allp[:, 0].max()+    endc = {}+    for p, c in tris:+        if np.allclose(p[:, 0], xmax, atol=1e-4): endc[c] = endc.get(c, 0) + 1+    tcol = max(endc, key=endc.get)+    boxes = []+    for c, ps in groups.items():+        T = np.stack(ps)+        crosses = c != tcol and ((T[:, :, 0].min(1) < -1e-6) & (T[:, :, 0].max(1) > 1e-6)).any()  # terminals: always two boxes+        P = T.reshape(-1, 3)+        parts = [P] if crosses else [P[P[:, 0] < 0], P[P[:, 0] > 0]]+        for Q in parts:+            if len(Q):+                boxes.append([Q.min(0), Q.max(0), c])+    tin = min(min(abs(lo[0]), abs(hi[0])) for lo, hi, c in boxes if c == tcol)+    # the body box reaches the same top as the overcoat box: coplanar tops z-fight, so stop the body at the overcoat's underside+    dark = min({c for _, _, c in boxes}, key=lambda c: sum(c))+    dk = [(lo, hi) for lo, hi, c in boxes if c == dark]+    if dark != tcol and dk:+        dlo = min(lo[2] for lo, hi in dk); dhi = max(hi[2] for lo, hi in dk)+        for b in boxes:+            if b[2] not in (dark, tcol) and b[1][2] >= dhi - 1e-6:+                b[1] = b[1].copy(); b[1][2] = dlo+    out = []+    for lo, hi, c in boxes:+        if c != tcol:+            lo = lo.copy(); hi = hi.copy(); lo[0] = max(lo[0], -tin); hi[0] = min(hi[0], tin)+        v = np.array([[x, y, z] for x in (lo[0], hi[0]) for y in (lo[1], hi[1]) for z in (lo[2], hi[2])])+        for a_, b_, cc, d in [(0, 1, 3, 2), (4, 6, 7, 5), (0, 4, 5, 1), (2, 3, 7, 6), (0, 2, 6, 4), (1, 5, 7, 3)]:+            out.append((v[[a_, b_, cc]], c)); out.append((v[[a_, cc, d]], c))+    return out++def write_glb(tris, path, name):+    mats = []; groups = {}+    for p, c in tris:+        if c not in groups:+            groups[c] = []; mats.append(c)+        groups[c].append(p)+    bin_ = b""; views = []; accs = []; prims = []+    def add_view(data, target):+        nonlocal bin_+        off = len(bin_); bin_ += data+        while len(bin_) % 4: bin_ += b"\0"+        views.append({"buffer": 0, "byteOffset": off, "byteLength": len(data), "target": target})+        return len(views) - 1+    decoded = 0; ntri = 0+    for mi, c in enumerate(mats):+        P = np.concatenate(groups[c]).reshape(-1, 3, 3)+        n = np.cross(P[:, 1] - P[:, 0], P[:, 2] - P[:, 0]); n /= np.linalg.norm(n, axis=1, keepdims=True) + 1e-30+        pos = (P.reshape(-1, 3) / 1000.0).astype(np.float32)          # mm -> metres+        nor = np.repeat(n, 3, axis=0).astype(np.float32)+        # weld identical (position, normal) vertices+        key = np.concatenate([np.round(pos, 9), np.round(nor, 5)], 1)+        uniq, inv = np.unique(key, axis=0, return_inverse=True)+        upos = uniq[:, :3].astype(np.float32); unor = uniq[:, 3:].astype(np.float32)+        idx = inv.astype(np.uint16 if len(uniq) < 65535 else np.uint32).ravel()+        vp = add_view(upos.tobytes(), 34962); vn = add_view(unor.tobytes(), 34962); vi = add_view(idx.tobytes(), 34963)+        accs.append({"bufferView": vp, "componentType": 5126, "count": len(upos), "type": "VEC3",+                     "min": upos.min(0).tolist(), "max": upos.max(0).tolist()})+        accs.append({"bufferView": vn, "componentType": 5126, "count": len(unor), "type": "VEC3"})+        accs.append({"bufferView": vi, "componentType": 5123 if idx.dtype == np.uint16 else 5125, "count": len(idx), "type": "SCALAR"})+        prims.append({"attributes": {"POSITION": len(accs) - 3, "NORMAL": len(accs) - 2}, "indices": len(accs) - 1, "material": mi})+        decoded += upos.nbytes + unor.nbytes + idx.nbytes; ntri += len(idx) // 3+    gl = {"asset": {"version": "2.0", "generator": "capencoder passives lod.py (OCCT tessellation)"},+          "scene": 0, "scenes": [{"nodes": [0]}],+          "nodes": [{"name": name, "mesh": 0, "extras": {"adomUpAxis": "Z", "units": "metres"}}],+          "meshes": [{"name": name, "primitives": prims}],+          "materials": [{"name": f"mat_{i}", "pbrMetallicRoughness": {"baseColorFactor": [*c, 1.0], "metallicFactor": 0.0, "roughnessFactor": 0.6}} for i, c in enumerate(mats)],+          "accessors": accs, "bufferViews": views, "buffers": [{"byteLength": len(bin_)}]}+    js = json.dumps(gl, separators=(",", ":")).encode()+    while len(js) % 4: js += b" "+    glb = struct.pack("<III", 0x46546C67, 2, 12 + 8 + len(js) + 8 + len(bin_)) + struct.pack("<II", len(js), 0x4E4F534A) + js + struct.pack("<II", len(bin_), 0x004E4942) + bin_+    open(path, "wb").write(glb)+    return {"decodedBufferBytes": decoded, "triangles": ntri, "primitives": len(prims)}++def read_glb_tris(path):+    b = open(path, "rb").read(); n = struct.unpack("<I", b[12:16])[0]+    j = json.loads(b[20:20 + n]); binc = b[20 + n + 8:]+    def acc(i, dt, w):+        a = j["accessors"][i]; v = j["bufferViews"][a["bufferView"]]+        return np.frombuffer(binc, dt, a["count"] * w, v["byteOffset"]).reshape(-1, w) if w > 1 else np.frombuffer(binc, dt, a["count"], v["byteOffset"])+    out = []+    for p in j["meshes"][0]["primitives"]:+        pos = acc(p["attributes"]["POSITION"], np.float32, 3) * 1000.0+        ia = j["accessors"][p["indices"]]+        idx = acc(p["indices"], np.uint16 if ia["componentType"] == 5123 else np.uint32, 1)+        col = tuple(j["materials"][p["material"]]["pbrMetallicRoughness"]["baseColorFactor"][:3])+        for t in idx.reshape(-1, 3):+            out.append((pos[t].astype(float), col))+    return out++ortho.VIEWS["iso"] = (tuple(np.array([-0.7071, 0.7071, 0])), tuple(np.array([-0.4082, -0.4082, 0.8165])), tuple(np.array([0.5774, 0.5774, 0.5774])))+def preview(path, png):+    tris = read_glb_tris(path)+    allp = np.concatenate([t[0] for t in tris]); ext = np.abs(allp).max()+    img = ortho.render(tris, "iso", 300 / (3.3 * ext), size=(400, 300))+    # painter's sorting by triangle centroid can bury faces in the top plane (and the marking 5 um above it);+    # faces in those planes cannot be hidden in a view from above, so draw them again, plane by plane, bottom first+    near = lambda c: any(max(abs(a - b) for a, b in zip(c, m)) < 2e-3 for m in MARK_COLS)+    zt = allp[:, 2].max()+    marked = [t for t in tris if MARK_COLS and near(t[1])]+    top = zt - 0.005 if marked else zt+    plane = [t for t in tris if not (MARK_COLS and near(t[1])) and np.abs(t[0][:, 2] - top).max() < 1e-4]+    for layer in (plane, [t for t in marked if t[0][:, 2].min() > top + 1e-4]):+        if layer:+            img.alpha_composite(ortho.render(layer, "iso", 300 / (3.3 * ext), size=(400, 300)))+    bg = Image.new("RGBA", img.size, (48, 57, 68, 255)); bg.alpha_composite(img); bg.convert("RGB").save(png)++sha = lambda f: hashlib.sha256(open(f, "rb").read()).hexdigest()+levels = []+src_tris = None+if plain_step:+    MARK_COLS = {c for _, c in ortho.load_tris(step)} - {c for _, c in ortho.load_tris(plain_step)}+    print("marking colours", MARK_COLS)+for name, lin, ang in LEVELS + [("super-low", None, None)]:+    if name == "super-low":+        tris = boxes_from(src_tris)+    else:+        tris = tris_at(lin, ang)+        if name == "high":+            src_tris = [t for t in tris if t[1] not in MARK_COLS]+        if name == "low" and MARK_COLS:+            tris = [t for t in tris if t[1] not in MARK_COLS]+    path = os.path.join(lod_dir, f"{name}.glb")+    st = write_glb(tris, path, mpn)+    back = read_glb_tris(path); P = np.concatenate([t[0] for t in back])+    png = os.path.join(lod_dir, f"{name}.png"); preview(path, png)+    rec = {"level": name, "path": f"lod/{name}.glb", "sha256": sha(path), "sizeBytes": os.path.getsize(path), **st,+           "bounds": {"min": (P.min(0) / 1000).round(7).tolist(), "max": (P.max(0) / 1000).round(7).tolist()},+           "approximation": ({"method": "OCCT BRepMesh_IncrementalMesh", "linearDeflectionMm": lin, "angularDeflectionRad": ang}+                             if lin else {"method": "per-material bounding boxes split at the part centre (terminals, body, overcoat)"}),+           "detailVisibility": ({"virtualMpn": False, "manufacturerMarking": name in ("high", "medium"),+                                 "note": "datasheet marking code shown on the top coat" if name in ("high", "medium") else "datasheet marking omitted: unreadable at this level"}+                                if MARK_COLS else {"virtualMpn": False, "note": "no marking: the datasheet specifies none"}),+           "review": "Candidate: rendered preview checked by AI (silhouette, terminals, seat z=0); not human-reviewed",+           "preview": {"path": f"lod/{name}.png", "sha256": sha(png), "sizeBytes": os.path.getsize(png), "width": 400, "height": 300,+                       "view": "iso", "sourceLevelSha256": sha(path)}}+    levels.append(rec)+    print(name, st, rec["bounds"])+manifest = {"schema": "adom/component-lod@1", "mpn": mpn, "upAxis": "Z", "units": "metres",+            "status": "candidate (AI-checked, not human-reviewed)",+            "purpose": "clean board-use component models: no footprint pads, outlines, labels or silkscreen; no marking (none per datasheet)",+            "source": {"path": src_label, "sha256": sha(step)},+            "sourceToDisplay": "uniform scale 0.001 (mm to m); no rotation, no translation; origin = footprint origin, seat z = 0",+            "generator": "lod.py (capencoder passives, OCCT via cadquery-ocp)",+            "removedHelpers": [], "levels": levels,+            "hints": ["All four levels share pose and units; super-low is boxes per material group, chosen for board overviews."]}+json.dump(manifest, open(os.path.join(lod_dir, "manifest.json"), "w"), indent=1)
provenance/tools/ortho.pyadded+148
@@ -0,0 +1,148 @@+"""Orthographic renderer for small component STEPs (face colours kept).++render(step, view, ppmm) -> PIL RGBA image, model origin at the image centre.+views: top (look down -Z, +X right, +Y up), bottom (look up +Z, +X right, +Y down,+i.e. the part flipped about X), front (look +Y, +X right, +Z up),+end (look -X, +Y right... from +X end: -Y right, +Z up).+"""+import sys+import numpy as np+from PIL import Image, ImageDraw+from OCP.STEPCAFControl import STEPCAFControl_Reader+from OCP.TDocStd import TDocStd_Document+from OCP.TCollection import TCollection_ExtendedString+from OCP.XCAFDoc import XCAFDoc_DocumentTool, XCAFDoc_ColorType+from OCP.TDF import TDF_LabelSequence+from OCP.TopExp import TopExp_Explorer+from OCP.TopAbs import TopAbs_FACE, TopAbs_REVERSED, TopAbs_SOLID+from OCP.TopoDS import TopoDS+from OCP.Quantity import Quantity_Color+from OCP.BRepMesh import BRepMesh_IncrementalMesh+from OCP.BRep import BRep_Tool+from OCP.TopLoc import TopLoc_Location+++def load_tris(path):+    doc = TDocStd_Document(TCollection_ExtendedString("x"))+    r = STEPCAFControl_Reader(); r.SetColorMode(True); r.SetNameMode(True)+    r.ReadFile(path); r.Transfer(doc)+    st = XCAFDoc_DocumentTool.ShapeTool_s(doc.Main())+    ct = XCAFDoc_DocumentTool.ColorTool_s(doc.Main())+    labs = TDF_LabelSequence(); st.GetFreeShapes(labs)+    out = []+    leaves = []+    def walk(lab, loc, pcol):+        c = Quantity_Color(); col = pcol+        for t in (XCAFDoc_ColorType.XCAFDoc_ColorSurf, XCAFDoc_ColorType.XCAFDoc_ColorGen):+            if ct.GetColor_s(lab, t, c):+                col = (c.Red(), c.Green(), c.Blue()); break+        if st.IsReference_s(lab):+            ref = type(lab)(); st.GetReferredShape_s(lab, ref)+            walk(ref, loc.Multiplied(st.GetLocation_s(lab)), col); return+        kids = TDF_LabelSequence(); st.GetComponents_s(lab, kids)+        if kids.Length() > 0:+            for i in range(1, kids.Length() + 1): walk(kids.Value(i), loc, col)+            return+        # colours stored on sub-shape labels (e.g. one solid per glyph of a text compound)+        sub = TDF_LabelSequence(); st.GetSubShapes_s(lab, sub)+        if col is None and sub.Length() > 0:+            # a part whose colours sit on its solids and faces (the OCCT writer moves a part colour onto+            # each solid): draw every solid once in its solid colour; face colours still win below+            subs = [(st.GetShape_s(sub.Value(si)), sub.Value(si)) for si in range(1, sub.Length() + 1)]+            if any(s.ShapeType() == TopAbs_SOLID for s, _ in subs) and all(s.ShapeType() in (TopAbs_SOLID, TopAbs_FACE) for s, _ in subs):+                whole = st.GetShape_s(lab); ex = TopExp_Explorer(whole, TopAbs_SOLID)+                while ex.More():+                    so = ex.Current(); scol = None+                    for s, sl in subs:+                        if s.ShapeType() == TopAbs_SOLID and s.IsSame(so):+                            sc = Quantity_Color()+                            for t in (XCAFDoc_ColorType.XCAFDoc_ColorSurf, XCAFDoc_ColorType.XCAFDoc_ColorGen):+                                if ct.GetColor_s(sl, t, sc):+                                    scol = (sc.Red(), sc.Green(), sc.Blue()); break+                    leaves.append((so.Moved(loc), scol)); ex.Next()+                return+            for si in range(1, sub.Length() + 1):+                sl = sub.Value(si); sc = Quantity_Color(); scol = None+                for t in (XCAFDoc_ColorType.XCAFDoc_ColorSurf, XCAFDoc_ColorType.XCAFDoc_ColorGen):+                    if ct.GetColor_s(sl, t, sc):+                        scol = (sc.Red(), sc.Green(), sc.Blue()); break+                leaves.append((st.GetShape_s(sl).Moved(loc), scol))+            return+        leaves.append((st.GetShape_s(lab).Moved(loc), col))+    for li in range(1, labs.Length() + 1):+        walk(labs.Value(li), TopLoc_Location(), None)+    for shp, pcol in leaves:+        BRepMesh_IncrementalMesh(shp, 0.002, False, 0.2, True)+        e = TopExp_Explorer(shp, TopAbs_FACE)+        while e.More():+            f = TopoDS.Face_s(e.Current())+            c = Quantity_Color(); col = None+            for t in (XCAFDoc_ColorType.XCAFDoc_ColorSurf, XCAFDoc_ColorType.XCAFDoc_ColorGen):+                if ct.GetColor(f, t, c):+                    col = (c.Red(), c.Green(), c.Blue()); break+            if col is None:+                col = pcol or (0.6, 0.6, 0.6)+            loc = TopLoc_Location()+            tri = BRep_Tool.Triangulation_s(f, loc)+            if tri is not None:+                trsf = loc.Transformation()+                pts = np.array([(lambda p: (p.X(), p.Y(), p.Z()))(tri.Node(i).Transformed(trsf)) for i in range(1, tri.NbNodes() + 1)])+                rev = f.Orientation() == TopAbs_REVERSED+                for i in range(1, tri.NbTriangles() + 1):+                    a, b, cc = tri.Triangle(i).Get()+                    if rev:+                        b, cc = cc, b+                    out.append((pts[[a - 1, b - 1, cc - 1]], col))+            e.Next()+    return out+++VIEWS = {+    # (right axis vector, up axis vector, toward-viewer vector)+    "top": ((1, 0, 0), (0, 1, 0), (0, 0, 1)),+    "bottom": ((1, 0, 0), (0, -1, 0), (0, 0, -1)),+    "front": ((1, 0, 0), (0, 0, 1), (0, -1, 0)),+    "end": ((0, 1, 0), (0, 0, 1), (1, 0, 0)),+}+++def render(tris, view, ppmm, size=None, shade=True, ss=3):+    R, U, V = [np.array(v, float) for v in VIEWS[view]]+    allp = np.concatenate([t[0] for t in tris])+    ext = max(np.abs(allp @ R).max(), np.abs(allp @ U).max())+    if size is None:+        size = (int(2 * ext * ppmm + 40), int(2 * ext * ppmm + 40))+    W, H = size[0] * ss, size[1] * ss+    img = Image.new("RGBA", (W, H), (0, 0, 0, 0))+    d = ImageDraw.Draw(img)+    items = []+    light = np.array([0.3, 0.4, 0.85]); light /= np.linalg.norm(light)+    for p, col in tris:+        n = np.cross(p[1] - p[0], p[2] - p[0])+        nn = np.linalg.norm(n)+        if nn == 0:+            continue+        n /= nn+        if n @ V <= 1e-6:+            continue  # back face+        depth = (p @ V).mean()+        lv = 0.55 + 0.45 * abs(n @ (R * light[0] + U * light[1] + V * light[2])) if shade else 1.0+        rgb = tuple(int(255 * min(1, c * lv)) for c in col)+        xy = [(W / 2 + (q @ R) * ppmm * ss, H / 2 - (q @ U) * ppmm * ss) for q in p]+        items.append((depth, xy, rgb))+    items.sort(key=lambda x: x[0])+    for _, xy, rgb in items:+        d.polygon(xy, fill=rgb + (255,), outline=rgb + (255,))+    return img.resize(size, Image.LANCZOS)+++if __name__ == "__main__":+    tris = load_tris(sys.argv[1])+    out = sys.argv[2]+    ppmm = float(sys.argv[3]) if len(sys.argv) > 3 else 300+    imgs = [render(tris, v, ppmm) for v in ("top", "bottom", "front", "end")]+    w = sum(i.width for i in imgs); h = max(i.height for i in imgs)+    sheet = Image.new("RGBA", (w, h), (255, 255, 255, 255)); x = 0+    for i in imgs:+        sheet.alpha_composite(i, (x, 0)); x += i.width+    sheet.save(out)
provenance/tools/overlay_chip.pyadded+159
@@ -0,0 +1,159 @@+"""Prove a two-terminal chip model (MLCC / chip resistor) against its datasheet table.++The maker prints a dimension table with an unscaled sketch, so the overlay is made+against a true-scale drawing generated from the printed table (300 px/mm):+nominal outline solid black, tolerance limits dashed blue, the allowed range of+the terminal inner edge shaded amber.  The model is rendered at the same scale+(50 % fill, red outline) on top.  The numeric check measures the STEP itself.++usage: overlay_chip.py spec.json model.step out.png+spec: {"label","mpn","source","L":[nom,tol],"W":[nom,tol],"T":[nom,tol],+       (any of L/W/T may instead be {"nom":x or null,"min":a,"max":b} when the+        datasheet prints an asymmetric or max-only limit; a null nominal draws+        the printed maximum as the reference line),+       "band_top":[min,max],"band_bot":[min,max],"crops":[[png,[x0,y0,x1,y1]],...],+       "marking": "..."}+"""+import sys, json+import numpy as np+from PIL import Image, ImageDraw, ImageFont, ImageFilter+sys.path.insert(0, __file__.rsplit("/", 1)[0])+from ortho import load_tris, render+from OCP.STEPControl import STEPControl_Reader+from OCP.TopExp import TopExp_Explorer+from OCP.TopAbs import TopAbs_FACE+from OCP.TopoDS import TopoDS+from OCP.Bnd import Bnd_Box+from OCP.BRepBndLib import BRepBndLib+from OCP.BRepAdaptor import BRepAdaptor_Surface+from OCP.GeomAbs import GeomAbs_Plane++PP = 300.0+spec = json.load(open(sys.argv[1])); step = sys.argv[2]; out = sys.argv[3]+PP = float(spec.get("ppmm", PP))  # larger packages (0805) need a smaller scale to fit the 560 px panels+F = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"+font = ImageFont.truetype(F, 22); fsm = ImageFont.truetype(F, 18); fbig = ImageFont.truetype(F, 26)++# ---------- numeric measurement of the STEP ----------+tris = load_tris(step)+allp = np.concatenate([t[0] for t in tris])+x0, y0, z0 = allp.min(0); x1, y1, z1 = allp.max(0)+Lm, Wm, Tm = x1 - x0, y1 - y0, z1 - z0+# terminal colour = colour of triangles lying on the +X end face+endcols = {}+for p, c in tris:+    if np.allclose(p[:, 0], x1, atol=1e-4):+        endcols[c] = endcols.get(c, 0) + 1+tcol = max(endcols, key=endcols.get)+def band(zlev):+    xs = [p[:, 0] for p, c in tris if c == tcol and np.allclose(p[:, 2], zlev, atol=1e-4)]+    if not xs:+        return None+    xs = np.concatenate(xs)+    pos = xs[xs > 0]; neg = xs[xs < 0]+    return round(min(x1 - pos.min(), -x0 + neg.max() if len(neg) else 9), 3)+bt, bb = band(z1), band(z0)++def rng(v):+    if isinstance(v, dict):+        return (v["min"], v["max"])+    return (v[0] - v[1], v[0] + v[1]) if len(v) == 2 and v[1] < v[0] else tuple(v)+checks = []+def chk(name, val, lo, hi):+    ok = val is not None and lo - 1e-6 <= val <= hi + 1e-6+    checks.append((name, val, lo, hi, ok))+chk("L", round(Lm, 3), *rng(spec["L"]))+chk("W", round(Wm, 3), *rng(spec["W"]))+chk("T/H", round(Tm, 3), *rng(spec["T"]))+chk(spec.get("band_top_name", "terminal band, top"), bt, *spec["band_top"])+chk(spec.get("band_bot_name", "terminal band, bottom"), bb, *spec["band_bot"])+centred = bool(abs(x0 + x1) < 1e-3 and abs(y0 + y1) < 1e-3 and abs(z0) < 1e-3)+longx = bool(Lm > Wm)++# ---------- true-scale drawing + model per view ----------+def nlh(v):+    if isinstance(v, dict):+        lo, hi = v["min"], v["max"]+        return (v["nom"] if v.get("nom") is not None else hi), lo, hi+    return v[0], v[0] - v[1], v[0] + v[1]++def view_panel(view, A, B, bands, title):+    a_len, a_lo, a_hi = nlh(A); b_len, b_lo, b_hi = nlh(B)+    im = render(tris, view, PP, size=(560, 420))+    W_, H_ = im.size; cx = W_ / 2+    panel = Image.new("RGBA", (W_, H_), (255, 255, 255, 255))+    d = ImageDraw.Draw(panel)+    seat = view in ("front", "end")+    def rect(a, b, **kw):+        # a along horizontal, b along vertical; for side views the box sits on the seat line+        if seat:+            return [cx - a * PP / 2, H_ / 2 - b * PP, cx + a * PP / 2, H_ / 2]+        return [cx - a * PP / 2, H_ / 2 - b * PP / 2, cx + a * PP / 2, H_ / 2 + b * PP / 2]+    # allowed inner-edge range for the terminals (relative to nominal length)+    for (lo, hi, where) in bands:+        for s in (-1, 1):+            xa = cx + s * (a_len / 2 - hi) * PP; xb = cx + s * (a_len / 2 - lo) * PP+            if where == "full":+                ya, yb = rect(a_len, b_len)[1], rect(a_len, b_len)[3]+            elif where == "top":+                r_ = rect(a_len, b_len); ya, yb = r_[1] - 12, r_[1] + 12+            else:+                r_ = rect(a_len, b_len); ya, yb = r_[3] - 12, r_[3] + 12+            d.rectangle([min(xa, xb), ya, max(xa, xb), yb], fill=(255, 196, 80, 255))+    def dashed(box, col):+        xa, ya, xb, yb = box+        for (p, q) in (((xa, ya), (xb, ya)), ((xb, ya), (xb, yb)), ((xb, yb), (xa, yb)), ((xa, yb), (xa, ya))):+            n = int(max(abs(q[0] - p[0]), abs(q[1] - p[1])) / 8) or 1+            for i in range(0, n, 2):+                t0, t1 = i / n, min(1, (i + 1) / n)+                d.line([(p[0] + (q[0] - p[0]) * t0, p[1] + (q[1] - p[1]) * t0),+                        (p[0] + (q[0] - p[0]) * t1, p[1] + (q[1] - p[1]) * t1)], fill=col, width=2)+    dashed(rect(a_lo, b_lo), (30, 90, 220))+    dashed(rect(a_hi, b_hi), (30, 90, 220))+    d.rectangle(rect(a_len, b_len), outline=(0, 0, 0), width=2)+    arr = np.asarray(im).copy(); alpha = arr[:, :, 3].copy(); arr[:, :, 3] = (alpha * 0.5).astype(np.uint8)+    panel.alpha_composite(Image.fromarray(arr))+    m = Image.fromarray(((alpha > 128) * 255).astype(np.uint8))+    edge = (np.asarray(m, int) - np.asarray(m.filter(ImageFilter.MinFilter(3)), int)) > 0+    red = np.zeros((H_, W_, 4), np.uint8); red[edge] = (220, 30, 30, 255)+    panel.alpha_composite(Image.fromarray(red))+    d = ImageDraw.Draw(panel); d.text((10, 8), title, fill=(0, 0, 0), font=fsm)+    return panel++L, W, T = spec["L"], spec["W"], spec["T"]+bt_r, bb_r = spec["band_top"], spec["band_bot"]+panels = [+    view_panel("top", L, W, [(bt_r[0], bt_r[1], "full")], "TOP (looking down, X right)"),+    view_panel("bottom", L, W, [(bb_r[0], bb_r[1], "full")], "BOTTOM (flipped about X)"),+    view_panel("front", L, T, [(bt_r[0], bt_r[1], "top"), (bb_r[0], bb_r[1], "bot")], "SIDE (looking +Y)"),+    view_panel("end", W, T, [], "END (looking -X)"),+]+pw, ph = panels[0].size+sheet_w = pw * 4 + 50+crops = [Image.open(p).convert("RGB").crop(tuple(b)) for p, b in spec["crops"]]+crop_h = 0; scaled = []+for c in crops:+    s = min(1.0, (sheet_w - 40) / c.width, 420 / c.height)+    c2 = c.resize((int(c.width * s), int(c.height * s)), Image.LANCZOS); scaled.append(c2); crop_h += c2.height + 20+text_h = 120 + 30 * len(checks) + 90+sheet = Image.new("RGB", (sheet_w, 80 + ph + text_h + crop_h), (255, 255, 255))+d = ImageDraw.Draw(sheet)+d.text((20, 15), f"{spec['label']}: model vs datasheet table, true scale {int(PP)} px/mm", fill=(0, 0, 0), font=fbig)+d.text((20, 50), f"Model: {step.rsplit('/', 1)[-1]}   Datasheet: {spec['source']}", fill=(70, 70, 70), font=fsm)+for i, p in enumerate(panels):+    sheet.paste(p.convert("RGB"), (10 + i * (pw + 10), 80))+y = 90 + ph+d.text((20, y), "Legend: black = printed nominal, blue dashed = printed tolerance limits, amber = allowed position of the terminal inner edge, red outline + 50% fill = the 3D model.", fill=(0, 0, 0), font=fsm); y += 34+d.text((20, y), "Measured on the STEP              value     datasheet range     result", fill=(0, 0, 0), font=font); y += 32+for name, val, lo, hi, ok in checks:+    d.text((20, y), f"{name:<34}{'-' if val is None else f'{val:.3f}':>8}     {lo:.2f} to {hi:.2f}       {'PASS' if ok else 'FAIL'}",+           fill=(0, 120, 40) if ok else (200, 0, 0), font=font); y += 30+d.text((20, y + 4), f"Frame: Z-up, seated z=0, centred: {'yes' if centred else 'NO'}; long axis on X: {'yes' if longx else 'NO'}.   Marking: {spec['marking']}", fill=(0, 0, 0), font=fsm)+y += 50+for c in scaled:+    sheet.paste(c, (20, y)); d.rectangle([20, y, 20 + c.width, y + c.height], outline=(150, 150, 150)); y += c.height + 20+sheet.save(out)+res = {"step": step, "bbox_mm": [round(float(v), 4) for v in (x0, x1, y0, y1, z0, z1)],+       "checks": [{"dim": n, "model_mm": v, "datasheet_min": lo, "datasheet_max": hi, "pass": bool(ok)} for n, v, lo, hi, ok in checks],+       "z_up_seated_centred": bool(centred), "long_axis_x": bool(longx), "all_pass": bool(all(c[4] for c in checks) and centred and longx)}+print(json.dumps(res))
provenance/tools/parts.pyadded+53
@@ -0,0 +1,53 @@+"""Catalog data for the KEMET C1812C124J1GACTU page (all values from the cited sources).++Sources: KEMET datasheet C1003_C0G (2/20/2025), the KEMET product page, KEMET's STEP for this part+number (plain HTTP GET, no browser), KiCad's Capacitor_SMD library. Distributor numbers and stock:+adom-parts-search, 2026-10-21.+"""+TODAY = "2026-10-21"+ROOT = "/home/adom/project/uta-dynos/work/pages/_build"+PRODUCT = "https://yageogroup.com/en/us/capacitors/ceramic/mlccs-smd/C1812C124J1GACTU.html"  # kemet.com product URL redirects here+PRODUCT_KEMET = "https://www.kemet.com/en/us/capacitors/ceramic/mlccs-smd/C1812C124J1GACTU.html"+KSIM = "https://ksim3.kemet.com/capacitor-simulation?pn=C1812C124J1GACTU"+STEP_URL = "https://search.kemet.com/component-documentation/download/step/C1812C124J1GACTU.step"+DS_URL = "https://content.kemet.com/datasheets/KEM_C1003_C0G_SMD.pdf"+DS_URL_ALT = "https://yageogroup.com/content/datasheet/asset/file/KEM_C1003_C0G_SMD"+DS_PUB = "docs/KEMET-C1003-C0G-2025-02-20.pdf"+DS_TITLE = "KEMET, Surface Mount Multilayer Ceramic Chip Capacitors (SMD MLCCs), C0G Dielectric, 10 to 250 VDC (Commercial Grade), C1003_C0G, 2/20/2025 (22 pages)"+DOCS = [("ds", "kemet/KEMET-C1003-C0G-2025-02-20.pdf", DS_PUB, DS_TITLE, DS_URL)]+# (file, doc, page, crop box in 100 dpi pixels, caption); crops are cut from 150 dpi renders+FIGS = [+    ("kemet-ds-p1-overview-ordering.png", "ds", 1, [40, 120, 810, 1010], "p.1 overview (no change in capacitance with time and voltage, +/-30 ppm/C) and ordering information (C 1812 C 124 J 1 G A C TU)"),+    ("kemet-ds-p2-benefits-packaging.png", "ds", 2, [40, 100, 810, 540], "p.2 packaging C-spec (TU = 7 inch reel, unmarked) and benefits (no capacitance change with applied rated DC voltage, no decay with time)"),+    ("kemet-ds-p3-dimensions.png", "ds", 3, [40, 100, 810, 790], "p.3 dimensions: 1812 L 4.50 +/-0.30, W 3.20 +/-0.30, B 0.60 +/-0.35, S 2.30 min (mm)"),+    ("kemet-ds-p4-electrical.png", "ds", 4, [40, 100, 810, 690], "p.4 electrical parameters: -55 to +125 C, +/-30 ppm/C, 0 % aging, DWV 250 %, DF 0.1 %, IR"),+    ("kemet-ds-p8-waterfall-1812.png", "ds", 8, [40, 100, 810, 720], "p.8 Table 1B: 0.12 uF (124) in C1812C at 100 V (voltage code 1) has chip thickness code GH"),+    ("kemet-ds-p10-thickness.png", "ds", 10, [40, 100, 610, 960], "p.10 Table 2A: thickness code GH = 1.40 +/-0.15 mm; 1,000 pcs per 7 inch plastic reel"),+    ("kemet-ds-p12-land-pattern.png", "ds", 12, [40, 100, 810, 900], "p.12 Table 3: IPC-7351 land pattern for 1812 at density levels A, B and C"),+    ("kemet-ds-p13-soldering.png", "ds", 13, [40, 100, 810, 730], "p.13 soldering: reflow only for 1812, profile for 100 % matte Sn (260 C peak)"),+    ("kemet-ds-p15-marking.png", "ds", 15, [40, 100, 810, 780], "p.15 construction and marking: C0G devices are supplied unmarked only"),+]+P = dict(+    slug="c1812c124j1gactu", mpn="C1812C124J1GACTU", mfr="KEMET", mfr_full="KEMET Electronics Corporation (YAGEO Group)",+    source_slug="kemet", kind="C", category="Capacitor", subcategory="MLCC, Class 1 (C0G / NP0), commercial grade",+    value_sym="120n", value="0.12 uF (120 nF)", tol="+/-5 % (J)", package="1812 (4532 metric)",+    fp="C_1812_4532Metric", fp_lib="Capacitor_SMD", body_mm={"x_mm": 4.5, "y_mm": 3.2, "z_mm": 1.4},+    brief="MLCC 0.12 uF +/-5 %, 100 V, C0G/NP0, 1812 (4532 metric)",+    product_url=PRODUCT,+    dims_text="datasheet C1003_C0G p.3 (1812: L 4.50 +/-0.30, W 3.20 +/-0.30, B 0.60 +/-0.35, S 2.30 min) and p.8 + p.10 (0.12 uF, 100 V: thickness code GH, T 1.40 +/-0.15 mm)",+    overlay={"L": [4.5, 0.3], "W": [3.2, 0.3], "T": [1.4, 0.15], "band_top": [0.25, 0.95], "band_bot": [0.25, 0.95],+             "band_top_name": "B (band width), top", "band_bot_name": "B (band width), bottom", "ppmm": 100,+             "source": "KEMET C1003_C0G 2/20/2025 p.3 (1812) and p.10 (thickness code GH)"},+    land={"source": "KEMET C1003_C0G 2/20/2025 p.12, Table 3, 1812, density level B (median, nominal): C 2.05 (centre to pad centre), Y 1.40, X 3.50, V1 6.00, V2 4.00 mm",+          "kind": "nominal", "gap_a": 2.70, "len_b": 1.40, "wid_c": 3.50, "overall": 5.50,+          "letters": "gap = 2C - Y, land length = Y, land width = X, overall = 2C + Y (KEMET's letters C, Y, X)",+          "levels": {"A": {"C": 2.15, "Y": 1.60, "X": 3.60, "V1": 6.90, "V2": 4.60},+                     "B": {"C": 2.05, "Y": 1.40, "X": 3.50, "V1": 6.00, "V2": 4.00},+                     "C": {"C": 1.95, "Y": 1.20, "X": 3.40, "V1": 5.30, "V2": 3.70}}},+    marking="KEMET supplies C0G devices unmarked only (datasheet p.15), and the ordering code TU means 7 inch reel, unmarked (p.2)",+    marking_src="KEMET C1003_C0G 2/20/2025 p.2 and p.15",+    dist={"mouser": ("80-C1812C124J1G", "https://www.mouser.com/ProductDetail/KEMET/C1812C124J1GACTU?qs=Fn3VdHBzkcnCFZmhvW0Uag%3D%3D", 1701, "$2.98 (1), $1.79 (100), lead time 266 days"),+          "digikey": ("399-C1812C124J1GACTUTR-ND", "https://www.digikey.com/en/products/detail/kemet/C1812C124J1GACTU/1874760", 1761, "$3.53 (1)")},+    lcsc_note="JLCPCB/LCSC search for C1812C124J1GACTU returned no part on 2026-10-21",+)+PARTS = [P]
provenance/tools/push.shadded+34
@@ -0,0 +1,34 @@+#!/usr/bin/env bash+# usage: push.sh <slug>   (pushes /home/adom/project/uta-dynos/work/pages/<slug> to adom/<slug>)+set -u+slug="$1"; dir="/home/adom/project/uta-dynos/work/pages/$slug"+cd "$dir" || exit 1+mapfile -t files < <(find . -type f ! -name page.json -printf '%s %P\n' | sort -k2)+batch=(); size=0; n=0+push() {+  local tries=0+  while :; do+    out=$(adom-wiki repo push "adom/$slug" --files "$@" -m "$MSG" 2>&1); rc=$?+    if echo "$out" | grep -q -E '429|RATE_LIMITED'; then+      w=$(echo "$out" | grep -oE '"retry_after":[0-9]+' | grep -oE '[0-9]+' | head -1); w=${w:-20}+      echo "rate limited, waiting $w s"; sleep "$w"; tries=$((tries+1)); [ $tries -gt 5 ] && return 1; continue+    fi+    echo "$out" | tail -4; return $rc+  done+}+flush() {+  [ ${#batch[@]} -eq 0 ] && return 0+  n=$((n+1)); MSG="Component record for $slug: files batch $n"+  echo "== batch $n: ${#batch[@]} files, $size bytes"+  push "${batch[@]}" || { echo "PUSH FAILED"; exit 1; }+  batch=(); size=0; sleep 3+}+for line in "${files[@]}"; do+  s=${line%% *}; f=${line#* }+  if [ $((size + s)) -gt 3300000 ]; then flush; fi+  batch+=("$f"); size=$((size + s))+done+flush+MSG="Component record for $slug: page.json (last, keeps hero and title)"+echo "== page.json"+push page.json || { echo "PUSH FAILED"; exit 1; }
provenance/tools/records.pyadded+454
@@ -0,0 +1,454 @@+"""Write README.md, land-pattern/README.md, page.json, package.json, models.json, provenance.json,+verification.json and LICENSE for the C1812C124J1GACTU page.++usage: records.py <slug>   (run after build.py, again after finish.py)+"""+import sys, os, json, hashlib, re+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))+from parts import P, TODAY, ROOT, DOCS, PRODUCT, PRODUCT_KEMET, KSIM, STEP_URL, DS_URL, DS_URL_ALT++slug = sys.argv[1]; assert slug == P["slug"]+OUT = f"/home/adom/project/uta-dynos/work/pages/{slug}"+MPN = P["mpn"]; OWNER = "adom"+B = json.load(open(os.path.join(ROOT, "work", f"{slug}-build.json")))+STEP, ORIG, FP, SYM = B["step"], B["orig"], B["fp"], B["sym"]+check = B["check"]+land = json.load(open(os.path.join(OUT, "land-pattern/land-check.json")))+lod = json.load(open(os.path.join(OUT, "lod/manifest.json")))+figs = json.load(open(os.path.join(OUT, "docs/figures/figures.json")))+repose = json.load(open(os.path.join(OUT, f"provenance/{STEP}.json")))+p_ = lambda f: os.path.join(OUT, f)+ex = lambda f: os.path.exists(p_(f))+sha = lambda f: 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()+D = P["dist"]; L = P["land"]; LV = L["levels"]+ds = dict(zip(("key", "local", "file", "title", "url"), DOCS[0]))+by = {"name": "Claude (AI agent) for John Lauer", "handle": "john"}+HERO = f"{slug}-hero.glb"; HERO_SIDE = f"{slug}-hero.overlay.json"; HERO_PNG = "docs/verify/hero-preview.png"+LBR = f"{MPN}.lbr"; SCH = f"altium/{MPN}.SchLib"; PCB = f"altium/{MPN}.PcbLib"; HUB = f"{MPN}.adom-lbr.json"+REPOSE = f"provenance/{STEP}.json"+have_hero, have_alt, have_lbr = ex(HERO), ex(SCH) and ex(PCB), ex(LBR)+GL = "https://gitlab.com/kicad/libraries"+F = lambda n: f"docs/figures/{n}"+k = land["kicad"]+band = next(c for c in check["checks"] if c["dim"].startswith("B (band width), top"))["model_mm"]++# ---------------- land pattern write-up ----------------+t = "| Dimension | KiCad C_1812_4532Metric (used) | KEMET density level B | Difference |\n|---|---|---|---|\n"+for r in land["rows"]:+    t += f"| {r['dim']} | {r['kicad_mm']:.2f} mm | {r['maker_nominal']:.2f} mm | {r['difference_mm']:+.2f} mm |\n"+lv = "| Level | C (centre to land centre) | Y (land length) | X (land width) | V1 x V2 (courtyard) |\n|---|---|---|---|---|\n"+for n, name in (("A", "A, maximum protrusion"), ("B", "B, median (nominal)"), ("C", "C, minimum protrusion")):+    v = LV[n]; lv += f"| {name} | {v['C']:.2f} | {v['Y']:.2f} | {v['X']:.2f} | {v['V1']:.2f} x {v['V2']:.2f} |\n"+land_readme = f"""# Land pattern check: {MPN}++The footprint on this page is KiCad's standard library footprint `{P['fp_lib']}:{P['fp']}`, renamed to+`{FP[:-10]}` so it can sit beside other parts in one library folder. The copper is unchanged; only the name and+the 3D model path (`${{KIPRJMOD}}/{STEP}`) differ from upstream. This page compares it with the land pattern KEMET+recommends for 1812 parts in datasheet C1003_C0G (2/20/2025), page 12, Table 3 (IPC-7351).++![KiCad pads (teal) over KEMET's density level B land (amber dashed), true scale](land-pattern.png)++## KEMET's three density levels for 1812 (mm)++{lv}+KEMET's drawing (p.12) gives C from the part centre to each land centre, so the gap between the lands is 2C - Y and the+overall length is 2C + Y. KEMET describes level B as "a robust solder attachment condition for reflow solder processes".++## KiCad pads against level B++{t}+## Result++KiCad's footprint is IPC-7351 nominal, the same rule as KEMET's level B: land centres, land length, gap and overall length+are identical. The only difference is the land width: KiCad's pads are {k['wid']:.2f} mm wide, KEMET's level B lands+{LV['B']['X']:.2f} mm (0.10 mm narrower, 0.05 mm per side; level C uses 3.40 mm). The KiCad pads have rounded corners+(roundrect ratio 0.18); KEMET draws plain rectangles. KiCad's courtyard is 6.00 x 3.90 mm, KEMET's V1 x V2 6.00 x 4.00 mm.+Both cover the full printed body width (3.20 +/-0.30 mm). This page keeps the KiCad pads. A design that wants KEMET's+level B exactly should widen each pad to {LV['B']['X']:.2f} mm.++KEMET lists the 1812 case as reflow only (datasheet p.3 and p.13).++The drawing and [land-check.json](land-check.json) come from `provenance/tools/landcheck.py`.+"""+open(p_("land-pattern/README.md"), "w").write(land_readme)++# ---------------- verification.json ----------------+entries = []+ck = [{"text": f"{c['dim']}: model {c['model_mm']:.3f} mm, KEMET {c['datasheet_min']:.2f} to {c['datasheet_max']:.2f} mm", "result": "pass" if c["pass"] else "fail"} for c in check["checks"]]+ck += [{"text": "thickness code for this exact part: 0.12 uF (124) at 100 V (voltage code 1) in C1812C is GH (Table 1B, p.8), T = 1.40 +/-0.15 mm (Table 2A, p.10)", "result": "pass"},+       {"text": "KEMET's model is drawn at the top of every printed range: L 4.80 (max), W 3.50 (max), T 1.55 (GH max); its file name is CCS_CCSTD_1812_L4.8W3.5T1.55B0.6", "result": "note"},+       {"text": f"band measured on the top face: {band:.3f} mm from the end (KEMET's file name says B0.6); inside B 0.60 +/-0.35 mm", "result": "note"},+       {"text": "re-posed from Y-up to Z-up by one rigid rotation and translation; package solid volume unchanged (23.90241 mm3), 72 face colours copied", "result": "pass"},+       {"text": "three 5 um KEMET logo glyph solids dropped from the board model: KEMET supplies C0G parts unmarked only (p.15); the original download is kept unchanged under vendor/", "result": "pass"}]+entries.append({"date": TODAY, "by": by, "status": "verified", "title": "KEMET 3D model checked against the printed dimensions for thickness code GH",+                "scope": ["3d-model"], "variants": ["*"], "checks": ck,+                "evidence": [{"label": "overlay (top, bottom, side, end at true scale)", "href": f"docs/verify/{slug}-overlay.png"},+                             {"label": "numbers", "href": f"docs/verify/{slug}-check.json"},+                             {"label": "re-pose record", "href": REPOSE},+                             {"label": "datasheet p.3 dimensions", "href": F("kemet-ds-p3-dimensions.png")},+                             {"label": "datasheet p.8 thickness code", "href": F("kemet-ds-p8-waterfall-1812.png")},+                             {"label": "datasheet p.10 thickness range", "href": F("kemet-ds-p10-thickness.png")}],+                "files": {STEP: sha(STEP), ORIG: sha(ORIG)}})+lck = [{"text": f"{r['dim']}: KiCad {r['kicad_mm']:.2f} mm, KEMET level B {r['maker_nominal']:.2f} mm ({r['difference_mm']:+.2f} mm)", "result": "note"} for r in land["rows"]]+lck.append({"text": "KiCad's IPC-7351 nominal footprint matches KEMET's level B except the land width (3.40 vs 3.50 mm); KiCad footprint kept (renamed, copper unchanged)", "result": "pass"})+lck.append({"text": "footprint names its 3D model: (model \"${KIPRJMOD}/" + STEP + "\")", "result": "pass"})+entries.append({"date": TODAY, "by": by, "status": "verified", "title": "Footprint compared with KEMET's recommended land pattern (Table 3, level B)",+                "scope": ["footprint"], "variants": ["*"], "checks": lck,+                "evidence": [{"label": "comparison drawing", "href": "land-pattern/land-pattern.png"}, {"label": "numbers", "href": "land-pattern/land-check.json"},+                             {"label": "write-up", "href": "land-pattern/README.md"}, {"label": "datasheet p.12", "href": F("kemet-ds-p12-land-pattern.png")}],+                "files": {FP: sha(FP)}})+entries.append({"date": TODAY, "by": by, "status": "verified", "title": "Marking: the real part is unmarked",+                "scope": ["3d-model", "marking"], "variants": ["*"],+                "checks": [{"text": f"{P['marking']} ({P['marking_src']})", "result": "pass"},+                           {"text": "no AI-added part-number etch on any model; KEMET's CAD logo removed from the board model, kept in the vendor original", "result": "pass"}],+                "evidence": [{"label": "datasheet p.15", "href": F("kemet-ds-p15-marking.png")}, {"label": "datasheet p.2", "href": F("kemet-ds-p2-benefits-packaging.png")}],+                "files": {STEP: sha(STEP)}})+entries.append({"date": TODAY, "by": by, "status": "verified", "title": "Board-use LOD GLBs are clean and share the STEP pose",+                "scope": ["lod"], "variants": ["*"],+                "checks": [{"text": "four levels written, read back, bounds equal the STEP (Z-up, metres, seat z = 0)", "result": "pass"},+                           {"text": "no footprint, pad outline, label or silkscreen helpers; terminals present at every level", "result": "pass"},+                           {"text": "previews looked at by the AI agent, not yet by a person", "result": "note"}],+                "evidence": [{"label": "manifest", "href": "lod/manifest.json"}], "files": {lv_["path"]: lv_["sha256"] for lv_ in lod["levels"]}})+entries.append({"date": TODAY, "by": by, "status": "verified", "title": "Pin 1, polarity and symbol",+                "scope": ["footprint", "3d-model", "symbol"], "variants": ["*"],+                "checks": [{"text": "non-polar two-terminal capacitor (datasheet p.2): pads 1 and 2 are interchangeable; symbol pins 1 and 2 map to pads 1 and 2", "result": "pass"},+                           {"text": "symbol pin ends at (0, +2.54) and (0, -2.54) mm, on the 2.54 mm grid", "result": "pass"},+                           {"text": "symbol fields MPN, Manufacturer, Mouser, DigiKey, Adom Wiki, Datasheet and Footprint filled; LCSC present but empty because JLCPCB/LCSC list no part (" + TODAY + ")", "result": "note"},+                           {"text": "model terminals sit on the footprint pads (both centred on the origin, long axis on X)", "result": "pass"}],+                "evidence": [], "files": {FP: sha(FP), SYM: sha(SYM), STEP: sha(STEP)}})+if have_hero:+    entries.append({"date": TODAY, "by": by, "status": "verified", "title": "Hero GLB built to the adom-hero standard",+                    "scope": ["3d-model", "hero"], "variants": ["*"],+                    "checks": [{"text": "built with adom/component-hero hero_glb.py (unmodified) from package-reference.glb, the footprint and page.json; alignment check OK", "result": "pass"},+                               {"text": "teal dashed pad outlines (alpha 0.5), silkscreen (alpha 0.3) and pad-number labels 1 and 2 (alpha 0.5); no pin-1 dot because the part is non-polar", "result": "pass"},+                               {"text": "preview looked at by the AI agent; the live wiki viewer still needs a person's look", "result": "note"}],+                    "evidence": [{"label": "preview", "href": HERO_PNG}, {"label": "overlay record", "href": HERO_SIDE}],+                    "files": {HERO: sha(HERO), "package-reference.glb": sha("package-reference.glb")}})+if have_alt or have_lbr:+    entries.append({"date": TODAY, "by": by, "status": "verified", "title": "Fusion and Altium libraries generated from the KiCad symbol and footprint",+                    "scope": ["symbol", "footprint"], "variants": ["*"],+                    "checks": [{"text": "adom-lbr generate --part-metadata page.json: .lbr lint passed", "result": "pass"},+                               {"text": "adom-lbr import-kicad then assemble-altium --no-dedupe: native .SchLib and .PcbLib written, STEP body embedded", "result": "pass"},+                               {"text": "not yet opened in Fusion or Altium (no desktop app used for this page)", "result": "note"}],+                    "evidence": [], "files": {f: sha(f) for f in (LBR, SCH, PCB, HUB) if ex(f)}})+json.dump({"schema": "adom/verification@1", "part": MPN, "variants": [], "entries": entries}, open(p_("verification.json"), "w"), indent=1)++# ---------------- models.json ----------------+SRC = f"KEMET 3D model (STEP) for {MPN}, {STEP_URL}"+models = [+    {"path": STEP, "format": "step", "title": "Package model (STEP, KEMET, Z-up)", "status": "current", "slot": "step", "variant": "plain", "role": "manufacturer",+     "use": ["board-cad"], "marking": None, "derived_from": ORIG,+     "unique": "The board CAD model: KEMET's package solid re-posed to Z-up, seated on z = 0 and centred; KEMET's CAD logo glyphs dropped (the real part is unmarked)",+     "source": SRC, "sha256": sha(STEP), "git_blob": blob(STEP), "bytes": os.path.getsize(p_(STEP)), "thumb": "docs/models/step-plain.png"},+    {"path": ORIG, "format": "step", "title": "KEMET original download (STEP)", "status": "current", "variant": "original", "role": "manufacturer",+     "use": ["archive"], "marking": {"text": "KEMET", "surface": "top of the ceramic", "colour": "SolidWorks default lavender (no colour set by KEMET)"},+     "unique": "The file exactly as downloaded: Y-up, corner at the origin, with KEMET's logo as three 5 um solids on top",+     "source": SRC, "sha256": sha(ORIG), "git_blob": blob(ORIG), "bytes": os.path.getsize(p_(ORIG))},+    {"path": "package-reference.glb", "format": "glb", "title": "Package model (GLB)", "status": "current", "slot": "model_3d_plain", "variant": "plain",+     "role": "derived", "use": ["render", "board-insertion"], "marking": None, "derived_from": STEP,+     "unique": "Clean board-use model for viewers (step2glb service), no footprint artwork", "source": SRC,+     "sha256": sha("package-reference.glb"), "git_blob": blob("package-reference.glb"), "bytes": os.path.getsize(p_("package-reference.glb"))},+]+if have_hero:+    models.append({"path": HERO, "format": "glb", "title": "Hero model (GLB)", "status": "current", "slot": "model_3d", "variant": "hero", "role": "derived",+                   "use": ["wiki-hero"], "marking": None, "derived_from": "package-reference.glb",+                   "unique": "The page's 3D view: the package plus teal dashed pad outlines, silkscreen and pad-number labels from the footprint (adom-hero standard)",+                   "source": "adom/component-hero hero_glb.py", "sha256": sha(HERO), "git_blob": blob(HERO), "bytes": os.path.getsize(p_(HERO)), "thumb": HERO_PNG})+for lv_ in lod["levels"]:+    models.append({"path": lv_["path"], "format": "glb", "title": f"Board LOD, {lv_['level']}", "status": "current", "variant": "lod", "role": "derived",+                   "use": ["board-insertion", "render"], "marking": None, "derived_from": STEP,+                   "unique": f"{lv_['level']} detail for board and scaffold viewers: {lv_['triangles']} triangles, {lv_['primitives']} primitives",+                   "thumb": lv_["preview"]["path"], "source": SRC, "sha256": lv_["sha256"], "git_blob": blob(lv_["path"]), "bytes": lv_["sizeBytes"]})+json.dump({"schema": "adom/models@1", "part": MPN, "models": models, "updated": TODAY}, open(p_("models.json"), "w"), indent=1)++# ---------------- LICENSE ----------------+open(p_("LICENSE"), "w").write(f"""{MPN} component page++Original Adom content on this page (README text, the KiCad symbol {SYM}, the verification+drawings, the JSON records and the scripts in provenance/tools/) is licensed under the Creative+Commons Attribution 4.0 International License (CC-BY-4.0): https://creativecommons.org/licenses/by/4.0/++Third-party files keep their own terms and are not relicensed by this page:+- {ORIG} is KEMET's 3D model for this part, downloaded from KEMET, under KEMET's terms.+  {STEP} and every model derived from it (package-reference.glb, {HERO}, lod/*.glb, the body+  inside altium/{MPN}.PcbLib) are re-posed derivatives of that file and keep KEMET's terms.+- {FP} is KiCad library content, CC-BY-SA 4.0 with the KiCad library exception+  ({GL}/kicad-footprints); the footprint inside {LBR} and altium/{MPN}.PcbLib keeps that license.+- {ds['file']} and the page crops in docs/figures/ are KEMET documents, copyright of KEMET+  Electronics Corporation (YAGEO Group), included unchanged (or cropped) for reference.+""")++# ---------------- README ----------------+spec_rows = [+    ("Capacitance", "0.12 uF (120 nF) +/-5 % (code 124, J)", "p.1 ordering information"),+    ("Rated voltage", "100 V DC (voltage code 1)", "p.1"),+    ("Dielectric", "C0G (NP0), EIA Class I, temperature compensating", "p.1"),+    ("Temperature coefficient", "0 +/-30 ppm/C from -55 to +125 C, referenced to +25 C and 0 V DC", "p.1, p.4"),+    ("Voltage coefficient", "no capacitance change with applied rated DC voltage", "p.1, p.2"),+    ("Aging", "0 % (maximum capacitance loss per decade hour); no capacitance decay with time", "p.2, p.4"),+    ("Dissipation factor", "0.1 % max at 25 C (1 kHz, 1.0 Vrms for parts above 1,000 pF)", "p.4"),+    ("Insulation resistance", "1,000 MOhm x uF or 100 GOhm, whichever is lower: about 8.3 GOhm min for 0.12 uF (100 V applied, 25 C)", "p.4"),+    ("Dielectric withstanding voltage", "250 % of rated voltage (250 V) for 5 +/-1 s, charge current up to 50 mA", "p.4"),+    ("After life, humidity and moisture tests", "capacitance shift 0.3 % or +/-0.25 pF max, DF 0.5 % max, IR 10 % of initial limit", "p.4"),+    ("Operating temperature", "-55 to +125 C", "p.4"),+    ("Size", "L 4.50 +/-0.30, W 3.20 +/-0.30, T 1.40 +/-0.15 (thickness code GH), B 0.60 +/-0.35, S 2.30 min (mm)", "p.3, p.8, p.10"),+    ("Termination", "100 % matte tin over nickel barrier over copper (termination code C)", "p.1, p.15"),+    ("Packaging (TU)", "7 inch reel, unmarked; embossed plastic tape 12 mm wide, 8 mm pitch; 1,000 pieces per reel", "p.2, p.10, p.16"),+    ("Soldering", "reflow only for 1812; matte Sn profile peaks at 260 C; up to three reflow passes", "p.3, p.13"),+    ("Compliance", "lead (Pb) free, RoHS and REACH compliant without exemptions", "p.1, p.2"),+    ("Marking", "none: C0G parts are supplied unmarked only", "p.2, p.15"),+]+spec_tbl = "| Parameter | Value | Datasheet page |\n|---|---|---|\n" + "".join(f"| {a} | {b} | {c} |\n" for a, b, c in spec_rows)+dist_tbl = ("| Distributor | Part number | Stock seen " + TODAY + " | Price seen |\n|---|---|---|---|\n"+            + "".join(f"| {n} | [{D[kk][0]}]({D[kk][1]}) | {D[kk][2]:,} | {D[kk][3]} |\n" for kk, n in (("mouser", "Mouser"), ("digikey", "DigiKey")))+            + "| LCSC / JLCPCB | not listed | - | - |\n")+fig_tbl = "| Figure | Source |\n|---|---|\n" + "".join(f"| [{f['caption']}]({f['file']}) | KEMET C1003_C0G, p.{f['page']} |\n" for f in figs)+cad_rows = [(f"`{SYM}`", "KiCad symbol (standard capacitor graphic, pin ends on the 2.54 mm grid) with MPN, Manufacturer, Mouser, DigiKey, LCSC (empty) and Adom Wiki fields"),+            (f"`{FP}`", f"KiCad footprint (KiCad library `{P['fp']}`, renamed; model `${{KIPRJMOD}}/{STEP}`)")]+if have_lbr: cad_rows.append((f"`{LBR}`", "Fusion / EAGLE library made with `adom-lbr generate`"))+if have_alt: cad_rows.append((f"`{SCH}`, `{PCB}`", f"Altium symbol and footprint libraries (native binary) made with `adom-lbr import-kicad` and `assemble-altium` (the PcbLib embeds the STEP body); `{HUB}` is the adom-lbr hub record"))+cad_rows += [(f"`{STEP}`", "3D model, STEP: KEMET's package solid, Z-up, seated and centred"), (f"`{ORIG}`", "KEMET's STEP exactly as downloaded"),+             ("`package-reference.glb`", "3D model, GLB (clean, for board viewers)")]+if have_hero: cad_rows.append((f"`{HERO}`", "the page's 3D view: package plus footprint pad outlines, silkscreen and pad labels"))+cad_rows += [("`lod/`", "board-use GLBs in four levels of detail, with previews and manifest")]+cad_tbl = "| File | What it is |\n|---|---|\n" + "".join(f"| {a} | {b} |\n" for a, b in cad_rows)+res_tbl = ("| Resource | Link |\n|---|---|\n"+           f"| Product page | {PRODUCT} (the KEMET address {PRODUCT_KEMET} redirects there) |\n"+           f"| Datasheet C1003_C0G, 2/20/2025 | [PDF on this page]({ds['file']}) ([KEMET]({DS_URL}), [YAGEO]({DS_URL_ALT})) |\n"+           f"| 3D model (STEP) | [{STEP_URL}]({STEP_URL}) (copy under `vendor/`) |\n"+           f"| KEMET K-SIM simulation (impedance, ESR, temperature rise) | {KSIM} |\n"+           "| Flexible termination version | KEMET product bulletin C1062_C0G_FT-CAP_SMD (datasheet p.1 note 1) |\n")+hero_para = (f"""+The page viewer shows `{HERO}`: the clean model plus nonphysical reference artwork drawn under it to the adom-hero+standard (adom/component-hero): the footprint's pads as teal dashed outlines at 50 % opacity, its silkscreen at 70 %+transparency and the pad numbers 1 and 2. The part is non-polar, so no pin-1 dot is added. The artwork never goes into+the STEP, `package-reference.glb` or the board LODs.++![Hero preview, top and oblique]({HERO_PNG})+""" if have_hero else "")+readme = f"""# {MPN}++KEMET {MPN}: {P['brief']}. A commercial grade, class I multilayer ceramic chip capacitor from KEMET's C0G series+(KEMET is part of the YAGEO Group). C0G holds its capacitance against DC voltage, temperature and time, which makes it the+choice for timing, tuning, filters, snubbers and other circuits where the value must not move.++::adom-verified{{src="/{OWNER}/{slug}/render/verification.json"}}++| Field | Value |+|---|---|+| Manufacturer | {P['mfr_full']} |+| Manufacturer part number | {MPN} |+| Type | {P['subcategory']} |+| Capacitance | {P['value']}, {P['tol']} |+| Rated voltage | 100 V DC |+| Dielectric | C0G (NP0): 0 +/-30 ppm/C, -55 to +125 C |+| Package | {P['package']}, 4.50 x 3.20 x 1.40 mm (thickness code GH) |+| Marking | none (C0G parts are supplied unmarked) |+| Datasheet | [{ds['title']}]({ds['file']}) |++## Why C0G: stable with voltage, temperature and time++The figures below are KEMET's, from the datasheet on this page:++- **Voltage.** "No capacitance change with respect to applied rated DC voltage" (p.2; the overview on p.1 says C0G+  "exhibits no change in capacitance with respect to time and voltage"). At 100 V this part is still 0.12 uF.+- **Temperature.** Capacitance change is limited to +/-30 ppm/C from -55 to +125 C, referenced to +25 C (p.1, p.4).+  Worked out from that limit: at most +/-0.3 % at +125 C and +/-0.24 % at -55 C.+- **Time.** Aging rate 0 % per decade hour (p.4), "no capacitance decay with time" (p.2).+- **No piezoelectric noise**, low ESR and ESL and high ripple current capability are listed as benefits (p.1).++By contrast, a class II X7R capacitor is allowed +/-15 % over -55 to +125 C (the EIA X7R definition), loses part of its+capacitance under DC bias, and ages over time; at a 100 V rating that bias loss can be large. That is the reason to+choose this C0G part over an X7R of the same value where the capacitance has to stay put: the cost is size, since 0.12 uF+at 100 V needs an 1812 case in C0G.++![Datasheet p.2: benefits and packaging codes](docs/figures/kemet-ds-p2-benefits-packaging.png)++![Datasheet p.4: electrical parameters](docs/figures/kemet-ds-p4-electrical.png)++## Key specifications++{spec_tbl}+Page numbers refer to [KEMET C1003_C0G, 2/20/2025]({ds['file']}).++## Ordering++{dist_tbl}+Stock and prices are a snapshot from the Adom parts search on {TODAY}; check the distributor before ordering. DigiKey lists+the part as Active. {P['lcsc_note']}.++## Part number++C ceramic | 1812 case size | C standard series | 124 = 12 x 10^4 pF = 0.12 uF | J +/-5 % | 1 = 100 V | G = C0G |+A failure rate not applicable | C 100 % matte Sn termination | TU 7 inch reel, unmarked (datasheet p.1 and p.2). The+thickness is not in the part number: Table 1B (p.8) gives code GH for 0.12 uF at 100 V in 1812, and Table 2A (p.10)+gives GH = 1.40 +/-0.15 mm.++![Datasheet p.1: overview and ordering information](docs/figures/kemet-ds-p1-overview-ordering.png)++## Manufacturer and resources++{res_tbl}+## Pins++| Pin | Function |+|---|---|+| 1 | terminal (non-polar) |+| 2 | terminal (non-polar) |++A ceramic capacitor has no polarity: either terminal may face either way (datasheet p.2, "non-polar device").++## Land pattern: KiCad footprint vs KEMET++<table>+<tr>+<td width="104"><a href="land-pattern/README.md"><img src="land-pattern/land-pattern-icon.png" width="112" alt="Land pattern"></a><br><sub>&nbsp;&nbsp;&nbsp;&nbsp;Land&nbsp;pattern&nbsp;&nbsp;&nbsp;&nbsp;</sub></td>+<td>The footprint is KiCad's standard `{P['fp']}`, and this guide lays its pads over KEMET's recommended IPC-7351 land (datasheet p.12, density level B) at true scale. Land centres, length and gap are identical; KiCad's pads are 0.10 mm narrower (3.40 vs 3.50 mm) and have rounded corners. Use it to decide whether to keep the KiCad pads or widen them to KEMET's level B before layout.<br><br><a href="land-pattern/README.md">The comparison, with KEMET's three density levels</a></td>+</tr>+</table>++## 3D model++The model is **KEMET's own STEP** for this part number, downloaded by plain HTTP from KEMET's component documentation+service. KEMET's file (CCS_CCSTD_1812_L4.8W3.5T1.55B0.6) is Y-up with a corner at the origin; `{STEP}` is the same+package solid moved by one rigid transform (rotate +90 degrees about X, then translate) so it is Z-up, seated on z = 0 and+centred on the footprint origin, with its face colours copied over and its volume unchanged. KEMET's file also carries a+"KEMET" logo as three 5 um thick solids on top of the ceramic, coloured only with SolidWorks' default lavender. The real+part has no marking (KEMET supplies C0G parts unmarked only, p.15), so the board model leaves the logo out. The download+itself is kept unchanged as `{ORIG}`, and every step is recorded in [`{REPOSE}`]({REPOSE}).++The model is drawn at the top of every printed range: 4.80 x 3.50 x 1.55 mm against L 4.50 +/-0.30, W 3.20 +/-0.30 and,+for this part's thickness code GH, T 1.40 +/-0.15 mm. Its terminal band measures {band:.2f} mm on the top face, inside+B 0.60 +/-0.35 mm. A real part will usually be smaller than the model in all three directions.+{hero_para}+::adom-models{{src="/{OWNER}/{slug}/render/models.json"}}++## Model checked against the printed dimensions++The model was rendered in orthographic top, bottom, side and end views at true scale and laid over a drawing made at the same+scale from the printed limits (nominal outline, tolerance limits, allowed terminal band). Every dimension is inside the printed+range: {", ".join(f"{c['dim']} {c['model_mm']:.3f} mm ({c['datasheet_min']:.2f} to {c['datasheet_max']:.2f})" for c in check['checks'])}.++![Model over the printed dimensions](docs/verify/{slug}-overlay.png)++Source: {P['dims_text']}.++## Board levels of detail++Clean board-use GLBs (no footprint pads, outlines, labels or silkscreen) in four levels, all in the same Z-up, metre,+footprint-origin pose as the STEP. Previews are rendered from each GLB.++::adom-component-lod{{src="/{OWNER}/{slug}/render/lod/manifest.json"}}++## CAD files++{cad_tbl}+## Datasheet figures++The pages this record cites, cropped from the datasheet:++{fig_tbl}+## Sources and license++- KEMET datasheet C1003_C0G, 2/20/2025 ({DS_URL}), sha256 {sha(ds['file'])}+- KEMET 3D model: {STEP_URL}, sha256 {sha(ORIG)}, {TODAY}+- KiCad library footprint: [{P['fp_lib']}.pretty/{P['fp']}.kicad_mod]({GL}/kicad-footprints/-/blob/master/{P['fp_lib']}.pretty/{P['fp']}.kicad_mod)+- Distributor numbers, stock and prices: Mouser, DigiKey and JLCPCB/LCSC through the Adom parts search, {TODAY}++Original Adom content is CC-BY-4.0. KEMET's STEP, its derivatives and the datasheet keep KEMET's terms and the KiCad+footprint keeps its upstream license (CC-BY-SA 4.0 with the KiCad library exception); see [LICENSE](LICENSE).+[provenance.json](provenance.json) records every source and file hash; the scripts that built the checks are in+`provenance/tools/`.++This is a catalog record for the part, not a manufacturing approval: check KEMET's current specification for your design.+"""+readme = re.sub(r"\n{3,}", "\n\n", readme)+for bad in ("—", "–"):+    assert bad not in readme and bad not in land_readme, "dash in README"+open(p_("README.md"), "w").write(readme)++# ---------------- page.json / package.json ----------------+title = f"{MPN} - {P['mfr']}"+parts_ = {"symbol": SYM, "footprint": FP, "step": STEP, "model_3d": HERO if have_hero else "package-reference.glb",+          "model_3d_plain": "package-reference.glb", "datasheet": ds["file"]}+if have_lbr: parts_["lbr"] = LBR+if have_alt: parts_.update({"altium_schlib": SCH, "altium_pcblib": PCB})+component = {+    "mpn": MPN, "manufacturer": P["mfr"], "package": P["package"], "category": P["category"], "subcategory": P["subcategory"],+    "pin_count": 2, "body_size": P["body_mm"], "value": P["value"], "tolerance": P["tol"], "rating": "100 V DC",+    "dielectric": "C0G / NP0 (0 +/-30 ppm/C, -55 to +125 C; no change with DC voltage; 0 % aging)",+    "thickness_code": "GH (1.40 +/-0.15 mm)",+    "pins": [{"number": "1", "name": "~"}, {"number": "2", "name": "~"}],+    "parts": parts_,+    "manufacturer_links": {"product": PRODUCT, "datasheet_pdf": DS_URL, "step": STEP_URL, "simulation": KSIM},+    "distributor_links": {"mouser": {"part": D["mouser"][0], "url": D["mouser"][1]}, "digikey": {"part": D["digikey"][0], "url": D["digikey"][1]}, "lcsc": False},+    "lcsc_note": P["lcsc_note"],+    "stock": {"date": TODAY, "mouser": D["mouser"][2], "digikey": D["digikey"][2], "source": "adom-parts-search"},+    "marking": {"text": None, "statement": P["marking"], "source": P["marking_src"]},+    "provenance": {"model_3d": "KEMET STEP for this part number, re-posed to Z-up, CAD logo glyphs dropped, checked against the printed dimensions for thickness code GH",+                   "footprint": "KiCad standard library C_1812_4532Metric, renamed", "land_pattern_check": "land-pattern/README.md (matches KEMET level B except land width 3.40 vs 3.50 mm)",+                   "omitted": ["KEMET logo glyph solids from the board model (real part is unmarked; original kept in vendor/)"]},+}+eda = {"kicad": {"symbol": SYM, "footprint": FP, "model": STEP, "provenance": "generated"}}+if have_lbr: eda["fusion"] = {"lbr": LBR, "provenance": "generated"}+if have_alt: eda["altium"] = {"schlib": SCH, "pcblib": PCB, "provenance": "generated"}+page = {"slug": slug, "title": title, "eda_assets": eda, "type": "hardware", "kind": "component", "version": "0.1.0",+        "description": P["brief"], "brief": P["brief"],+        "tags": ["hardware", "component", "passive", "capacitor", "mlcc", "1812", "c0g", "np0", "100v", "kemet"],+        "discovery_triggers": [MPN, MPN.lower(), "80-C1812C124J1G", "0.12 uF 100 V 1812 C0G", "120nF 100V C0G 1812 capacitor", "KEMET C0G 1812"],+        "dependencies": {}, "license": "CC-BY-4.0", "hero": {"path": "docs/hero.png", "chrome": False},+        "component": component, "visibility": {"public": True}, "source": {"tool": "kemet", "url": PRODUCT}}+json.dump(page, open(p_("page.json"), "w"), indent=1)+json.dump({"name": slug, "title": title, "version": "0.1.0", "description": P["brief"], "type": "hardware", "kind": "component",+           "license": "CC-BY-4.0", "files": []}, open(p_("package.json"), "w"), indent=1)++# ---------------- provenance.json ----------------+files = {}+for dp, dn, fn in os.walk(OUT):+    for f in fn:+        rel = os.path.relpath(os.path.join(dp, f), OUT)+        if rel != "provenance.json":+            files[rel] = sha(rel)+finp = os.path.join(ROOT, "work", f"{slug}-finish.json")+fin = json.load(open(finp)) if os.path.exists(finp) else {}+prov = {+    "manufacturer_reference": PRODUCT,+    "datasheet": {"file": ds["file"], "sha256": sha(ds["file"]), "url": DS_URL, "same_bytes_at": DS_URL_ALT, "title": ds["title"], "revision": "C1003_C0G 2/20/2025", "checked": TODAY},+    "specs_read_from_datasheet": {"capacitance": "0.12 uF +/-5 % (124, J), p.1", "rated_voltage": "100 V (code 1), p.1", "dielectric": "C0G, p.1",+                                  "tcc": "+/-30 ppm/C, -55 to +125 C, p.1 and p.4", "voltage": "no capacitance change with applied rated DC voltage, p.2",+                                  "aging": "0 %, p.4", "df": "0.1 %, p.4", "ir": "1,000 MOhm uF or 100 GOhm, p.4", "dwv": "250 % rated, p.4",+                                  "dimensions_mm": {"L": "4.50 +/-0.30", "W": "3.20 +/-0.30", "T": "1.40 +/-0.15 (GH, p.8 and p.10)", "B": "0.60 +/-0.35", "S_min": "2.30"},+                                  "packaging": "TU: 7 inch reel, unmarked, 1,000 pcs plastic, 12 mm tape, 8 mm pitch (p.2, p.10, p.16)",+                                  "marking": "C0G supplied unmarked only, p.15", "soldering": "reflow only (1812), p.3 and p.13"},+    "symbol_source": "Original, written for this page (standard capacitor graphic, non-polar, pin ends on the 2.54 mm grid). CC-BY-4.0.",+    "footprint_source": f"KiCad standard library {P['fp_lib']}:{P['fp']} (CC-BY-SA 4.0 with the KiCad library exception); renamed {FP[:-10]}, model path ${{KIPRJMOD}}/{STEP}; copper unchanged",+    "footprint_upstream_url": f"{GL}/kicad-footprints/-/raw/master/{P['fp_lib']}.pretty/{P['fp']}.kicad_mod",+    "footprint_upstream_sha256": hashlib.sha256(open(os.path.join(ROOT, "dl/kicad", P["fp"] + ".kicad_mod"), "rb").read()).hexdigest(),+    "manufacturer_step": {"found": True, "url": STEP_URL,+                          "also_served_at": "https://connect.kemet.com:7667/gateway/IntelliData-ComponentDocumentation/1.0/download/step/C1812C124J1GACTU (same bytes)",+                          "method": "plain HTTP GET, no browser, no session", "retrieved": TODAY, "file": ORIG, "sha256": sha(ORIG),+                          "header_name": "CCS_CCSTD_1812_L4.8W3.5T1.55B0.6.STEP", "authoring": "SolidWorks 2021, 2024-09-06",+                          "units": "mm", "up_axis": "+Y", "repose": REPOSE},+    "board_step": {"file": STEP, "sha256": sha(STEP), "transform": repose["matrix_3x4"], "dropped": repose["dropped"], "kept": repose["kept"]},+    "geometry_status": "Manufacturer-issued CAD for this part number, every printed dimension checked for thickness code GH (docs/verify).",+    "marking": {"real_marking": None, "statement": P["marking"], "source": P["marking_src"]},+    "frame": {"units": "mm (STEP), metres (GLB)", "up_axis": "Z", "seat_z": 0, "origin": "footprint origin"},+    "glb": {"tool": "step2glb convert (service)", "source": STEP, "exit": 0},+    "hero_image": {"tool": "step2glb thumbnail --width 1600 --height 1000 (iso, 16:10)", "source": STEP, "path": "docs/hero.png"},+    "hero_glb": fin.get("hero"), "fusion_lbr": fin.get("lbr"), "altium": fin.get("altium"),+    "document_figures": figs,+    "datasheet_check": {"overlay": f"docs/verify/{slug}-overlay.png", "numbers": f"docs/verify/{slug}-check.json", "result": "PASS" if check["all_pass"] else "FAIL"},+    "land_pattern_check": {"readme": "land-pattern/README.md", "numbers": "land-pattern/land-check.json", "source": L["source"], "levels": LV,+                           "result": "KiCad matches KEMET level B except land width (3.40 vs 3.50 mm); KiCad kept"},+    "distributors": {"tool": "adom-parts-search search " + MPN, "date": TODAY,+                     "mouser": {"part": D["mouser"][0], "url": D["mouser"][1], "stock": D["mouser"][2], "price": D["mouser"][3]},+                     "digikey": {"part": D["digikey"][0], "url": D["digikey"][1], "stock": D["digikey"][2], "price": D["digikey"][3]},+                     "lcsc": None, "lcsc_note": P["lcsc_note"]},+    "lod": {"manifest": "lod/manifest.json", "generator": "provenance/tools/lod.py", "source": STEP, "source_sha256": sha(STEP)},+    "tools_note": "Scripts adapted from adom/grm188r72a103ka01d and adom/msash168sc7224ktca01; the hero generator is adom/component-hero hero_glb.py, unmodified.",+    "files": files, "files_note": "provenance.json itself is not listed.", "built_by": "Claude (AI agent) for John Lauer", "date": TODAY,+}+json.dump(prov, open(p_("provenance.json"), "w"), indent=1)+print("records written", slug, "hero" if have_hero else "-", "altium" if have_alt else "-", "lbr" if have_lbr else "-")
provenance/tools/repose_step.pyadded+117
@@ -0,0 +1,117 @@+"""Re-pose KEMET's 1812 STEP into the Adom component frame; keep the package solid, drop the logo glyphs.++KEMET's file (CCS_CCSTD_1812_L4.8W3.5T1.55B0.6, SolidWorks 2021) is Y-up with one corner at the origin:+length on +X (0 to 4.8), thickness on +Y (0 to 1.55), width on -Z (-3.5 to 0). It holds four solids:+the package (one solid, 72 faces, every face coloured: terminals grey, ceramic brown) and three thin+solids (5 um thick) spelling "KEMET" on the top of the ceramic, which carry only SolidWorks' default+lavender. KEMET supplies C0G parts unmarked only (datasheet C1003_C0G p.15), so the logo is CAD+branding, not a marking on the real part: the board-use model keeps the package solid only. The+original download is kept unchanged beside it.++The Adom frame is Z-up, mm, seated on z = 0, centred on the footprint origin, long axis on X.+One rigid transform (rotate +90 deg about X, so y -> z and z -> -y, then translate) is applied with+BRepBuilderAPI_Transform and every face colour of the package is copied onto the moved face.+Nothing is scaled; the package solid's volume and faces are checked unchanged.++usage: repose_step.py in.step out.step   (prints one JSON line)+"""+import sys, json, hashlib, math+from OCP.STEPCAFControl import STEPCAFControl_Reader, STEPCAFControl_Writer+from OCP.TDocStd import TDocStd_Document+from OCP.TCollection import TCollection_ExtendedString+from OCP.XCAFDoc import XCAFDoc_DocumentTool, XCAFDoc_ColorType+from OCP.Quantity import Quantity_Color+from OCP.TDF import TDF_LabelSequence+from OCP.IFSelect import IFSelect_RetDone+from OCP.STEPControl import STEPControl_AsIs+from OCP.gp import gp_Trsf, gp_Ax1, gp_Pnt, gp_Dir, gp_Vec+from OCP.Bnd import Bnd_Box+from OCP.BRepBndLib import BRepBndLib+from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform+from OCP.TDataStd import TDataStd_Name+from OCP.Interface import Interface_Static+from OCP.TopExp import TopExp_Explorer+from OCP.TopAbs import TopAbs_SOLID, TopAbs_FACE+from OCP.GProp import GProp_GProps+from OCP.BRepGProp import BRepGProp++src, dst = sys.argv[1], sys.argv[2]+SURF = XCAFDoc_ColorType.XCAFDoc_ColorSurf+doc = TDocStd_Document(TCollection_ExtendedString("XmlOcaf"))+r = STEPCAFControl_Reader(); r.SetColorMode(True); r.SetNameMode(True)+assert r.ReadFile(src) == IFSelect_RetDone+r.Transfer(doc)+st = XCAFDoc_DocumentTool.ShapeTool_s(doc.Main()); ct = XCAFDoc_DocumentTool.ColorTool_s(doc.Main())+free = TDF_LabelSequence(); st.GetFreeShapes(free)+assert free.Length() == 1, free.Length()+lab = free.Value(1)+assert not st.IsAssembly_s(lab)+shp = st.GetShape_s(lab)+bb = lambda s: (lambda b: (BRepBndLib.Add_s(s, b, False), b.Get())[1])(Bnd_Box())+x0, y0, z0, x1, y1, z1 = bb(shp)++def vol(s):+    g = GProp_GProps(); BRepGProp.VolumeProperties_s(s, g); return g.Mass()+solids = []+ex = TopExp_Explorer(shp, TopAbs_SOLID)+while ex.More():+    solids.append(ex.Current()); ex.Next()+solids.sort(key=vol, reverse=True)+body, glyphs = solids[0], solids[1:]+def nfaces(s):+    n = 0; e = TopExp_Explorer(s, TopAbs_FACE)+    while e.More(): n += 1; e.Next()+    return n+dropped = [{"faces": nfaces(g), "volume_mm3": round(vol(g), 5), "bbox_mm": [round(v, 4) for v in bb(g)],+            "thickness_mm": round(bb(g)[4] - bb(g)[1], 4)} for g in glyphs]+assert all(d["thickness_mm"] < 0.01 for d in dropped), dropped++# face colours of the package solid+subs = TDF_LabelSequence(); st.GetSubShapes_s(lab, subs)+face_cols = []+for i in range(1, subs.Length() + 1):+    sl = subs.Value(i); s = st.GetShape_s(sl)+    c = Quantity_Color()+    if s.ShapeType() == TopAbs_FACE and ct.GetColor_s(sl, SURF, c):+        face_cols.append((s, c))+body_faces = []+e = TopExp_Explorer(body, TopAbs_FACE)+while e.More(): body_faces.append(e.Current()); e.Next()+coloured = [(f, next((c for s, c in face_cols if s.IsSame(f)), None)) for f in body_faces]+assert all(c is not None for _, c in coloured), "package face without colour"++rot = gp_Trsf(); rot.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(1, 0, 0)), math.pi / 2)+tr = gp_Trsf(); tr.SetTranslation(gp_Vec(-(x0 + x1) / 2, (z0 + z1) / 2, -y0))+tot = tr.Multiplied(rot)+mv = BRepBuilderAPI_Transform(body, tot, True)+new = mv.Shape()++doc2 = TDocStd_Document(TCollection_ExtendedString("XmlOcaf"))+st2 = XCAFDoc_DocumentTool.ShapeTool_s(doc2.Main()); ct2 = XCAFDoc_DocumentTool.ColorTool_s(doc2.Main())+nl = st2.AddShape(new, False)+TDataStd_Name.Set_s(nl, TCollection_ExtendedString("C1812C124J1GACTU"))+for f, c in coloured:+    sl = st2.AddSubShape(nl, mv.ModifiedShape(f))+    assert not sl.IsNull(), "moved face not found"+    ct2.SetColor(sl, c, SURF)++Interface_Static.SetCVal_s("write.step.schema", "AP214IS")+Interface_Static.SetCVal_s("write.step.unit", "MM")+w = STEPCAFControl_Writer(); w.SetColorMode(True); w.SetNameMode(True)+w.Transfer(doc2, STEPControl_AsIs)+assert w.Write(dst) == IFSelect_RetDone++nb = bb(new)+sha = lambda f: hashlib.sha256(open(f, "rb").read()).hexdigest()+m = [[round(tot.Value(i, j), 6) for j in range(1, 5)] for i in range(1, 4)]+cols = sorted({(round(c.Red(), 3), round(c.Green(), 3), round(c.Blue(), 3)) for _, c in coloured})+print(json.dumps({"source": src.rsplit("/", 1)[-1], "source_sha256": sha(src), "source_bbox_mm": [round(v, 4) for v in (x0, y0, z0, x1, y1, z1)],+                  "source_units": "mm", "source_up_axis": "+Y", "source_solids": len(solids),+                  "kept": {"solid": "package (terminals and ceramic)", "faces": len(body_faces), "volume_mm3": round(vol(body), 5),+                           "volume_after_mm3": round(vol(new), 5), "face_colours_srgb": cols},+                  "dropped": {"what": "KEMET logo glyph solids on the ceramic top (SolidWorks default lavender, no own colour); the real C0G part is unmarked (C1003_C0G p.15)",+                              "solids": dropped},+                  "rotation": "+90 deg about X (y -> z, z -> -y)",+                  "translation_mm": [round(-(x0 + x1) / 2, 4), round((z0 + z1) / 2, 4), round(-y0, 4)], "matrix_3x4": m,+                  "output": dst.rsplit("/", 1)[-1], "output_sha256": sha(dst),+                  "output_bbox_mm": [round(v, 4) for v in nb], "LWT_mm": [round(nb[3] - nb[0], 4), round(nb[4] - nb[1], 4), round(nb[5] - nb[2], 4)]}))
provenance/tools/verify_live.pyadded+35
@@ -0,0 +1,35 @@+"""Check over HTTP that every file page.json declares (component.parts, eda_assets, hero) and every+file in the local page tree is served by the wiki, plus the viewer routes.+usage: verify_live.py <slug>"""+import sys, os, json, urllib.request, urllib.parse, hashlib+slug = sys.argv[1]; D = f"/home/adom/project/uta-dynos/work/pages/{slug}"+W = "https://wiki.adom.inc"+p = json.load(open(os.path.join(D, "page.json")))+declared = set(p["component"]["parts"].values())+for v in p.get("eda_assets", {}).values():+    declared |= {x for k, x in v.items() if k != "provenance"}+declared.add(p["hero"]["path"])+declared |= {"verification.json", "models.json", "lod/manifest.json", "README.md", "page.json"}+local = {os.path.relpath(os.path.join(a, f), D) for a, _, fs in os.walk(D) for f in fs}+def get(url):+    try:+        with urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": "adom-verify"}), timeout=60) as r:+            return r.status, r.read()+    except urllib.error.HTTPError as e:+        return e.code, b""+    except Exception as e:+        return str(e), b""+bad = []+for f in sorted(declared | local):+    st, body = get(f"{W}/api/pages/adom/{slug}/files/" + urllib.parse.quote(f))+    same = os.path.exists(os.path.join(D, f)) and hashlib.sha256(body).hexdigest() == hashlib.sha256(open(os.path.join(D, f), "rb").read()).hexdigest()+    ok = st == 200 and len(body) > 0 and (same or f == "page.json")+    if not ok or f in declared:+        print(("OK  " if ok else "BAD ") + f"{st} {len(body):>8} {'same' if same else 'DIFF'} {f}{'  [declared]' if f in declared else ''}")+    if not ok: bad.append(f)+for r in ("viewer/3d", "viewer/symbol", "viewer/footprint"):+    st, body = get(f"{W}/{r}/adom/component/{slug}")+    print(f"route {r}: {st} {len(body)} bytes"); +    if st != 200 or len(body) < 500: bad.append(r)+st, body = get(f"{W}/adom/{slug}"); print("page:", st, len(body))+print(json.dumps({"slug": slug, "checked": len(declared | local), "bad": bad}))