app
AI Flow
Public Made by Adomby adom
Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board.
← Commit history
AI Flow 0.1.61: silkscreen the fab would clip blocks gate and finish; board-glb clips silk at mask openings and its check refuses an unclipped render
5 files changed
+67−15
SKILL.md+5@@ -123,6 +123,11 @@ The clock starts at the prompt even when there is no board yet. - **`evidence add --kind calcs|simulation|vendor-model --file <json|md> [--note ...]`** records the design record in the run and the ledger with its sha256 (a copy lands in `<run>/evidence/`). A simulation result that says it fails (a top-level `"pass": false`, or a `"checks"` row `{"name","value","limit","pass"}` that fails) is refused. Vendor PSpice/LTspice runs belong to the separately published adom/adom-spice-skillpack; `--kind vendor-model` records their result. - **`finish`** also refuses without a passed sourcing check against the board when the spec or the requirements name a sourcing profile, and without the evidence kinds the spec lists in `"requireEvidence": ["calcs", "simulation"]`. +**Silkscreen the fab would clip blocks the board (0.1.61).** KiCad grades `silk_over_copper`, `silk_overlap`, `silk_edge_clearance`, `text_height` and `text_thickness` as warnings by default.+- `gate` and `finish` treat them as blocking, like the other manufacturing warnings. `finish` runs KiCad's full DRC on the live board for this.+- Ink over a mask opening is clipped off by the fab: the text prints broken and the pad carries ink at its edge. The fix is to move the text, or the pad, never to downgrade the rule.+- KiCad's STEP export keeps the whole ink. So `board-glb copper` lays the exposed copper over the ink inside every mask opening, the way the fab prints it, and `board-glb check` fails a GLB whose silkscreen is not clipped.+ ## The fields step (adom-fields) After the pours and the analyses: `adom-fields analyze --board <the current board> --spec <spec> --out fields` (about two minutes; `adom-wiki pkg install adom/adom-fields` if it is missing), `adom-fields serve --fields fields --port 8874 &`, then `step fields` and `tour fields`: the binary opens the app in a pup window on the test box, films that window while `adom-fields tour` walks the board gently (the app paces itself: slow eased moves about nine seconds long, the board drifting a few degrees through every held beat, one copper pour isolated at a time, a quick turn-over to the bottom layer, because the flow plays this clip back at ten times speed, where a snap reads as frenetic) (each net lit, the hot parts, the issues flown to; the app's toasts are the captions), and records the issues in the run. When the Fields URL is a localhost proxy (an Adom Hydrogen desktop container's `VSCODE_PROXY_URI` is `http://{{port}}.localhost:<port>/`, reachable only on the machine hosting the container), the window opens on the user's primary machine (`adom-bridge targets` names it) instead of the test box, and the answer says so; `--on <box>` overrides. The issues (a current neck, a tab over its rise budget, a chip missing its thermal vias) are yours to answer before delivery; the app's screenshots are drawings you can register with `artifact`.
crates/adom-aiflow/src/main.rs+13−10@@ -2990,6 +2990,16 @@ fn main() { if live["unconnected"].as_u64() != Some(0) || live["errors"].as_u64().unwrap_or(u64::MAX) > inherited { missing.push(format!("live validate: {} unconnected, {} errors (inherited {inherited})", live["unconnected"], live["errors"])); }+ // KiCad's full DRC on the live board, warnings included: the silkscreen findings the fab makes real+ // (ink over a mask opening is clipped off, ink on ink, ink past the edge, text under the fab minimum)+ // block whatever severity the project gives them; the routing validate above counts errors only+ let drc = br.call("kicad_run_drc", &json!({"filePath": r.data["remoteBoard"].as_str().unwrap_or(""), "refillZones": true, "reason": "finish: full DRC, warnings included"}));+ let found: Vec<Value> = drc["summary"]["first"].as_array().or(drc["violations"].as_array()).cloned().unwrap_or_default();+ let silk: Vec<String> = found.iter().filter(|v| v["type"].as_str().map(|t| aiflow_copper::SILK_BLOCKING.contains(&t)).unwrap_or(false))+ .map(|v| format!("{} ({})", v["type"].as_str().unwrap_or(""), v["items"].as_array().map(|a| a.iter().map(|i| i.as_str().map(str::to_string).unwrap_or_else(|| i["description"].as_str().unwrap_or("").to_string())).collect::<Vec<_>>().join(" / ")).unwrap_or_default())).collect();+ if !silk.is_empty() {+ missing.push(format!("silkscreen the fab would clip or blur ({}): {}. Move the text (or the pad), never downgrade the rule (skill aiflow-silkscreen)", silk.len(), silk.iter().take(6).cloned().collect::<Vec<_>>().join("; ")));+ } } // the work before the board: the sourcing check when a profile is named, the evidence the spec asks for let (spec_v, req_v) = (spec_soft(&r), requirements_of(&r));@@ -3446,12 +3456,8 @@ fn main() { ok(&format!("{done} board corner(s) rounded to {rad} mm; the outline has no sharp corner"), &["Copper keeps its edge clearance from the new arcs: refill the pours (`land pours` again) if a pour followed the old corner.".into()]); } Cmd::Timeline { action, port } => {- let mut r = load_run(&cli);+ let r = load_run(&cli); let flow_full: Value = serde_json::from_str(include_str!("../../../flows/board.json")).unwrap();- if r.data["steps"].as_object().map(|o| o.keys().all(|k| k == "intake")).unwrap_or(true) {- let want = json!(sequence::sequence_of(&flow_full, sequence::lane(&dir, &r.data)));- if r.data["sequence"] != want { r.data["sequence"] = want; r.save().unwrap(); }- } let seq: Vec<String> = r.data["sequence"].as_array().map(|a| a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect()).unwrap_or_else(|| sequence::sequence_of(&flow_full, sequence::lane(&dir, &r.data))); let tl = sequence::timeline(&dir, &r.data, &flow_full, &seq, &r.step_report()); sequence::write_timeline(&dir, &tl, TIMELINE_PAGE);@@ -3941,11 +3947,8 @@ fn main() { // the required steps, in order: a run that starts now carries its lane's sequence, and no step // in it can be skipped (sequence.rs) let flow_full: Value = serde_json::from_str(include_str!("../../../flows/board.json")).unwrap();- // the lane can still change while only intake is done (the run learns it is a Fusion board from the- // brief or the requirements written during intake)- if r.data["steps"].as_object().map(|o| o.keys().all(|k| k == "intake")).unwrap_or(true) {- let want = json!(sequence::sequence_of(&flow_full, sequence::lane(&dir, &r.data)));- if r.data["sequence"] != want { r.data["sequence"] = want; }+ if r.data["sequence"].is_null() && r.data["steps"].as_object().map(|o| o.keys().all(|k| k == "intake")).unwrap_or(true) {+ r.data["sequence"] = json!(sequence::sequence_of(&flow_full, sequence::lane(&dir, &r.data))); } let seq: Vec<String> = r.data["sequence"].as_array().map(|a| a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect()).unwrap_or_default(); if !seq.is_empty() && !*back {
crates/aiflow-copper/src/lib.rs+17−3@@ -512,10 +512,16 @@ fn count_types(list: &[&Value]) -> BTreeMap<String, usize> { m } +/// Silkscreen findings the fab makes real: ink over a mask opening is clipped off (the text prints broken, the pad+/// carries ink at its edge), ink on ink is unreadable, ink past the edge is cut, text under the fab's minimum blurs.+/// A render of the KiCad STEP shows the unclipped ink, a board no fab makes. Blocking whatever the project's severity.+pub const SILK_BLOCKING: &[&str] = &["silk_over_copper", "silk_overlap", "silk_edge_clearance", "text_height", "text_thickness"];+ // Manufacturing constraints remain blocking even when a project downgrades them to warnings.-fn blocking_violation(v: &Value) -> bool {- v["severity"] == "error" || (v["severity"] == "warning" && matches!(v["type"].as_str(),- Some("hole_to_hole" | "holes_co_located" | "drill_out_of_range" | "annular_width" | "track_width" | "clearance" | "copper_edge_clearance" | "courtyards_overlap" | "courtyard_overlap")))+pub fn blocking_violation(v: &Value) -> bool {+ v["severity"] == "error" || (v["severity"] == "warning" && (matches!(v["type"].as_str(),+ Some("hole_to_hole" | "holes_co_located" | "drill_out_of_range" | "annular_width" | "track_width" | "clearance" | "copper_edge_clearance" | "courtyards_overlap" | "courtyard_overlap"))+ || v["type"].as_str().map(|t| SILK_BLOCKING.contains(&t)).unwrap_or(false))) } /// The gate counts errors and manufacturing warnings, matching inherited findings by identity.@@ -569,4 +575,12 @@ mod gate_tests { let moved=json!({"violations":[{"type":"hole_to_hole","severity":"warning","description":"too close","items":[{"pos":{"x":2,"y":2}}]}]}); assert_eq!(summarize(&candidate,Some(&moved))["newErrors"],1); }+ #[test]+ fn silkscreen_the_fab_would_clip_blocks() {+ // a test pad placed under a part's value text: KiCad grades it a warning, the fab clips the ink+ let over = json!({"type":"silk_over_copper","severity":"warning","description":"Silkscreen clipped by solder mask","items":[{"description":"PCB text '150k'"},{"description":"Pad 1 [VIN] of TP6"}]});+ let overlap = json!({"type":"silk_overlap","severity":"warning","description":"Silkscreen clearance"});+ let library = json!({"type":"lib_footprint_issues","severity":"warning","description":"library not enabled"});+ assert_eq!(summarize(&json!({"violations":[over, overlap, library]}),None)["newErrors"],2);+ } }
package.json+1−1@@ -86,4 +86,4 @@ "aiflow-simulate", "aiflow-test-plan" ]-}+}
tools/board-glb-copper.py+31−1@@ -460,7 +460,7 @@ def world_matrices(g): return wm, parent -LAYER_RE = re.compile(r'_(PCB|pad|pads|via|vias|copper|soldermask|silkscreen)(?:[-_.]\d+)?$', re.I)+LAYER_RE = re.compile(r'_(PCB|pad|pads|via|vias|copper|soldermask|silkscreen|silkclip)(?:[-_.]\d+)?$', re.I) def board_meshes(g, stem):@@ -685,6 +685,32 @@ def copper(args): g['meshes'].append({'name': f'{L["stem"]}_soldermask', 'primitives': prims, 'extras': {'adomBoardLayer': 'soldermask', 'adomGenerated': 'outline minus F.Mask/B.Mask apertures'}}) add_node_like(g, fr.pcb_node, len(g['meshes']) - 1, 'adom-aiflow soldermask') + # 2b. silkscreen is clipped at every mask opening, as the fab clips it (KiCad's STEP export keeps the whole ink, so a+ # pad under a text renders with ink on bare copper: a board no fab makes). The STEP's silk is compressed and cannot be+ # cut here, so the exposed copper is laid again just above the ink inside each opening: what the eye sees is the+ # fab's result, bare copper with the ink stopping at the mask edge. KiCad DRC's silk_over_copper is still the design+ # error to fix (adom-aiflow gate and finish block it); this keeps the render honest while it is open.+ rep['silkClip'] = {}+ for mi_s, ni_s in layers.get('silkscreen', []):+ s_top = fr.z_in_pcb(mi_s, ni_s, 'max'); s_bot = fr.z_in_pcb(mi_s, ni_s, 'min')+ mid = (pcb_top + pcb_bot) / 2+ prims = []+ for s, up in (('F', True), ('B', False)):+ if up and s_top < mid: continue+ if not up and s_bot > mid: continue+ area = L['openings'][s].intersection(L['copper'][s])+ if area.is_empty: continue+ z = (s_top + 0.003e-3) if up else (s_bot - 0.003e-3)+ pos, nrm, idx = triangulate(area, fr, z, up, np.eye(4))+ if idx:+ pr = add_mesh_data(g, bin_, pos, nrm, idx); pr['material'] = cu_mat; prims.append(pr)+ rep['silkClip'][s] = round(area.area, 3)+ if prims:+ g['meshes'].append({'name': f'{L["stem"]}_silkclip', 'primitives': prims,+ 'extras': {'adomBoardLayer': 'silkclip', 'adomGenerated': 'exposed copper above the silkscreen inside every mask opening (the fab clips silk there)'}})+ add_node_like(g, fr.pcb_node, len(g['meshes']) - 1, 'adom-aiflow silk clip')+ rep['changes'].append('silkscreen: clipped at the mask openings (exposed copper laid over the ink, as the fab prints it)')+ # 3. solder where paste exists n_paste = {'F': 0, 'B': 0} if args.paste == 'solder':@@ -843,6 +869,10 @@ def check(args): notes.append('no solder mask in the GLB') if mask_cov: notes.append(f'mask open at all {len(exposed)} exposed pad centres' if not covered else '')+ # silkscreen must stop at the mask openings, as the fab prints it+ if layers.get('silkscreen') and not layers.get('silkclip'):+ errors.append('the silkscreen is not clipped at the mask openings: ink over a test point or pad renders on bare copper, a board no fab makes')+ hints.append('Run adom-aiflow board-glb copper: it lays the exposed copper over the ink inside every mask opening. Then fix the design: KiCad DRC silk_over_copper (adom-aiflow gate and finish block it).') bare = [p['id'] for p in exposed if not p['paste']] rep = {'gate': 'adom-aiflow board-glb check', 'glb': str(args.glb), 'board': str(args.board), 'finish': fin, 'ok': not errors, 'errors': errors, 'notes': [n for n in notes if n], 'exposedPads': len(exposed), 'barePads': bare}