← Commit history
Cargo.lock+10−10
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-aiflow"-version = "0.1.38"+version = "0.1.39" dependencies = [  "aiflow-analyze",  "aiflow-board",@@ -24,7 +24,7 @@ dependencies = [  [[package]] name = "aiflow-analyze"-version = "0.1.38"+version = "0.1.39" dependencies = [  "serde",  "serde_json",@@ -33,7 +33,7 @@ dependencies = [  [[package]] name = "aiflow-board"-version = "0.1.38"+version = "0.1.39" dependencies = [  "serde",  "serde_json",@@ -41,7 +41,7 @@ dependencies = [  [[package]] name = "aiflow-bridge"-version = "0.1.38"+version = "0.1.39" dependencies = [  "aiflow-board",  "serde",@@ -50,7 +50,7 @@ dependencies = [  [[package]] name = "aiflow-copper"-version = "0.1.38"+version = "0.1.39" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -60,7 +60,7 @@ dependencies = [  [[package]] name = "aiflow-grid"-version = "0.1.38"+version = "0.1.39" dependencies = [  "aiflow-board",  "serde",@@ -69,7 +69,7 @@ dependencies = [  [[package]] name = "aiflow-place"-version = "0.1.38"+version = "0.1.39" dependencies = [  "aiflow-board",  "serde",@@ -78,7 +78,7 @@ dependencies = [  [[package]] name = "aiflow-pours"-version = "0.1.38"+version = "0.1.39" dependencies = [  "aiflow-board",  "aiflow-copper",@@ -88,7 +88,7 @@ dependencies = [  [[package]] name = "aiflow-router"-version = "0.1.38"+version = "0.1.39" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -98,7 +98,7 @@ dependencies = [  [[package]] name = "aiflow-run"-version = "0.1.38"+version = "0.1.39" dependencies = [  "serde",  "serde_json",
Cargo.toml+1−1
@@ -14,7 +14,7 @@ members = [ ]  [workspace.package]-version = "0.1.38"+version = "0.1.39" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
SKILL.md+2−2
@@ -136,13 +136,13 @@ Publish missing identified component pages with verified symbol/footprint/model  Global component pages serve ALL ADOM USERS. Publish only distinct reusable manufacturer parts or independently specified reusable custom components, after duplicate checking. Keep page titles and assets part-focused and portable. Board reference mappings, unresolved identities, one-off footprints, bare copper features and project-only assemblies belong in the board project wiki page, not new component catalog entries. Do not turn project-specific placeholders into generic-looking pages merely by renaming them. -## Early component quality and optional MPN marking+## Early component quality and MPN marking  Before placement, declare `step components` and run `components` with your `--run` and `--ai-thread`. It writes `components.json`, inventories every board reference and preserves review notes on rerun. This is an AI review register, not an automated geometry or licensing certification. Record wiki links, classification, dimensions, terminals, pin-1/polarity, plain/marked variants, top/bottom/oblique visual evidence and redistribution evidence. Unknowns remain unknown.  Use manufacturer drawings and permitted CAD as reference checks. Third-party downloads may be private reference-only; never redistribute them without permission, or call extracted/recolored/etched geometry independently authored. Compare independently generated geometry with dimensional and visual evidence. Publish only reusable assets on global pages; board-only definitions stay in the project. -Offer `components --etch on` or `--etch off` (initial default `ask` is not consent). Discover adom/adom-chip-laser and the current STEP engine. Keep the plain model; cache a separate MPN-marked STEP, GLB and reviewed thumbnails when enabled, with input hash, generator version/parameters, text/mode and native-render evidence. Protect pin-1, polarity and functional features. Marking is an identification aid, not a claim of real factory markings. Do not regenerate a valid shared cache, and do not claim KiCad/Fusion/Altium parity until each named viewer was checked.+MPN marking is on by default for ICs: every IC's model carries its laser-etched MPN and `finish` checks it (`components --etch off` declines for a run). Mark with `adom-aiflow-etch --in <model.step> --mpn <MPN> --out <model.step>`: it uses adom/adom-chip-laser but keeps the model's own colours (the service alone repaints the part black and grey) and lays the text along the part's longest top-face direction, standing Y-up vendor models upright first (`--up y`). Use `--mark dark` on light bodies. Component-page heroes add the footprint art per eda-component-hero (see aiflow-sourcing). Keep the plain model; cache a separate MPN-marked STEP, GLB and reviewed thumbnails when enabled, with input hash, generator version/parameters, text/mode and native-render evidence. Protect pin-1, polarity and functional features. Marking is an identification aid, not a claim of real factory markings. Do not regenerate a valid shared cache, and do not claim KiCad/Fusion/Altium parity until each named viewer was checked.  ## Provenance is required for shared component assets 
bin/adom-aiflow
⋯ 1 unchanged line ⋯
crates/adom-aiflow/src/main.rs+32−2
@@ -702,6 +702,28 @@ fn repull_failed_clips(r: &mut Run, br: &Bridge, dir: &Path) -> (usize, usize) {     (pulled, missing) } +/// IC footprints (U*, IC*) whose bound 3D model is a local STEP without a laser-etched MPN part.+/// Models are found beside the source board (${KIPRJMOD}); an unresolvable path is left to `models`.+fn unmarked_ics(r: &Run) -> Vec<String> {+    let Ok(b) = Board::load(&r.current_board()) else { return vec![] };+    let proj = r.data["sourceBoard"].as_str().map(|p| Path::new(p).parent().map(|d| d.to_path_buf()).unwrap_or_default()).unwrap_or_default();+    let mut out = Vec::new();+    for fp in b.root.find_all("footprint") {+        let Some(reference) = fp.find_all("property").find(|p| p.atom(1) == Some("Reference")).and_then(|p| p.atom(2)).map(str::to_string) else { continue };+        let is_ic = reference.starts_with("IC") || (reference.starts_with('U') && reference[1..].chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false));+        if !is_ic { continue; }+        for m in fp.find_all("model").filter_map(|n| n.value()) {+            let path = m.replace("${KIPRJMOD}", &proj.display().to_string());+            if !(path.to_ascii_lowercase().ends_with(".step") || path.to_ascii_lowercase().ends_with(".stp")) { continue; }+            match std::fs::read(&path) {+                Ok(bytes) => { if !bytes.windows(10).any(|w| w == b"laser_etch") { out.push(reference.clone()); } }+                Err(_) => {}+            }+        }+    }+    out+}+ /// Stop the running clip of the current step, pull it, cut it. finish and deliver call this so /// no recorder outlives the run (a clip left running once recorded nine hours of a static editor). fn stop_running_clip(r: &mut Run, br: &Bridge, dir: &Path, why: &str) -> Option<String> {@@ -2101,7 +2123,7 @@ fn main() {                 let text = std::fs::read_to_string(&path).unwrap_or_else(|e| err(&format!("read components register: {e}"), &[]));                 serde_json::from_str(&text).unwrap_or_else(|e| err(&format!("invalid components register (left untouched): {e}"), &[]))             } else { json!({}) };-            let preference = etch.as_deref().or_else(|| old["mpnMarking"].as_str()).unwrap_or("ask");+            let preference = etch.as_deref().or_else(|| old["mpnMarking"].as_str()).unwrap_or("on");             let mut entries = Vec::new();             for fp in b.root.find_all("footprint") {                 let props: BTreeMap<String, String> = fp.find_all("property").filter_map(|p| Some((p.atom(1)?.into(), p.atom(2)?.into()))).collect();@@ -2125,7 +2147,7 @@ fn main() {             let flow: Value = serde_json::from_str(include_str!("../../../flows/board.json")).unwrap();             let step = flow["steps"].as_array().unwrap().iter().find(|s| s["name"] == "components").unwrap();             let mut hints: Vec<String> = step["workflow"].as_array().unwrap().iter().filter_map(Value::as_str).map(str::to_owned).collect();-            hints.push(format!("MPN marking preference: {preference}. 'ask' means offer the option, not consent; 'off' preserves plain models; 'on' requests additional reviewed marked variants. Existing review notes are preserved on rerun; re-review when source properties or geometry changes."));+            hints.push(format!("MPN marking preference: {preference} (default on: every IC's model carries its laser-etched MPN, and finish checks it). Mark a model with `adom-aiflow-etch --in <model.step> --mpn <MPN> --out <model.step>`: it keeps the model's colours and lays the text along the long axis. 'off' keeps plain models; 'ask' offers the option. Existing review notes are preserved on rerun; re-review when source properties or geometry changes."));             ok(&format!("component register: {} ({} references); AI review is pending", path.display(), report["components"].as_array().unwrap().len()), &hints);         }         Cmd::Plan => {@@ -2764,6 +2786,14 @@ fn main() {                 let have = r.data["evidence"].as_array().map(|a| a.iter().any(|e| e["kind"].as_str() == Some(k.as_str()) && e["pass"] != false)).unwrap_or(false);                 if !have { missing.push(format!("{k} evidence (evidence add --kind {k} --file <json|md>)")); }             }+            // MPN marking: unless it was turned off, every IC (U*, IC*) carries its laser-etched MPN on the+            // top face of its model (a 'laser_etch' part in the STEP), so the board and the component+            // pages show which chip is which+            let marking = r.data["components"]["mpnMarking"].as_str().unwrap_or("on").to_string();+            if marking != "off" {+                let unmarked = unmarked_ics(&r);+                if !unmarked.is_empty() { missing.push(format!("MPN laser etch on the ICs ({}): `adom-aiflow-etch --in <model.step> --mpn <MPN> --out <model.step>`, or `components --etch off` to decline", unmarked.join(", "))); }+            }             if !missing.is_empty() {                 err(&format!("not finished: {}", missing.join("; ")), &["A board with any of these open is not a result. Keep going: the hints of the failing stage say what to change.".into()]);             }
docs/component-sourcing.md+2−2
@@ -8,13 +8,13 @@ Publish missing identified component pages with verified symbol/footprint/model  Global component pages serve ALL ADOM USERS. Publish only distinct reusable manufacturer parts or independently specified reusable custom components, after duplicate checking. Keep page titles and assets part-focused and portable. Board reference mappings, unresolved identities, one-off footprints, bare copper features and project-only assemblies belong in the board project wiki page, not new component catalog entries. Do not turn project-specific placeholders into generic-looking pages merely by renaming them. -## Early component quality and optional MPN marking+## Early component quality and MPN marking  Before placement, declare `step components` and run `components` with your `--run` and `--ai-thread`. It writes `components.json`, inventories every board reference and preserves review notes on rerun. This is an AI review register, not an automated geometry or licensing certification. Record wiki links, classification, dimensions, terminals, pin-1/polarity, plain/marked variants, top/bottom/oblique visual evidence and redistribution evidence. Unknowns remain unknown.  Use manufacturer drawings and permitted CAD as reference checks. Third-party downloads may be private reference-only; never redistribute them without permission, or call extracted/recolored/etched geometry independently authored. Compare independently generated geometry with dimensional and visual evidence. Publish only reusable assets on global pages; board-only definitions stay in the project. -Offer `components --etch on` or `--etch off` (initial default `ask` is not consent). Discover adom/adom-chip-laser and the current STEP engine. Keep the plain model; cache a separate MPN-marked STEP, GLB and reviewed thumbnails when enabled, with input hash, generator version/parameters, text/mode and native-render evidence. Protect pin-1, polarity and functional features. Marking is an identification aid, not a claim of real factory markings. Do not regenerate a valid shared cache, and do not claim KiCad/Fusion/Altium parity until each named viewer was checked.+MPN marking is on by default for ICs: every IC's model carries its laser-etched MPN and `finish` checks it (`components --etch off` declines for a run). Mark with `adom-aiflow-etch --in <model.step> --mpn <MPN> --out <model.step>`: it uses adom/adom-chip-laser but keeps the model's own colours (the service alone repaints the part black and grey) and lays the text along the part's longest top-face direction, standing Y-up vendor models upright first (`--up y`). Use `--mark dark` on light bodies. Component-page heroes add the footprint art per eda-component-hero (see aiflow-sourcing). Keep the plain model; cache a separate MPN-marked STEP, GLB and reviewed thumbnails when enabled, with input hash, generator version/parameters, text/mode and native-render evidence. Protect pin-1, polarity and functional features. Marking is an identification aid, not a claim of real factory markings. Do not regenerate a valid shared cache, and do not claim KiCad/Fusion/Altium parity until each named viewer was checked.  ## Provenance is required for shared component assets 
docs/release-0.1.39.mdadded+8
@@ -0,0 +1,8 @@+# AI Flow 0.1.39++MPN laser etch on the parts, done properly, and component heroes that show the part on its footprint.++- **`adom-aiflow-etch`** (installed with the package): laser-etches a part's MPN onto the top face of its STEP with adom/adom-chip-laser, but keeps the model's own colours and lays the text along the part's longest top-face direction. The service on its own repaints the whole part black and grey and writes across X (adom/adom-chip-laser#2), so the tool turns the part so its long axis lies along X (and stands Y-up vendor models upright with `--up y`), keeps only the service's inlay, adds it to the untouched original as a separate `laser_etch` part with its own colour (`--mark light|dark|r,g,b`), and turns it back into the original frame. On a SOT-23-6 the mark is 0.22 mm along the long axis against 0.14 mm across it. It uses OCP (OpenCascade's Python bindings) in its own venv, made on first use.+- **Marking is on by default for ICs.** `components` now defaults to `on`, and `finish` refuses while an IC's (U*, IC*) bound STEP has no `laser_etch` part; `components --etch off` declines for a run.+- **Component pages.** aiflow-sourcing and aiflow-molecule now require the eda-component-hero hero (etched part, teal dashed copper outlines, footprint silkscreen, pin labels) whenever a component page is created, including through molecule-publish's "create missing component pages" stage, which does not ask for it.+- The components step in the flow says the same: long axis, colours, Y-up models, and leave a part unmarked rather than ship a mark across a polarity stripe or off the face.
flows/board.json+3−3
@@ -28,10 +28,10 @@         "Use portable project/library model paths, preserve placement and routing, rerun kicad_model_check and inspect the saved board in native KiCad 3D. Report reference coverage, reused/created page URLs, unresolved identities and missing CAD separately. A successful download or wiki publication alone does not close a missing-model finding.",         "Judge quality per component, not per board screenshot: publish a linked visual register with top, bottom and oblique views. Check dimensions, units, terminal count and pitch, pin-1/polarity, body/pad alignment, standoff, materials and visible details. File resolution alone is not quality. Record pass, needs-work, reference-only or unknown with evidence and limits.",         "Prefer manufacturer CAD when the source permits the intended redistribution. Keep third-party/Ultra Librarian downloads as private reference-only inputs unless redistribution is explicitly permitted. Compare independently generated models against manufacturer drawings and permitted reference views; retain dimensional deviations and source hashes. Converting, extracting, recoloring, or etching a restricted model does not make it independently authored or license-cleared.",-        "Offer optional MPN marking using adom/adom-chip-laser or the current adom-step2glb laser-etch service. Ask whether to enable it; default to unmarked models until the user chooses. Check the shared page cache first. Preserve the plain STEP and publish an additional marked STEP, derived GLB and reviewed thumbnails only for assets eligible for redistribution. Keep the mark clear of pin-1, polarity, terminals and optical/mechanical features. Marking is an identification aid, not evidence of actual factory top-marking, especially on tiny passives.",+        "MPN marking is ON by default for ICs (John, 2026-09-30): every IC's 3D model carries its MPN laser-etched on the top face, and finish checks it; passives are optional. Run `adom-aiflow-etch --in <model.step> --mpn <MPN> --out <model.step>` (adom/adom-chip-laser service plus a colour-preserving merge). Do not post a STEP to the service and use its output directly: the service repaints the whole part black and grey (adom/adom-chip-laser#2). Keep the plain model beside the marked one; `components --etch off` declines for a run. Marking is an identification aid, not the part's factory top mark.",         "Cache reviewed artifacts on the existing global component page: source/plain STEP, optional MPN STEP, GLB, thumbnails and machine-readable provenance. Record input hash, generator/tool version, parameters, units, transforms, marking text/mode, reference evidence and review results. Cache keys must change when geometry, text or generator parameters change. Keep board-only transforms and mappings in the board project. Never claim cross-EDA parity without rendering the variant in the named native viewers.",         "When publishing or improving a component page, make per-file provenance mandatory even though creating a new page is optional. Record original source URL/file and revision, retrieval date, source and output SHA-256, authoring classification (manufacturer-supplied, source-derived, AI-created, or unknown), generator/version and parameters, units/transforms, redistribution evidence and limitations. For AI-created geometry cite the actual datasheet page/figure/table and dimensions used, list simplifications and reference comparisons, and never present copied/extracted CAD as independent work. State which checks ran and which remain unverified; retain plain and marked variant lineage. Put a readable provenance section on the page plus a machine-readable asset record and a link to that component's issue tracker. Unknown provenance stays unknown, not a fabricated source. Reuse/improve existing pages first; offer new global-page publication only for reusable components, keeping board-specific records in the project.",-        "For optional MPN marking, fit the text along the longest usable top-face direction with the largest legible size, respecting pin-1 and mechanical features. Compare both orientations and retain the package coordinate frame. Cache and identify the marked variant explicitly.",+        "Fit the mark along the LONGEST usable top-face direction at the largest legible size (a SOT-23-6 gets 0.22 mm along its long axis against 0.14 mm across it); the service only writes across X, so turn the part first (adom-aiflow-etch does). Stand Y-up vendor models upright first (`--up y`), or the mark lands on a side face. Use a light mark on dark epoxy and a dark mark on light bodies (`--mark dark`). Look at the result: colours intact, text inside the top face, clear of pin-1 and polarity features; a mark across a polarity stripe or off the face is worse than none, so leave that part unmarked and say why.",         "Offer the optional Hydrogen progress widget. If enabled, reuse saved component, marked-model, symbol and later board/analysis thumbnails via widget event; do not generate extra screenshots or call a model solely for the widget. Respect widget disable immediately.",         "Before accepting a wiki component, read its actual native model_3d and STEP bindings and hash the referenced downloads against the reviewed variants. Wait for the native viewer to finish painting, then inspect the visible body and readable mark; nonzero meshes or a successful HTTP request do not prove a painted model. Check the static hero too. Use a descriptive manufacturer/MPN/function title in both page.json and package.json; follow wiki-component and preserve other contributors and original files.",         "A resolved 3D file may still be hidden by native EDA appearance filters (for example KiCad excludes models not in position files). Check those filters before inventing replacement parts or changing BOM/placement flags. Keep bridge-specific visibility, binding and native audit operations in the owning bridge; report missing capabilities there. Explicit rights-holder permission may authorize a derivative despite a restrictive package default; record who authorized what and its scope, without extending it to third-party assets.",@@ -240,4 +240,4 @@   ],   "clips": "every `step <name>` stops the previous step's clip and starts a new window recording tagged with the step, when the board is open on a test box; run.json captures[] carries one entry per clip with its step, start, stop and file, and deliver lists them; the final video is cut from these clips, one segment per step, so two engines' videos line up step for step; a return (step <name> --back --why) is a new visit and gets its own clip, tagged <step>-<visit> with the reason, so the rework is on camera and the final cut can show the loop",   "screenshots": "every step visit gets two background screenshots of the editor window, at its start and at its end (shot-<step>-<visit>-start.png, shot-<step>-<visit>-end.png), logged as artifacts, so a run's own README has a picture for every step without anyone taking one"-}+}\ No newline at end of file
install.sh+2
@@ -25,6 +25,8 @@ ln -sf "$HERE/tools/aiflow-widget.py" "$HOME/.local/bin/adom-aiflow-widget" ln -sf "$HERE/tools/library-tour.py" "$HOME/.local/bin/adom-aiflow-library-tour" chmod +x "$HERE/tools/kicad-cli-remote" ln -sf "$HERE/tools/kicad-cli-remote" "$HOME/.local/bin/adom-aiflow-kicad-cli-remote"+chmod +x "$HERE/tools/laser-etch/adom-aiflow-etch" "$HERE/tools/laser-etch/"*.py+ln -sf "$HERE/tools/laser-etch/adom-aiflow-etch" "$HOME/.local/bin/adom-aiflow-etch" for skill in "$HERE"/skills/*; do   [ -f "$skill/SKILL.md" ] || continue   name="$(basename "$skill")"
package.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.1.38",+  "version": "0.1.39",   "title": "AI Flow",   "description": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. One Rust binary with a crate per step (placement helpers, a grid router with Kelvin taps, pours with keepouts, KiCad's DRC gate, live landing through the KiCad Bridge, copper measurement, current and thermal analysis) and a finish line that refuses an unfinished board. Every command answers with hints for the AI; every turn, its thinking time and every rework loop go into run.jsonl, so Claude, Codex and any other engine are compared on the same flow. KiCad today; Altium, Fusion and Adom's own web apps next.",   "summary": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. The AI thinks its way from placement through routing, pours, current and thermal analysis to a delivered video; the binary does the fast, deterministic parts of every step, hands back hints, and keeps a ledger of every turn, every return to an earlier step, and the clock from the prompt to done.",
page.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.1.38",+  "version": "0.1.39",   "title": "AI Flow",   "description": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. One Rust binary with a crate per step (placement helpers, a grid router with Kelvin taps, pours with keepouts, KiCad's DRC gate, live landing through the KiCad Bridge, copper measurement, current and thermal analysis) and a finish line that refuses an unfinished board. Every command answers with hints for the AI; every turn, its thinking time and every rework loop go into run.jsonl, so Claude, Codex and any other engine are compared on the same flow. KiCad today; Altium, Fusion and Adom's own web apps next.",   "summary": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. The AI thinks its way from placement through routing, pours, current and thermal analysis to a delivered video; the binary does the fast, deterministic parts of every step, hands back hints, and keeps a ledger of every turn, every return to an earlier step, and the clock from the prompt to done.",
skills/adom-aiflow/SKILL.md+2−2
@@ -136,13 +136,13 @@ Publish missing identified component pages with verified symbol/footprint/model  Global component pages serve ALL ADOM USERS. Publish only distinct reusable manufacturer parts or independently specified reusable custom components, after duplicate checking. Keep page titles and assets part-focused and portable. Board reference mappings, unresolved identities, one-off footprints, bare copper features and project-only assemblies belong in the board project wiki page, not new component catalog entries. Do not turn project-specific placeholders into generic-looking pages merely by renaming them. -## Early component quality and optional MPN marking+## Early component quality and MPN marking  Before placement, declare `step components` and run `components` with your `--run` and `--ai-thread`. It writes `components.json`, inventories every board reference and preserves review notes on rerun. This is an AI review register, not an automated geometry or licensing certification. Record wiki links, classification, dimensions, terminals, pin-1/polarity, plain/marked variants, top/bottom/oblique visual evidence and redistribution evidence. Unknowns remain unknown.  Use manufacturer drawings and permitted CAD as reference checks. Third-party downloads may be private reference-only; never redistribute them without permission, or call extracted/recolored/etched geometry independently authored. Compare independently generated geometry with dimensional and visual evidence. Publish only reusable assets on global pages; board-only definitions stay in the project. -Offer `components --etch on` or `--etch off` (initial default `ask` is not consent). Discover adom/adom-chip-laser and the current STEP engine. Keep the plain model; cache a separate MPN-marked STEP, GLB and reviewed thumbnails when enabled, with input hash, generator version/parameters, text/mode and native-render evidence. Protect pin-1, polarity and functional features. Marking is an identification aid, not a claim of real factory markings. Do not regenerate a valid shared cache, and do not claim KiCad/Fusion/Altium parity until each named viewer was checked.+MPN marking is on by default for ICs: every IC's model carries its laser-etched MPN and `finish` checks it (`components --etch off` declines for a run). Mark with `adom-aiflow-etch --in <model.step> --mpn <MPN> --out <model.step>`: it uses adom/adom-chip-laser but keeps the model's own colours (the service alone repaints the part black and grey) and lays the text along the part's longest top-face direction, standing Y-up vendor models upright first (`--up y`). Use `--mark dark` on light bodies. Component-page heroes add the footprint art per eda-component-hero (see aiflow-sourcing). Keep the plain model; cache a separate MPN-marked STEP, GLB and reviewed thumbnails when enabled, with input hash, generator version/parameters, text/mode and native-render evidence. Protect pin-1, polarity and functional features. Marking is an identification aid, not a claim of real factory markings. Do not regenerate a valid shared cache, and do not claim KiCad/Fusion/Altium parity until each named viewer was checked.  ## Provenance is required for shared component assets 
skills/aiflow-molecule/SKILL.md+4
@@ -85,3 +85,7 @@ A locked interface, a rules profile in the board, and after the build a converte - Contacts on the short edges: J1 VIN (0,12), J2 GND (0,8), J3 EN (0,4); J4 VOUT (24,12), J5 GND (24,8), J6 VMON (24,4). Input on the left, output on the right, each with its own ground. - Rules: 3rd party fab 2-layer profile, 1.6 mm FR4, designed for 0.5 oz outer copper. - The Adom library footprints reference their STEP through the plugin manager's path; the project carries copies in `kicad/3d/` so the board renders on any machine.++## Component wiki pages: the hero is annotated and etched++Whenever a component page is created or its model changes (here, or through molecule-publish's "create missing component pages" stage), the page's hero GLB follows `eda-component-hero` (adom/eda-engineering): the part with its laser-etched MPN (`adom-aiflow-etch`, colours kept, along the long axis), teal dashed copper-pad outlines from the real pads at alpha 0.50, the footprint silkscreen at 0.30, and pin labels at 0.50, as separate nonphysical nodes in the verified Z-up seating frame with pin 1 and polarity matching the footprint (use its `tools/component-reference-overlay.py`). The plain STEP and the board GLB stay clean (no footprint art); publish the marked STEP as an extra variant with provenance. Check the published hero by looking at it: top, oblique, underside and the pin-1 corner. A plain grey thumbnail as a component hero is not done.
skills/aiflow-sourcing/SKILL.md+4
@@ -106,3 +106,7 @@ step2glb thumbnail part.step       # look at it - The inductor was first drawn from the datasheet with a local OCCT install before looking properly; Abracon's official STEP turned up only when searching the series `AMPLH5030S`. Panasonic publishes electrolytic STEPs by case (`DS_Alumi_D_5.zip`), and that search caught a case size written into the BOM from memory (the page says 6.3 x 5.8 mm, case D). Search first, draw last. - Three models were Y-up and needed -90 X: Abracon AMPLH5030S, Panasonic case D, and the wiki's Samsung CL21A226MPQNNNE 0805. `step2glb features` showed it. - `design/bom.csv` ended the run without rows for R7/R8 (the VMON divider), the contacts and the test pads: exactly what the netlist diff in section 3 catches.++## Component wiki pages: the hero is annotated and etched++Whenever a component page is created or its model changes (here, or through molecule-publish's "create missing component pages" stage), the page's hero GLB follows `eda-component-hero` (adom/eda-engineering): the part with its laser-etched MPN (`adom-aiflow-etch`, colours kept, along the long axis), teal dashed copper-pad outlines from the real pads at alpha 0.50, the footprint silkscreen at 0.30, and pin labels at 0.50, as separate nonphysical nodes in the verified Z-up seating frame with pin 1 and polarity matching the footprint (use its `tools/component-reference-overlay.py`). The plain STEP and the board GLB stay clean (no footprint art); publish the marked STEP as an extra variant with provenance. Check the published hero by looking at it: top, oblique, underside and the pin-1 corner. A plain grey thumbnail as a component hero is not done.
tools/laser-etch/adom-aiflow-etchadded+11
@@ -0,0 +1,11 @@+#!/usr/bin/env bash+# adom-aiflow-etch: colour-preserving, long-axis MPN laser etch of a part STEP (see etch_part.py).+# OCP (OpenCascade's Python bindings) lives in its own venv, made on first use.+set -euo pipefail+HERE="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"+VENV="${AIFLOW_OCC_VENV:-$HOME/.local/share/adom-aiflow/occ}"+if [ ! -x "$VENV/bin/python" ] || ! "$VENV/bin/python" -c "import OCP" 2>/dev/null; then+  echo "adom-aiflow-etch: setting up OCP in $VENV (once)" >&2+  python3 -m venv "$VENV" && "$VENV/bin/pip" -q install cadquery-ocp >&2+fi+exec "$VENV/bin/python" "$HERE/etch_part.py" "$@"
tools/laser-etch/etch_part.pyadded+43
@@ -0,0 +1,43 @@+#!/usr/bin/env python3+"""Laser-etch a part's MPN onto the top face of its STEP, keeping the model's own colours.++The adom-chip-laser service etches across X on a Z-up part and repaints the whole model black and+grey. This stands the part up (--up y for Y-up vendor models), turns it so its LONGEST top-face+direction lies along X, has the service etch that, keeps only the service's inlay, adds it to the+UNTOUCHED original as a separate 'laser_etch' part with its own colour, and turns everything back.+Output is in the original's frame, so the board's model path and transform do not change.++Usage: etch_part.py --in part.step --mpn MPN --out part-etched.step [--up z|y] [--frac 0.55]+                    [--mark light|dark|r,g,b] [--service URL]+Needs OCP (OpenCascade's Python bindings; `adom-aiflow-etch` makes its own venv on first use)."""+import argparse, json, os, subprocess, sys, tempfile, urllib.request+HERE = os.path.dirname(os.path.abspath(__file__)); PY = sys.executable+ap = argparse.ArgumentParser()+ap.add_argument("--in", dest="src", required=True); ap.add_argument("--mpn", required=True); ap.add_argument("--out", required=True)+ap.add_argument("--up", default="z", choices=["z", "y"]); ap.add_argument("--frac", default="0.55")+ap.add_argument("--mark", default="light"); ap.add_argument("--service", default=os.environ.get("ADOM_CHIP_LASER_URL", "https://laserchip-6dn1tl7vjz5u.adom.cloud"))+a = ap.parse_args()+from OCP.STEPControl import STEPControl_Reader+from OCP.Bnd import Bnd_Box+from OCP.BRepBndLib import BRepBndLib+def ext(p):+    r = STEPControl_Reader(); r.ReadFile(p); r.TransferRoots(); b = Bnd_Box(); BRepBndLib.Add_s(r.OneShape(), b)+    mn, mx = b.CornerMin(), b.CornerMax(); return mx.X() - mn.X(), mx.Y() - mn.Y(), mx.Z() - mn.Z()+def run(*args):+    r = subprocess.run([PY, *args], capture_output=True, text=True)+    if r.returncode: sys.exit(f"ERROR: {' '.join(args[:1])} failed: {r.stderr[-600:]}")+tmp = tempfile.mkdtemp(prefix="etch-"); cur = a.src; undo = []+if a.up == "y":                                     # Y-up vendor model: stand it up first+    run(os.path.join(HERE, "rotate_step_xcaf.py"), cur, f"{tmp}/up.step", "90", "x"); cur = f"{tmp}/up.step"; undo.append(("-90", "x"))+dx, dy, dz = ext(cur)+if dy > dx * 1.02:                                  # long axis along Y: turn it onto X so the mark runs along it+    run(os.path.join(HERE, "rotate_step_xcaf.py"), cur, f"{tmp}/long.step", "90", "z"); cur = f"{tmp}/long.step"; undo.append(("-90", "z"))+req = urllib.request.Request(f"{a.service.rstrip('/')}/etch?mpn={urllib.request.quote(a.mpn)}&frac={a.frac}", data=open(cur, "rb").read(), method="POST")+with urllib.request.urlopen(req, timeout=300) as resp:+    open(f"{tmp}/etched.step", "wb").write(resp.read()); size = resp.headers.get("X-Etch-Fontsize-Mm")+rgb = {"light": "0.78,0.78,0.76", "dark": "0.08,0.08,0.08"}.get(a.mark, a.mark)+run(os.path.join(HERE, "merge_etch.py"), cur, f"{tmp}/etched.step", f"{tmp}/merged.step", "0.01", rgb); cur = f"{tmp}/merged.step"+for i, (deg, ax) in enumerate(reversed(undo)):+    nxt = f"{tmp}/undo{i}.step"; run(os.path.join(HERE, "rotate_step_xcaf.py"), cur, nxt, deg, ax); cur = nxt+os.replace(cur, a.out)+print(json.dumps({"ok": True, "out": a.out, "mpn": a.mpn, "fontMm": float(size) if size else None, "alongLongAxis": True, "turnedForLongAxis": any(ax == "z" for _, ax in undo), "stoodUp": a.up == "y", "mark": rgb}))
tools/laser-etch/merge_etch.pyadded+45
@@ -0,0 +1,45 @@+#!/usr/bin/env python3+"""Colour-preserving laser etch: the etch service repaints the whole part (black body, grey inlay) and+drops the model's own colours. Keep the ORIGINAL coloured model untouched and add only the service's+inlay solids (the thin solids at the top of the etched part) as one light-grey 'laser_etch' part,+lifted a hair so it never z-fights with the top face.+Usage: merge_etch.py <original.step> <etched.step> <out.step> [lift_mm=0.01] [r,g,b of the mark, default light grey]"""+import sys+from OCP.STEPControl import STEPControl_Reader, STEPControl_AsIs+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.TopExp import TopExp_Explorer+from OCP.TopAbs import TopAbs_SOLID+from OCP.TopoDS import TopoDS, TopoDS_Compound+from OCP.BRep import BRep_Builder+from OCP.Bnd import Bnd_Box+from OCP.BRepBndLib import BRepBndLib+from OCP.gp import gp_Trsf, gp_Vec+from OCP.TopLoc import TopLoc_Location+from OCP.Quantity import Quantity_Color, Quantity_TOC_RGB+from OCP.TDataStd import TDataStd_Name+orig, etched, out = sys.argv[1:4]; lift = float(sys.argv[4]) if len(sys.argv) > 4 else 0.01+rgb = [float(v) for v in sys.argv[5].split(",")] if len(sys.argv) > 5 else [0.78, 0.78, 0.76]   # light on dark epoxy; pass a dark colour for a light body (an aluminium can)+def box(s):+    b = Bnd_Box(); BRepBndLib.Add_s(s, b); mn, mx = b.CornerMin(), b.CornerMax(); return mn.X(), mn.Y(), mn.Z(), mx.X(), mx.Y(), mx.Z()+r = STEPControl_Reader(); r.ReadFile(etched); r.TransferRoots(); e = r.OneShape()+top = box(e)[5]+ex = TopExp_Explorer(e, TopAbs_SOLID); inlay = []+while ex.More():+    s = TopoDS.Solid(ex.Current()); ex.Next()+    x0, y0, z0, x1, y1, z1 = box(s)+    if z1 - z0 < 0.2 and top - z1 < 0.05: inlay.append(s)      # thin, at the very top: the etch+comp = TopoDS_Compound(); bb = BRep_Builder(); bb.MakeCompound(comp)+for s in inlay: bb.Add(comp, s)+t = gp_Trsf(); t.SetTranslation(gp_Vec(0, 0, lift)); comp2 = comp.Moved(TopLoc_Location(t))+doc = TDocStd_Document(TCollection_ExtendedString("d"))+cr = STEPCAFControl_Reader(); cr.SetColorMode(True); cr.SetNameMode(True); cr.SetLayerMode(True); cr.ReadFile(orig); cr.Transfer(doc)+st = XCAFDoc_DocumentTool.ShapeTool_s(doc.Main()); ct = XCAFDoc_DocumentTool.ColorTool_s(doc.Main())+lab = st.AddShape(comp2, False)+TDataStd_Name.Set_s(lab, TCollection_ExtendedString("laser_etch"))+ct.SetColor(lab, Quantity_Color(rgb[0], rgb[1], rgb[2], Quantity_TOC_RGB), XCAFDoc_ColorType.XCAFDoc_ColorSurf)+w = STEPCAFControl_Writer(); w.SetColorMode(True); w.SetNameMode(True); w.SetLayerMode(True)+w.Transfer(doc, STEPControl_AsIs); w.Write(out)+print(f"inlay solids {len(inlay)}; etched top z {top:.3f}")
tools/laser-etch/rotate_step_xcaf.pyadded+41
@@ -0,0 +1,41 @@+#!/usr/bin/env python3+"""Rotate a STEP about Z through its bbox centre, keeping colours and names (XCAF): used to lay a+laser-etch mark along a part's long axis when the etch service writes across X only.+Usage: rotate_step_xcaf.py <in.step> <out.step> <degrees> [x|y|z, default z]"""+import sys, math+from OCP.STEPCAFControl import STEPCAFControl_Reader, STEPCAFControl_Writer+from OCP.STEPControl import STEPControl_AsIs+from OCP.TDocStd import TDocStd_Document+from OCP.TCollection import TCollection_ExtendedString+from OCP.XCAFDoc import XCAFDoc_DocumentTool+from OCP.TDF import TDF_ChildIterator+from OCP.gp import gp_Trsf, gp_Ax1, gp_Pnt, gp_Dir+from OCP.TopLoc import TopLoc_Location+from OCP.Bnd import Bnd_Box+from OCP.BRepBndLib import BRepBndLib+src, out, deg = sys.argv[1], sys.argv[2], float(sys.argv[3])+axis = sys.argv[4] if len(sys.argv) > 4 else "z"+doc = TDocStd_Document(TCollection_ExtendedString("doc"))+r = STEPCAFControl_Reader(); r.SetColorMode(True); r.SetNameMode(True); r.SetLayerMode(True)+r.ReadFile(src); r.Transfer(doc)+st = XCAFDoc_DocumentTool.ShapeTool_s(doc.Main())+labs = []+it = TDF_ChildIterator(st.Label(), False)+while it.More():+    l = it.Value()+    if st.IsFree_s(l) and st.IsTopLevel(l): labs.append(l)+    it.Next()+print('free shapes', len(labs))+b = Bnd_Box()+for l in labs: BRepBndLib.Add_s(st.GetShape_s(l), b)+mn, mx = b.CornerMin(), b.CornerMax()+d = {"x": gp_Dir(1, 0, 0), "y": gp_Dir(0, 1, 0), "z": gp_Dir(0, 0, 1)}[axis]+t = gp_Trsf(); t.SetRotation(gp_Ax1(gp_Pnt((mn.X()+mx.X())/2, (mn.Y()+mx.Y())/2, (mn.Z()+mx.Z())/2 if axis != "z" else 0), d), math.radians(deg))+# wrap the untouched shapes in a new assembly carrying the rotation: the colours stay on the originals+asm = st.NewShape()+for lab in labs:+    st.AddComponent(asm, lab, TopLoc_Location(t))+st.UpdateAssemblies()+w = STEPCAFControl_Writer(); w.SetColorMode(True); w.SetNameMode(True); w.SetLayerMode(True)+w.Transfer(doc, STEPControl_AsIs); w.Write(out)+print(f"rotated {deg} deg about {axis}; in bbox x {mx.X()-mn.X():.3f} y {mx.Y()-mn.Y():.3f}")
uninstall.sh+1−1
@@ -1,4 +1,4 @@ #!/bin/bash-rm -f "$HOME/.local/bin/adom-aiflow" "$HOME/.local/bin/adom-aiflow-kicad-cli-remote"+rm -f "$HOME/.local/bin/adom-aiflow" "$HOME/.local/bin/adom-aiflow-kicad-cli-remote" "$HOME/.local/bin/adom-aiflow-etch" rm -rf "$HOME/.claude/skills/adom-aiflow" "$HOME/.claude/skills/aiflow-measurement" echo "OK: adom-aiflow removed."