dsh-eda-mcp 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,67 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.2 — 2026-10-20
4
+
5
+ ### 覆铜调查结案(扩展 0.4.7 / 0.4.8 真机)
6
+
7
+ 结论:**这台构建(EDA 3.2.186 / pro-api 0.3.18.0bc12bb0)无法通过 API 创建铜面图元**。不是参数问题,证据链如下。
8
+
9
+ | 尝试 | 结果 |
10
+ |---|---|
11
+ | `pcb_PrimitivePour.create` 静态入口 × 8 种组合(单/复杂多边形、原始源码数组、空网络、层 1/2/字符串、全参数显式、100×100 与 300×300 mil) | 全部 `无法创建覆铜边框图元` |
12
+ | 实例入口 `new IPCB_PrimitivePour(...).create()`(0.4.7 新增,验证构造器在运行时可取) | 同样报错 |
13
+ | 填充方式 `solid`/`45grid`/`90grid`/`0`/`1`/`2`/`SOLID`/`GRID45`/`45GRID` | 全部报错 |
14
+ | `pcb_PrimitiveFill.create`(文档说明它同样带 `net` 参数)× 4 种(对象/原始数组/复杂源码/显式 fillMode) | 全部 `无法创建填充图元` |
15
+ | **控制组**:同一多边形管线写丝印折线 `pcb_PrimitivePolyline` / `pcb_draw_polyline` | 也写不出来(`getAll` 0 条),而 `pcb_get_polylines` 内部路径能读到既有板框 1 条 |
16
+
17
+ 外部资料也到此为止:`prodocs` 正文是前端渲染(HTML 只有导航),官方配方给的流程就是我们实现的那条(`createPolygon`/`createComplexPolygon` → pour → `rebuildCopperRegion`),搜 "无法创建覆铜边框图元" 无任何社区解法,应用 bundle 内也搜不到该字符串。
18
+
19
+ **因此撤掉 `pcb_create_copper_fill`**:它在这台构建上必然失败,留着就是给调用方挖坑。白名单里的 `pcb_MathPolygon.createComplexPolygon` 与 `pcb_PrimitiveFill.*` 保留(换构建可能有效),但不再包装成工具。
20
+
21
+ **正解**:覆铜在界面手工铺(GND → 顶层+底层 → 重新铺铜);`eda_design_audit` 会持续把缺覆铜记成 `copper-pour` warn,`eda_delivery_checklist` 也会带上它。
22
+
23
+
24
+ ### 通知堆叠 / 粘连(扩展 0.4.9)
25
+
26
+ 屏幕实测:三条提示叠在画布上("未连接…点击右侧按钮"、"扩展已加载 v0.4.8"、"已连接;端口 39009"),且"未连接"那条永不消失。
27
+
28
+ - 根因:`showToastMessage` 的 `timer: 0` 语义是**永不自动隐藏**,而 `SYS_Message` **没有任何隐藏或替换 API**;连接成功时也没有人撤掉它。
29
+ - 修法:未连接提示改成 `showFollowMouseTip(tip, 8000)`,并在 WebSocket `onopen` 里 `removeFollowMouseTip` 显式撤掉;`showToast` 一律传有限时长(默认 4 s);文案改为"菜单 DSH MCP → 连接 DSH MCP 服务器"(跟随提示没有按钮)。
30
+ - 线协议 **0.4.8 → 0.4.9**(纯 UI 层)。
31
+
32
+ ## 1.0.1 — 2026-10-20
33
+
34
+ 1.0.0 的真机回归修掉 4 个 bug(全部由"跑一遍"发现,不是靠读代码):
35
+
36
+ 1. **原理图几何审计误报 30 对重叠**(`eda_schematic_audit`)
37
+ `sch_PrimitiveComponent.getAll` 的列表里混着图框/标题栏图元,其包围盒是整张页面(实测 `0..1170 × 0..825`),于是与每个符号"重叠"。现在**丢弃面积超过页面 40% 的图元**并在 `droppedSheetPrimitives` 里报数量;复测:30 对误报 → **0 fail**(31 器件 / 30 个有效包围盒 / 丢弃 1 个)。
38
+
39
+ 2. **参数持久化"报成功但没写"**(`eda_design_intake`)
40
+ ① `dmt_Project.modifyProjectDescription` 在本机构建上**返回 false、不生效**(与 create/delete/copyProject 同类);② 我的调用还把文本当成 uuid 传。现在:先按官方签名 `(projectUuid, description)` 写并**回读校验**,失败则落到工程目录侧的**侧车文件** `.dsh-eda-spec/<uuid>.json`,`persisted` 只在真的读回成功后为 true,并新增 `persistVia`(`description` / `file` / `none`)与 `persistPath`。真机复测:`persisted=true via=file`。
41
+
42
+ 3. **PCB 机械检查是死代码**(`eda_design_audit`)
43
+ `pcb_PrimitiveComponent.getAll([undefined, undefined])` 在该构建上返回**空列表**(同样的 API 在 `pcb_get_components` 里能取到 29 个器件),导致器件重叠 / 出板框两项检查从未执行。改用与 `pcb_get_components` 相同的调用形态,并把 `counts.componentsRead` 暴露出来;复测:`componentsRead: 29`。
44
+
45
+ 4. **交付清单挂死超时**(`eda_delivery_checklist`)
46
+ 自动切文档后紧接着调网表导出会被页面加载阻塞 → MCP 超时。现在切文档后等待 1.5 s;同时把**跳过**与**失败**分开:新增 `verdict`(`pass` / `incomplete` / `fail`)、`skippedSteps`、`failedSteps`、`next`,默认仍自动切文档。真机复测:**一次调用 7.5 s,四步全过 `verdict=pass`**;`switchDocuments:false` 时如实报 `incomplete` 并指出缺哪个文档。
47
+
48
+ ### 回归结果(扩展 0.4.6 / 桥接 1.0.1)
49
+
50
+ | 项 | 结果 |
51
+ |---|---|
52
+ | `eda_schematic_audit` | ✅ 0 fail / 1 warn(12 个符号偏离 10 单位栅格)/ 1 skip(无标签锚点) |
53
+ | `eda_netlist_erc` | ✅ 15 项全执行,14 pass / 0 fail / 1 warn(USB-C SBU1/SBU2 未接) |
54
+ | `eda_design_intake` 持久化 | ✅ `persisted=true via=file` |
55
+ | `eda_design_audit`(PCB) | ✅ `componentsRead:29`、0 违规 / 4 警告(5V 线宽偏细、无覆铜、无丝印、无定位孔) |
56
+ | `eda_delivery_checklist` | ✅ `verdict=pass`、7.5 s、四步(ERC / BOM / PCB 审计 / Gerber 86.8 KB) |
57
+ | `eda_pcb_canvas_origin` | ✅ 偏移 `0,0`,画布↔数据坐标互转一致 |
58
+ | `eda_pcb_create_pour` | ❌ **仍失败**:单多边形与复杂多边形都被接受,但 `pcb_PrimitivePour.create` 对任何网络/层/填充方式一律拒绝 → 判定为**应用侧限制**(1.0.0 里我写"是我用错 API",据此修正) |
59
+
60
+ ### 已知遗留(需你决定)
61
+
62
+ - 侧车目录 `D:\文档\LCEDA-Pro\projects\.dsh-eda-spec\` 是回归测试写入的(1 个 json),清理需要你确认(它在你的工程目录内)。
63
+ - 覆铜只能手工:工具已把失败原因写准,`eda_design_audit` 持续报 `copper-pour` warn。
64
+
3
65
  ## 1.0.0 — 2026-10-20
4
66
 
5
67
  第一个稳定版。0.7.4 之后这一轮把"能画"推进到"画得对、画得像参考设计、交付有证据":工具 106 → **109**,单测 6 → **8 套**,线协议 0.4.2 → **0.4.6**。
@@ -146,7 +146,7 @@
146
146
  | eda_launch 带 project 没打开工程 | 命令行路径被应用忽略 | 用 eda_open_project({ name }) 打开 |
147
147
  | 关应用后改动丢失 | eda_close 走了强制路径 | 关前先保存;看返回的 forced |
148
148
  | run_code 读大 JSON 被截断 | 宿主 read 有单行上限 | 用 node:fs 读盘或分片读 |
149
- | 覆铜创建失败(无法创建覆铜边框图元) | 应用侧限制:polygonStage=ok(轮廓被接受)但 pours 被拒,与网络/填充方式/坐标无关 | 工具会重试一次并给出该诊断;手工在 PCB 页框选覆铜(GND,顶层+底层),design_audit 记为 warn |
149
+ | 覆铜/填充一律被拒(无法创建覆铜边框图元 / 无法创建填充图元) | 应用侧限制:静态+实例入口、单/复杂多边形、原始源码数组、6 种填充枚举、全参数组合全部被拒;同一多边形管线在本构建上连丝印折线都写不出来 | 不要在 API 层反复试:覆铜请在界面手工铺(GND、顶层+底层);design_audit 持续记 copper-pour warn |
150
150
  | pcb_draw_mounting_hole 报“无法创建焊盘图元” | pad 形状必填,且槽孔只能用 ELLIPSE | 已内建:圆孔 ELLIPSE(d,d),槽孔 ELLIPSE(长,宽)+hole[SLOT,宽,长];OBLONG/RECTANGLE 焊盘会被拒 |
151
151
  | pcb_PrimitiveString.getAll / pcb_PrimitivePad.getAll 返回 0 | 这两个 API 按层查询 | 传 layer:丝印 3/4,多层焊盘 12;不传参恒为 0 |
152
152
  | 按坐标算的越界没被发现 | 图框/原点假设与真实页面不一致 | 用 eda_get_page_info 的真实值 + eda_capture 导 PNG 看实际版面 |
@@ -239,6 +239,13 @@
239
239
  | `eda_delivery_checklist` | 一次串完 ERC + PCB 审计/DRC + Gerber + BOM,逐步 PASS/FAIL;`outPath` 落 Markdown 报告 | 交付前,代替"我说做完了" |
240
240
 
241
241
  **交付口径**:`eda_delivery_checklist` 返回 `ok:true` 才算可以下单;任何一步 FAIL 都要先修,不许口头绕过。报告里带时间戳与计数,可直接给人复核。
242
+ ### 交付清单怎么读(1.0.1 起)
243
+
244
+ - 一次调用会**自己切到原理图再切到 PCB**(`switchDocuments:false` 改按当前文档跑一半,另一半标注跳过原因);
245
+ - 返回 `verdict`:`pass`(四步全过)/ `incomplete`(有步骤因文档不对被跳过)/ `fail`(有步骤真失败);`ok` 等价于 `verdict==='pass'`;
246
+ - `skippedSteps` 与 `failedSteps` 分开列 —— **别把"没跑"当成"通过"**;四步全过实测约 **7.5 s**;
247
+ - 投板前最后一条命令就用它,`outPath` 落 Markdown 存档。
248
+
242
249
 
243
250
  **参数持久化**:`eda_design_intake({answers})` 会把答复写进**工程描述**(`[dsh-eda-spec] {...}`),下次会话调用 `eda_design_intake` 会自动读回,不用重新猜;`reset:true` 清空。
244
251
 
package/docs/tools.md CHANGED
@@ -53,7 +53,7 @@ Design limits enforced by these tools (track widths, via sizes, net naming) are
53
53
  | `eda_open_document` | Open/activate a document tab by uuid (dmt_EditorControl.openDocument). Works for a schematic page or a PCB, so tab-dependent APIs can run. | `documentUuid: string, splitScreenId?: string` |
54
54
  | `eda_get_net_labels` | List every label-like primitive on the active schematic page: attribute primitives (netlabel/netport/netflag) PLUS plain texts left by the createNetLabel fallback, so they can be audited and deleted. Reports kind/primitiveId/net per entry. | `parentPrimitiveId?: string` |
55
55
  | `eda_schematic_audit` | Geometry audit of the active schematic page (the netlist ERC only judges connectivity): symbols outside the frame, symbols overlapping the title block, body-on-body symbol overlap, off-grid placement, and labels anchored inside a symbol body. Read-only; pair it with eda_netlist_erc before routing. | `grid?: number, includeOk?: boolean` |
56
- | `eda_delivery_checklist` | Delivery checklist: run connectivity (eda_netlist_erc), PCB audit + DRC, Gerber export and BOM export in ONE read-only call and report per-step pass/fail with counts. Pass outPath to also write a Markdown report you can hand to a reviewer. Use it instead of claiming a board is finished. | `outPath?: string, pcbDrc?: boolean` |
56
+ | `eda_delivery_checklist` | Delivery checklist: run connectivity (eda_netlist_erc), PCB audit + DRC, Gerber export and BOM export in ONE read-only call and report per-step pass/fail with counts. It switches to the schematic and the PCB itself (switchDocuments:false keeps the current document and reports the other steps as skipped). verdict is pass / incomplete / fail; pass outPath to also write a Markdown report you can hand to a reviewer. | `outPath?: string, pcbDrc?: boolean, switchDocuments?: boolean` |
57
57
  | `eda_design_intake` | Requirement intake for a new board: call it BEFORE placing anything when the request is vague (for example: draw me an X board). Without answers it returns the questions in 6 sections (power, core, IO, mechanical, process, delivery), each marked critical or fallback-able with the reason it changes the drawing; pass answers:{questionId:value} to record them and see what is still missing. Ask the user with ask_user_question for the critical ones instead of guessing. | `answers?: object, reset?: boolean` |
58
58
  | `eda_netlist_erc` | Read-only electrical smoke test over the schematic NETLIST (no application DRC needed): duplicate designators, unconnected pins, single-endpoint nets, shorted parts, power rails and decoupling, USB D+/D- pair, crystal (two nets + load caps + reaches the MCU), reset RC, BOOT pull resistor, Type-C CC pull-downs. DRC can be clean while the circuit is still wrong; these failures all show up in the netlist. Pass includeOk:true to see passing checks too. | `includeOk?: boolean` |
59
59
  | `eda_pcb_canvas_origin` | Read (or align) the PCB canvas origin and convert points between the two coordinate spaces. JLCEDA displays CANVAS coordinates in its UI but every API takes DATA coordinates; the two differ by this offset, so a coordinate read off the screen will land somewhere else unless you convert or align. align:true calls setCanvasOrigin(0,0) so the display matches the API. | `align?: boolean, offsetX?: number, offsetY?: number, point?: object` |
@@ -212,6 +212,9 @@ function dshEdaMcpStatus() {
212
212
  'pcb_PrimitivePad.create',
213
213
  'pcb_PrimitivePad.getAll',
214
214
  'pcb_PrimitivePad.delete',
215
+ 'pcb_PrimitiveFill.create',
216
+ 'pcb_PrimitiveFill.getAll',
217
+ 'pcb_PrimitiveFill.delete',
215
218
  'pcb_PrimitivePour.create',
216
219
  'pcb_PrimitivePour.modify',
217
220
  'pcb_PrimitivePour.delete',
@@ -459,21 +462,54 @@ function dshEdaMcpStatus() {
459
462
  : undefined;
460
463
  if (!pourPolygon) pourPolygon = eda.pcb_MathPolygon.createPolygon(pourSource);
461
464
  if (!pourPolygon) throw new Error('dsh.internal.pourCreate: invalid polygon source ' + JSON.stringify(pourSource));
462
- var pour = await eda.pcb_PrimitivePour.create(
463
- optional(pourArgs[0]), optional(pourArgs[1]), pourPolygon, optional(pourArgs[3]), optional(pourArgs[4]),
464
- optional(pourArgs[5]), optional(pourArgs[6]), optional(pourArgs[7]), optional(pourArgs[8])
465
- );
465
+ // Route 1: the documented static factory.
466
+ var pourRoutes = [];
467
+ var pour = null;
468
+ var usedRoute = 'static';
469
+ try {
470
+ pour = await eda.pcb_PrimitivePour.create(
471
+ optional(pourArgs[0]), optional(pourArgs[1]), pourPolygon, optional(pourArgs[3]), optional(pourArgs[4]),
472
+ optional(pourArgs[5]), optional(pourArgs[6]), optional(pourArgs[7]), optional(pourArgs[8])
473
+ );
474
+ } catch (staticError) {
475
+ pourRoutes.push('static: ' + (staticError && staticError.message ? staticError.message : String(staticError)));
476
+ }
477
+ // Route 2: the instance entry point (@internal in the typings, but the class
478
+ // constructor is exported at runtime on some builds).
466
479
  if (!pour) {
467
- throw new Error('dsh.internal.pourCreate: pcb_PrimitivePour.create returned undefined (net/layer/polygon rejected; copper layers only)');
480
+ var PourCtor = null;
481
+ try {
482
+ if (typeof eda.IPCB_PrimitivePour === 'function') PourCtor = eda.IPCB_PrimitivePour;
483
+ else if (eda.pcb_PrimitivePour && typeof eda.pcb_PrimitivePour.constructor === 'function' && eda.pcb_PrimitivePour.constructor !== Object) PourCtor = eda.pcb_PrimitivePour.constructor;
484
+ } catch (ctorProbeError) { PourCtor = null; }
485
+ if (PourCtor !== null) {
486
+ usedRoute = 'instance';
487
+ try {
488
+ var pourInstance = new PourCtor(
489
+ optional(pourArgs[0]), optional(pourArgs[1]), pourPolygon, optional(pourArgs[3]), optional(pourArgs[4]),
490
+ optional(pourArgs[5]), optional(pourArgs[6]), optional(pourArgs[7]), optional(pourArgs[8])
491
+ );
492
+ pour = await pourInstance.create();
493
+ } catch (instanceError) {
494
+ pourRoutes.push('instance: ' + (instanceError && instanceError.message ? instanceError.message : String(instanceError)));
495
+ usedRoute = 'none';
496
+ }
497
+ } else {
498
+ pourRoutes.push('instance: no exported constructor');
499
+ usedRoute = 'none';
500
+ }
468
501
  }
469
- // A pour border alone leaves no copper: the region has to be rebuilt for the
470
- // fill (and the clearance against other nets) to be computed.
502
+ if (!pour) {
503
+ throw new Error('dsh.internal.pourCreate: every route refused (' + pourRoutes.join(' | ') + ')');
504
+ }
505
+ // A pour border alone leaves no copper: rebuild so the fill (and the clearance
506
+ // against other nets) is actually computed.
471
507
  var rebuilt = false;
472
508
  if (pour && typeof pour.rebuildCopperRegion === 'function') {
473
509
  try { await pour.rebuildCopperRegion(); rebuilt = true; } catch (error) { rebuilt = false; }
474
510
  }
475
511
  var pourJson = toJsonable(pour);
476
- if (pourJson && typeof pourJson === 'object') pourJson.rebuilt = rebuilt;
512
+ if (pourJson && typeof pourJson === 'object') { pourJson.rebuilt = rebuilt; pourJson.route = usedRoute; }
477
513
  return pourJson;
478
514
  }
479
515
  // Internal helper: long-running jobs (auto-routing / auto-layout) are started
@@ -579,6 +615,9 @@ function dshEdaMcpStatus() {
579
615
  function () {
580
616
  wsReady = true;
581
617
  lastPongAt = Date.now();
618
+ // The 'not connected' tip is sticky by design while we are offline; once the
619
+ // socket is up it must go away, otherwise it sits on the canvas forever.
620
+ hideConnectTip();
582
621
  sendObject({ type: 'hello', version: VERSION });
583
622
  }
584
623
  );
@@ -599,10 +638,11 @@ function dshEdaMcpStatus() {
599
638
  registerWebSocket();
600
639
  }
601
640
 
602
- function showToast(message, type) {
641
+ function showToast(message, type, seconds) {
603
642
  try {
604
643
  if (typeof eda !== 'undefined' && eda.sys_Message && typeof eda.sys_Message.showToastMessage === 'function') {
605
- eda.sys_Message.showToastMessage(message, type || 'info');
644
+ // Always pass a finite timer: omitting it (or 0) leaves the toast on screen forever.
645
+ eda.sys_Message.showToastMessage(message, type || 'info', seconds || 4);
606
646
  } else {
607
647
  console.log('[dsh-eda-mcp] ' + message);
608
648
  }
@@ -706,20 +746,30 @@ function dshEdaMcpStatus() {
706
746
  }
707
747
  }
708
748
 
749
+ // One removable tip instead of a stack of toasts: showToastMessage has no hide API
750
+ // (timer:0 meant "never"), so a stack of them used to pile up over the canvas and the
751
+ // '未连接' banner survived even after the connection was established (measured).
752
+ var CONNECT_TIP = 'DSH MCP 未连接:菜单 DSH MCP → 连接 DSH MCP 服务器(ws://127.0.0.1:39009/ws)';
753
+
754
+ function hideConnectTip() {
755
+ try {
756
+ if (typeof eda !== 'undefined' && eda.sys_Message && typeof eda.sys_Message.removeFollowMouseTip === 'function') {
757
+ eda.sys_Message.removeFollowMouseTip(CONNECT_TIP);
758
+ }
759
+ } catch (_) { /* ignore */ }
760
+ }
761
+
709
762
  function showConnectToast() {
710
763
  if (wsReady) return;
711
764
  try {
712
765
  if (typeof eda === 'undefined' || !eda.sys_Message) return;
713
- eda.sys_Message.showToastMessage(
714
- 'DSH MCP 未连接。点击右侧按钮连接本地桥接服务(ws://127.0.0.1:39009/ws)。',
715
- 'warning',
716
- 0,
717
- undefined,
718
- '连接 DSH MCP',
719
- "eda.sys_MessageBus.publishPublic('dsh-eda-mcp/connect', {})"
720
- );
766
+ if (typeof eda.sys_Message.showFollowMouseTip === 'function') {
767
+ eda.sys_Message.showFollowMouseTip(CONNECT_TIP, 8000);
768
+ return;
769
+ }
770
+ eda.sys_Message.showToastMessage(CONNECT_TIP, 'warning', 8);
721
771
  } catch (error) {
722
- console.error('[dsh-eda-mcp] connect toast failed:', error);
772
+ console.error('[dsh-eda-mcp] connect tip failed:', error);
723
773
  }
724
774
  }
725
775
 
@@ -2,7 +2,7 @@
2
2
  "uuid": "d5e0a1b2c3f4456789abcdef01234567",
3
3
  "name": "dsh-eda-bridge",
4
4
  "displayName": "DSH EDA MCP Bridge",
5
- "version": "0.4.6",
5
+ "version": "0.4.9",
6
6
  "description": "Local WebSocket bridge used by the dsh-eda-mcp plugin to let DeepSeek Harness draw schematics and drive PCB operations in JLCEDA Pro through the official extension API.",
7
7
  "entry": "./api-bridge.js",
8
8
  "publisher": "dsh-eda-mcp",
package/lib/bridge.mjs CHANGED
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
3
3
  import { execFileSync, spawn } from "node:child_process";
4
4
  import { statSync } from "node:fs";
5
5
  import { readFile } from "node:fs/promises";
6
- import { unlink, writeFile } from "node:fs/promises";
6
+ import { mkdir, unlink, writeFile } from "node:fs/promises";
7
7
  import { tmpdir } from "node:os";
8
8
  import { basename, join } from "node:path";
9
9
  import { createServer } from "node:http";
@@ -807,7 +807,7 @@ var WORKFLOW_GUIDE = WORKFLOW_SUMMARY + "\n\n" + [
807
807
  ].join("\n");
808
808
 
809
809
  // src/bridge/index.ts
810
- var VERSION = "0.4.6";
810
+ var VERSION = "0.4.9";
811
811
  var WS_PORT = Number(process.env.DSH_EDA_MCP_WS_PORT ?? 39009);
812
812
  var WS_HOST = "127.0.0.1";
813
813
  var ALLOW_RAW_API = (process.env.DSH_EDA_MCP_ALLOW_RAW_API ?? "1") !== "0";
@@ -1094,7 +1094,7 @@ var TOOLS = [
1094
1094
  { name: "pcb_draw_text", description: "Put text on a PCB layer - silkscreen by default (3 top silk / 4 bottom silk). Use it for pin functions, board markings, version and fabrication notes; silkscreen text must not cover pads.", inputSchema: objectSchema({ x: { type: "number" }, y: { type: "number" }, text: { type: "string" }, layer: { type: "string" }, fontSize: { type: "number" }, lineWidth: { type: "number" }, rotation: { type: "number" }, mirror: { type: "boolean" }, alignMode: { type: "integer" }, locked: { type: "boolean" } }, ["x", "y", "text"]) },
1095
1095
  { name: "pcb_draw_mounting_hole", description: 'Place a NON-plated mounting hole or a slot (pcb_PrimitivePad with metallization:false). shape "round" is a drill-sized hole, shape "slot" an oval cut-out. Use this for mounting holes and connector slots instead of hacking the board outline.', inputSchema: objectSchema({ x: { type: "number" }, y: { type: "number" }, diameter: { type: "number" }, shape: { type: "string", enum: ["round", "slot"] }, slotLength: { type: "number" }, rotation: { type: "number" }, plated: { type: "boolean" }, annulus: { type: "number" }, padNumber: { type: "string" }, layer: { type: "string" } }, ["x", "y", "diameter"]) },
1096
1096
  { name: "eda_schematic_audit", description: "Geometry audit of the active schematic page (the netlist ERC only judges connectivity): symbols outside the frame, symbols overlapping the title block, body-on-body symbol overlap, off-grid placement, and labels anchored inside a symbol body. Read-only; pair it with eda_netlist_erc before routing.", inputSchema: objectSchema({ grid: { type: "number" }, includeOk: { type: "boolean" } }) },
1097
- { name: "eda_delivery_checklist", description: "Delivery checklist: run connectivity (eda_netlist_erc), PCB audit + DRC, Gerber export and BOM export in ONE read-only call and report per-step pass/fail with counts. Pass outPath to also write a Markdown report you can hand to a reviewer. Use it instead of claiming a board is finished.", inputSchema: objectSchema({ outPath: { type: "string" }, pcbDrc: { type: "boolean" } }) },
1097
+ { name: "eda_delivery_checklist", description: "Delivery checklist: run connectivity (eda_netlist_erc), PCB audit + DRC, Gerber export and BOM export in ONE read-only call and report per-step pass/fail with counts. It switches to the schematic and the PCB itself (switchDocuments:false keeps the current document and reports the other steps as skipped). verdict is pass / incomplete / fail; pass outPath to also write a Markdown report you can hand to a reviewer.", inputSchema: objectSchema({ outPath: { type: "string" }, pcbDrc: { type: "boolean" }, switchDocuments: { type: "boolean" } }) },
1098
1098
  { name: "eda_design_intake", description: "Requirement intake for a new board: call it BEFORE placing anything when the request is vague (for example: draw me an X board). Without answers it returns the questions in 6 sections (power, core, IO, mechanical, process, delivery), each marked critical or fallback-able with the reason it changes the drawing; pass answers:{questionId:value} to record them and see what is still missing. Ask the user with ask_user_question for the critical ones instead of guessing.", inputSchema: objectSchema({ answers: { type: "object", additionalProperties: true }, reset: { type: "boolean" } }) },
1099
1099
  { name: "eda_netlist_erc", description: "Read-only electrical smoke test over the schematic NETLIST (no application DRC needed): duplicate designators, unconnected pins, single-endpoint nets, shorted parts, power rails and decoupling, USB D+/D- pair, crystal (two nets + load caps + reaches the MCU), reset RC, BOOT pull resistor, Type-C CC pull-downs. DRC can be clean while the circuit is still wrong; these failures all show up in the netlist. Pass includeOk:true to see passing checks too.", inputSchema: objectSchema({ includeOk: { type: "boolean" } }) },
1100
1100
  { name: "eda_pcb_canvas_origin", description: "Read (or align) the PCB canvas origin and convert points between the two coordinate spaces. JLCEDA displays CANVAS coordinates in its UI but every API takes DATA coordinates; the two differ by this offset, so a coordinate read off the screen will land somewhere else unless you convert or align. align:true calls setCanvasOrigin(0,0) so the display matches the API.", inputSchema: objectSchema({ align: { type: "boolean" }, offsetX: { type: "number" }, offsetY: { type: "number" }, point: { type: "object", additionalProperties: true } }) },
@@ -1509,6 +1509,13 @@ var BRIEF_TAG = "[dsh-eda-spec]";
1509
1509
  function briefBlock(brief) {
1510
1510
  return BRIEF_TAG + " " + JSON.stringify(brief);
1511
1511
  }
1512
+ function specFilePath(project) {
1513
+ const dir = String(project?.teamUuid ?? "");
1514
+ const uuid2 = String(project?.uuid ?? "");
1515
+ if (dir === "" || uuid2 === "") return "";
1516
+ if (!/^[A-Za-z]:[\\/]/.test(dir) && !dir.startsWith("/")) return "";
1517
+ return join(dir, ".dsh-eda-spec", uuid2 + ".json");
1518
+ }
1512
1519
  function parseBriefBlock(description) {
1513
1520
  const text = typeof description === "string" ? description : "";
1514
1521
  const index = text.indexOf(BRIEF_TAG);
@@ -1589,7 +1596,7 @@ async function auditPcb(args, timeoutMs) {
1589
1596
  const vias = await pcbPrimitiveList("pcb_PrimitiveVia.getAll", [netFilter, void 0], timeoutMs);
1590
1597
  const pours = await pcbPrimitiveList("pcb_PrimitivePour.getAll", [netFilter, void 0, void 0], timeoutMs);
1591
1598
  const outline = await pcbPrimitiveList("pcb_PrimitivePolyline.getAll", [void 0, PCB_LAYERS.boardOutline, void 0], timeoutMs);
1592
- const pcbComponents = await pcbPrimitiveList("pcb_PrimitiveComponent.getAll", [void 0, void 0], timeoutMs);
1599
+ const pcbComponents = await callExtension("pcb_PrimitiveComponent.getAll", [resolvePcbLayer(void 0), void 0], timeoutMs).then((raw) => Array.isArray(raw) ? raw : []).catch(() => []);
1593
1600
  const violations = [];
1594
1601
  const warnings = [];
1595
1602
  const add = (bucket, rule, message, samples) => {
@@ -1726,7 +1733,7 @@ async function auditPcb(args, timeoutMs) {
1726
1733
  ];
1727
1734
  return {
1728
1735
  ok: violations.length === 0,
1729
- counts: { tracks: tracks.length, vias: vias.length, pours: pours.length, outlinePolylines: outline.length, nets: netNames.length },
1736
+ counts: { tracks: tracks.length, vias: vias.length, pours: pours.length, outlinePolylines: outline.length, nets: netNames.length, componentsRead: pcbComponents.length },
1730
1737
  minWidthByNet: Object.fromEntries([...widthByNet.entries()].map(([net, width]) => [net, { widthMil: width, role: classifyNet(net), suggestedMil: suggestTrackWidthMil(net) }])),
1731
1738
  violations,
1732
1739
  warnings,
@@ -3078,23 +3085,33 @@ async function executeTool(name, rawArgs, timeoutMs) {
3078
3085
  }
3079
3086
  case "eda_schematic_audit": {
3080
3087
  const paper = paperOf(args.paper);
3088
+ const frame = paper.frame;
3089
+ const frameArea = (frame.right - frame.left) * (frame.top - frame.bottom);
3081
3090
  const rawList = await callExtension("sch_PrimitiveComponent.getAll", [], timeoutMs);
3082
3091
  const list = Array.isArray(rawList) ? rawList.slice(0, 200) : [];
3083
3092
  const components = [];
3093
+ let droppedSheetPrimitives = 0;
3084
3094
  for (let index = 0; index < list.length; index += 1) {
3085
3095
  const entry = list[index] ?? {};
3086
- const designator = String(entry.designator ?? entry.Designator ?? entry.name ?? "#" + index);
3096
+ const designator = String(entry.designator ?? entry.Designator ?? entry.Name ?? entry.name ?? "#" + index);
3087
3097
  const id = String(entry.primitiveId ?? entry.PrimitiveId ?? "");
3088
3098
  let bounds;
3089
3099
  if (id !== "") {
3090
3100
  const box = await bboxOf([id], timeoutMs);
3091
3101
  if (box !== void 0) bounds = { left: box.minX, right: box.maxX, bottom: box.minY, top: box.maxY };
3092
3102
  }
3103
+ if (bounds !== void 0) {
3104
+ const boundsArea = (bounds.right - bounds.left) * (bounds.top - bounds.bottom);
3105
+ if (boundsArea > frameArea * 0.4) {
3106
+ bounds = void 0;
3107
+ droppedSheetPrimitives += 1;
3108
+ }
3109
+ }
3093
3110
  components.push({ designator, x: Number(entry.x ?? entry.X ?? 0), y: Number(entry.y ?? entry.Y ?? 0), bounds });
3094
3111
  }
3095
3112
  const report = auditSchematicGeometry({
3096
3113
  components,
3097
- frame: paper.frame,
3114
+ frame,
3098
3115
  titleBlock: paper.titleBlock,
3099
3116
  grid: args.grid === void 0 ? 10 : Number(args.grid)
3100
3117
  });
@@ -3104,57 +3121,111 @@ async function executeTool(name, rawArgs, timeoutMs) {
3104
3121
  counts: report.counts,
3105
3122
  componentCount: report.componentCount,
3106
3123
  boxesRead: components.filter((component) => component.bounds !== void 0).length,
3107
- checks
3124
+ droppedSheetPrimitives,
3125
+ checks,
3126
+ notes: droppedSheetPrimitives > 0 ? ["Skipped " + droppedSheetPrimitives + " primitive(s) whose box covers the whole sheet (frame/title block); counting them made every symbol look overlapped."] : []
3108
3127
  };
3109
3128
  }
3110
3129
  case "eda_delivery_checklist": {
3111
3130
  const steps = [];
3112
3131
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3132
+ const project = await callExtension("dmt_Project.getCurrentProjectInfo", [], timeoutMs).catch(() => null);
3133
+ const boards = Array.isArray(project?.data) ? project.data : [];
3134
+ const schematicPage = boards[0]?.schematic?.page?.[0]?.uuid ?? "";
3135
+ const pcbUuid = boards.find((board) => board?.pcb?.uuid)?.pcb?.uuid ?? "";
3136
+ const switchDocuments = args.switchDocuments !== false;
3137
+ const documentType = async () => {
3138
+ try {
3139
+ const info = await callExtension("dmt_SelectControl.getCurrentDocumentInfo", [], timeoutMs);
3140
+ return Number(info?.documentType);
3141
+ } catch {
3142
+ return NaN;
3143
+ }
3144
+ };
3145
+ const activate = async (uuid2) => {
3146
+ if (typeof uuid2 !== "string" || uuid2 === "") return false;
3147
+ try {
3148
+ await callExtension("dmt_EditorControl.openDocument", [uuid2], timeoutMs);
3149
+ await new Promise((resolve) => setTimeout(resolve, 1500));
3150
+ return true;
3151
+ } catch {
3152
+ return false;
3153
+ }
3154
+ };
3113
3155
  let netlistErc = null;
3114
- try {
3115
- const parsedNetlist = await fetchParsedNetlist(timeoutMs);
3116
- netlistErc = runNetlistErc(parsedNetlist);
3117
- steps.push({ step: "netlist-erc", ok: netlistErc.ok, detail: netlistErc.counts, failing: netlistErc.checks.filter((check) => check.status === "fail").map((check) => check.id) });
3118
- } catch (error) {
3119
- steps.push({ step: "netlist-erc", ok: false, error: error instanceof Error ? error.message : String(error) });
3156
+ const onSchematic = switchDocuments ? await activate(schematicPage) : await documentType() === 1;
3157
+ if (!onSchematic) {
3158
+ steps.push({ step: "netlist-erc", document: "schematic", ok: false, skipped: "schematic page is not the active document" });
3159
+ steps.push({ step: "bom", document: "schematic", ok: false, skipped: "schematic page is not the active document" });
3160
+ } else {
3161
+ try {
3162
+ const parsedNetlist = await fetchParsedNetlist(timeoutMs);
3163
+ netlistErc = runNetlistErc(parsedNetlist);
3164
+ steps.push({
3165
+ step: "netlist-erc",
3166
+ document: "schematic",
3167
+ ok: netlistErc.ok,
3168
+ detail: netlistErc.counts,
3169
+ failing: netlistErc.checks.filter((check) => check.status === "fail").map((check) => check.id)
3170
+ });
3171
+ } catch (error) {
3172
+ steps.push({ step: "netlist-erc", document: "schematic", ok: false, error: error instanceof Error ? error.message : String(error) });
3173
+ }
3174
+ }
3175
+ let bom = null;
3176
+ if (onSchematic) {
3177
+ try {
3178
+ const payload = await callExtension("sch_ManufactureData.getBomFile", [void 0, "csv"], timeoutMs);
3179
+ const text = typeof payload === "string" ? payload : typeof payload?.text === "string" ? payload.text : "";
3180
+ const lines = text === "" ? 0 : text.split(/\r?\n/).filter((line) => line.trim() !== "").length;
3181
+ bom = { rows: Math.max(0, lines - 1), ok: lines > 1 };
3182
+ steps.push({ step: "bom", document: "schematic", ok: bom.ok, detail: { rows: bom.rows }, ...bom.ok ? {} : { error: "no BOM rows exported" } });
3183
+ } catch (error) {
3184
+ steps.push({ step: "bom", document: "schematic", ok: false, error: error instanceof Error ? error.message : String(error) });
3185
+ }
3120
3186
  }
3187
+ const onPcb = switchDocuments ? await activate(pcbUuid) : await documentType() === 3;
3121
3188
  let pcbAudit = null;
3122
- try {
3123
- pcbAudit = await auditPcb({ drc: args.pcbDrc !== false }, timeoutMs);
3124
- steps.push({
3125
- step: "pcb-audit",
3126
- ok: Array.isArray(pcbAudit.violations) && pcbAudit.violations.length === 0,
3127
- detail: { violations: pcbAudit.violations?.length ?? null, warnings: pcbAudit.warnings?.length ?? null, drc: pcbAudit.drc },
3128
- failing: (pcbAudit.violations ?? []).map((entry) => entry.rule)
3129
- });
3130
- } catch (error) {
3131
- steps.push({ step: "pcb-audit", ok: false, error: error instanceof Error ? error.message : String(error) });
3189
+ if (!onPcb) {
3190
+ steps.push({ step: "pcb-audit", document: "pcb", ok: false, skipped: "PCB is not the active document" });
3191
+ steps.push({ step: "gerber", document: "pcb", ok: false, skipped: "PCB is not the active document" });
3192
+ } else {
3193
+ try {
3194
+ pcbAudit = await auditPcb({ drc: args.pcbDrc !== false }, timeoutMs);
3195
+ steps.push({
3196
+ step: "pcb-audit",
3197
+ document: "pcb",
3198
+ ok: (pcbAudit.violations ?? []).length === 0,
3199
+ detail: { violations: pcbAudit.violations?.length ?? null, warnings: pcbAudit.warnings?.length ?? null, drc: pcbAudit.drc },
3200
+ failing: (pcbAudit.violations ?? []).map((entry) => entry.rule)
3201
+ });
3202
+ } catch (error) {
3203
+ steps.push({ step: "pcb-audit", document: "pcb", ok: false, error: error instanceof Error ? error.message : String(error) });
3204
+ }
3132
3205
  }
3133
3206
  let gerber = null;
3134
- try {
3135
- const payload = await callExtension("pcb_ManufactureData.getGerberFile", [void 0, void 0, void 0, void 0, void 0, void 0], timeoutMs);
3136
- const bytes = payload && typeof payload === "object" ? Number(payload.size ?? payload.bytes ?? 0) : typeof payload === "string" ? payload.length : 0;
3137
- gerber = { bytes, ok: bytes > 1e3 };
3138
- steps.push({ step: "gerber", ok: gerber.ok, detail: { bytes } });
3139
- } catch (error) {
3140
- steps.push({ step: "gerber", ok: false, error: error instanceof Error ? error.message : String(error) });
3141
- }
3142
- let bom = null;
3143
- try {
3144
- const payload = await callExtension("sch_ManufactureData.getBomFile", [void 0, "csv"], timeoutMs);
3145
- const text = typeof payload === "string" ? payload : typeof payload?.text === "string" ? payload.text : "";
3146
- const lines = text === "" ? 0 : text.split(/\r?\n/).filter((line) => line.trim() !== "").length;
3147
- bom = { rows: Math.max(0, lines - 1), ok: lines > 1 };
3148
- steps.push({ step: "bom", ok: bom.ok, detail: { rows: bom.rows } });
3149
- } catch (error) {
3150
- steps.push({ step: "bom", ok: false, error: error instanceof Error ? error.message : String(error) });
3207
+ if (onPcb) {
3208
+ try {
3209
+ const payload = await callExtension("pcb_ManufactureData.getGerberFile", [void 0, void 0, void 0, void 0, void 0, void 0], timeoutMs);
3210
+ const bytes = payload && typeof payload === "object" ? Number(payload.size ?? payload.bytes ?? 0) : typeof payload === "string" ? payload.length : 0;
3211
+ gerber = { bytes, ok: bytes > 1e3 };
3212
+ steps.push({ step: "gerber", document: "pcb", ok: gerber.ok, detail: { bytes } });
3213
+ } catch (error) {
3214
+ steps.push({ step: "gerber", document: "pcb", ok: false, error: error instanceof Error ? error.message : String(error) });
3215
+ }
3151
3216
  }
3152
- const failed = steps.filter((step) => step.ok !== true);
3217
+ const failedSteps = steps.filter((step) => step.ok !== true && step.skipped === void 0);
3218
+ const skippedSteps = steps.filter((step) => step.ok !== true && step.skipped !== void 0);
3219
+ const verdict = failedSteps.length > 0 ? "fail" : skippedSteps.length > 0 ? "incomplete" : "pass";
3153
3220
  const report = {
3154
- ok: failed.length === 0,
3221
+ ok: verdict === "pass",
3222
+ verdict,
3223
+ failedSteps: failedSteps.map((step) => step.step),
3224
+ skippedSteps: skippedSteps.map((step) => step.step),
3225
+ next: verdict === "pass" ? "Every step passed: the board can be ordered." : verdict === "incomplete" ? "Run the checklist again with the other document active (" + skippedSteps.map((step) => step.document).filter((value, index, all) => all.indexOf(value) === index).join(", ") + " needed)." : "Fix the failing steps first; skipped steps are listed separately.",
3155
3226
  startedAt,
3156
3227
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
3157
- steps,
3228
+ documents: { schematicPage, pcb: pcbUuid, switched: switchDocuments },
3158
3229
  netlistErc: netlistErc === null ? null : { ok: netlistErc.ok, counts: netlistErc.counts, componentCount: netlistErc.componentCount, netCount: netlistErc.netCount },
3159
3230
  pcbAudit: pcbAudit === null ? null : { violations: pcbAudit.violations, warnings: pcbAudit.warnings },
3160
3231
  gerber,
@@ -3168,10 +3239,12 @@ async function executeTool(name, rawArgs, timeoutMs) {
3168
3239
  "- \u5F00\u59CB\uFF1A" + report.startedAt,
3169
3240
  "- \u7ED3\u675F\uFF1A" + report.finishedAt,
3170
3241
  "- \u7ED3\u8BBA\uFF1A" + (report.ok ? "PASS\uFF08\u53EF\u4EE5\u4E0B\u5355\uFF09" : "FAIL\uFF08\u6709\u672A\u901A\u8FC7\u9879\uFF09"),
3242
+ "- \u539F\u7406\u56FE\u9875\uFF1A" + (schematicPage || "\u672A\u53D6\u5230"),
3243
+ "- PCB\uFF1A" + (pcbUuid || "\u672A\u53D6\u5230"),
3171
3244
  "",
3172
- "| \u6B65\u9AA4 | \u7ED3\u679C | \u8BF4\u660E |",
3173
- "|---|---|---|",
3174
- ...steps.map((step) => "| " + step.step + " | " + (step.ok === true ? "PASS" : "FAIL") + " | " + JSON.stringify(step.detail ?? step.error ?? {}) + " |"),
3245
+ "| \u6B65\u9AA4 | \u6587\u6863 | \u7ED3\u679C | \u8BF4\u660E |",
3246
+ "|---|---|---|---|",
3247
+ ...steps.map((step) => "| " + step.step + " | " + step.document + " | " + (step.ok === true ? "PASS" : "FAIL") + " | " + JSON.stringify(step.detail ?? step.error ?? {}) + " |"),
3175
3248
  "",
3176
3249
  "## \u7535\u6C14\u68C0\u67E5\uFF08eda_netlist_erc\uFF09",
3177
3250
  "",
@@ -3212,17 +3285,43 @@ async function executeTool(name, rawArgs, timeoutMs) {
3212
3285
  const merged = { ...designBrief ?? {}, ...answersReport.brief };
3213
3286
  const mergedReport = buildIntake(merged);
3214
3287
  let persisted = false;
3288
+ let persistVia = "none";
3289
+ let persistPath = "";
3215
3290
  if (Object.keys(merged).length > 0) {
3216
3291
  designBrief = merged;
3217
3292
  if (answers !== null) {
3218
3293
  try {
3219
3294
  const project = await callExtension("dmt_Project.getCurrentProjectInfo", [], timeoutMs);
3295
+ const projectUuid = String(project?.uuid ?? "");
3220
3296
  const existing = typeof project?.description === "string" ? project.description : "";
3221
3297
  const index = existing.indexOf(BRIEF_TAG);
3222
3298
  const base = index < 0 ? existing : existing.slice(0, index).trimEnd();
3223
3299
  const next = (base === "" ? "" : base + "\n") + briefBlock(merged);
3224
- await callExtension("dmt_Project.modifyProjectDescription", [next], timeoutMs);
3225
- persisted = true;
3300
+ let stored = false;
3301
+ if (projectUuid !== "") {
3302
+ try {
3303
+ await callExtension("dmt_Project.modifyProjectDescription", [projectUuid, next], timeoutMs);
3304
+ const readBack = await callExtension("dmt_Project.getCurrentProjectInfo", [], timeoutMs);
3305
+ stored = String(readBack?.description ?? "").includes(BRIEF_TAG);
3306
+ if (stored) persistVia = "description";
3307
+ } catch {
3308
+ stored = false;
3309
+ }
3310
+ }
3311
+ if (!stored) {
3312
+ const sidecar = specFilePath(project);
3313
+ if (sidecar !== "") {
3314
+ await mkdir(join(sidecar, ".."), { recursive: true });
3315
+ await writeFile(sidecar, JSON.stringify({ projectUuid, brief: merged }, null, 2) + "\n");
3316
+ const check = JSON.parse(await readFile(sidecar, "utf8"));
3317
+ stored = check !== null && typeof check === "object" && Object.keys(merged).every((key) => check.brief?.[key] === merged[key]);
3318
+ if (stored) {
3319
+ persistVia = "file";
3320
+ persistPath = sidecar;
3321
+ }
3322
+ }
3323
+ }
3324
+ persisted = stored === true;
3226
3325
  } catch {
3227
3326
  persisted = false;
3228
3327
  }
@@ -3238,6 +3337,8 @@ async function executeTool(name, rawArgs, timeoutMs) {
3238
3337
  brief: mergedReport.brief,
3239
3338
  hydratedFromProject: answers === null && designBrief !== null,
3240
3339
  persisted,
3340
+ persistVia,
3341
+ persistPath,
3241
3342
  sections: mergedReport.sections.filter((section) => section.questions.some((question) => mergedReport.brief[question.id] === void 0)),
3242
3343
  next: mergedReport.ready ? "All critical parameters are known: lay out the schematic, then eda_netlist_erc before routing." : "Ask the user for the missing critical parameters with ask_user_question, grouped by section, before placing parts. Do not invent values."
3243
3344
  };
@@ -3682,7 +3783,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
3682
3783
  raw = await attempt("solid", false);
3683
3784
  } catch (secondFailure) {
3684
3785
  const detail = secondFailure instanceof Error ? secondFailure.message : String(secondFailure);
3685
- throw new Error("eda_pcb_create_pour failed twice (first: " + (firstFailure instanceof Error ? firstFailure.message : String(firstFailure)) + "; retry: " + detail + "). polygonStage=" + (polygonOk ? "ok" : "failed") + ": " + (polygonOk ? "the app accepted the outline but refused the pour itself, so this is an application limit, not a coordinate problem. Pour by hand in the PCB editor (\u8986\u94DC \u2192 select GND \u2192 top + bottom); eda_design_audit reports it as a warn until then." : "the outline polygon was rejected too: check that every point is inside the board outline and flat [x1,y1,...].") + " retried=" + retried);
3786
+ throw new Error("eda_pcb_create_pour failed twice (first: " + (firstFailure instanceof Error ? firstFailure.message : String(firstFailure)) + "; retry: " + detail + "). polygonStage=" + (polygonOk ? "ok" : "failed") + ": " + (polygonOk ? "the app accepted the outline but refused the pour itself, and so does the complex polygon the docs require (createComplexPolygon); single and complex polygons, every net/layer/fill combination, all refused, so pour creation is an application limit on this build, not a coordinate problem. Pour by hand in the PCB editor (\u8986\u94DC \u2192 select GND \u2192 top + bottom); eda_design_audit reports it as a warn until then." : "the outline polygon was rejected too: check that every point is inside the board outline and flat [x1,y1,...].") + " retried=" + retried);
3686
3787
  }
3687
3788
  }
3688
3789
  return {
package/lib/index.js CHANGED
@@ -179,7 +179,7 @@ var SKILL_PLAYBOOK_LINES = [
179
179
  "| eda_launch \u5E26 project \u6CA1\u6253\u5F00\u5DE5\u7A0B | \u547D\u4EE4\u884C\u8DEF\u5F84\u88AB\u5E94\u7528\u5FFD\u7565 | \u7528 eda_open_project({ name }) \u6253\u5F00 |",
180
180
  "| \u5173\u5E94\u7528\u540E\u6539\u52A8\u4E22\u5931 | eda_close \u8D70\u4E86\u5F3A\u5236\u8DEF\u5F84 | \u5173\u524D\u5148\u4FDD\u5B58\uFF1B\u770B\u8FD4\u56DE\u7684 forced |",
181
181
  "| run_code \u8BFB\u5927 JSON \u88AB\u622A\u65AD | \u5BBF\u4E3B read \u6709\u5355\u884C\u4E0A\u9650 | \u7528 node:fs \u8BFB\u76D8\u6216\u5206\u7247\u8BFB |",
182
- "| \u8986\u94DC\u521B\u5EFA\u5931\u8D25\uFF08\u65E0\u6CD5\u521B\u5EFA\u8986\u94DC\u8FB9\u6846\u56FE\u5143\uFF09 | \u5E94\u7528\u4FA7\u9650\u5236\uFF1ApolygonStage=ok\uFF08\u8F6E\u5ED3\u88AB\u63A5\u53D7\uFF09\u4F46 pours \u88AB\u62D2\uFF0C\u4E0E\u7F51\u7EDC/\u586B\u5145\u65B9\u5F0F/\u5750\u6807\u65E0\u5173 | \u5DE5\u5177\u4F1A\u91CD\u8BD5\u4E00\u6B21\u5E76\u7ED9\u51FA\u8BE5\u8BCA\u65AD\uFF1B\u624B\u5DE5\u5728 PCB \u9875\u6846\u9009\u8986\u94DC\uFF08GND\uFF0C\u9876\u5C42+\u5E95\u5C42\uFF09\uFF0Cdesign_audit \u8BB0\u4E3A warn |",
182
+ "| \u8986\u94DC/\u586B\u5145\u4E00\u5F8B\u88AB\u62D2\uFF08\u65E0\u6CD5\u521B\u5EFA\u8986\u94DC\u8FB9\u6846\u56FE\u5143 / \u65E0\u6CD5\u521B\u5EFA\u586B\u5145\u56FE\u5143\uFF09 | \u5E94\u7528\u4FA7\u9650\u5236\uFF1A\u9759\u6001+\u5B9E\u4F8B\u5165\u53E3\u3001\u5355/\u590D\u6742\u591A\u8FB9\u5F62\u3001\u539F\u59CB\u6E90\u7801\u6570\u7EC4\u30016 \u79CD\u586B\u5145\u679A\u4E3E\u3001\u5168\u53C2\u6570\u7EC4\u5408\u5168\u90E8\u88AB\u62D2\uFF1B\u540C\u4E00\u591A\u8FB9\u5F62\u7BA1\u7EBF\u5728\u672C\u6784\u5EFA\u4E0A\u8FDE\u4E1D\u5370\u6298\u7EBF\u90FD\u5199\u4E0D\u51FA\u6765 | \u4E0D\u8981\u5728 API \u5C42\u53CD\u590D\u8BD5\uFF1A\u8986\u94DC\u8BF7\u5728\u754C\u9762\u624B\u5DE5\u94FA\uFF08GND\u3001\u9876\u5C42+\u5E95\u5C42\uFF09\uFF1Bdesign_audit \u6301\u7EED\u8BB0 copper-pour warn |",
183
183
  "| pcb_draw_mounting_hole \u62A5\u201C\u65E0\u6CD5\u521B\u5EFA\u710A\u76D8\u56FE\u5143\u201D | pad \u5F62\u72B6\u5FC5\u586B\uFF0C\u4E14\u69FD\u5B54\u53EA\u80FD\u7528 ELLIPSE | \u5DF2\u5185\u5EFA\uFF1A\u5706\u5B54 ELLIPSE(d,d)\uFF0C\u69FD\u5B54 ELLIPSE(\u957F,\u5BBD)+hole[SLOT,\u5BBD,\u957F]\uFF1BOBLONG/RECTANGLE \u710A\u76D8\u4F1A\u88AB\u62D2 |",
184
184
  "| pcb_PrimitiveString.getAll / pcb_PrimitivePad.getAll \u8FD4\u56DE 0 | \u8FD9\u4E24\u4E2A API \u6309\u5C42\u67E5\u8BE2 | \u4F20 layer\uFF1A\u4E1D\u5370 3/4\uFF0C\u591A\u5C42\u710A\u76D8 12\uFF1B\u4E0D\u4F20\u53C2\u6052\u4E3A 0 |",
185
185
  "| \u6309\u5750\u6807\u7B97\u7684\u8D8A\u754C\u6CA1\u88AB\u53D1\u73B0 | \u56FE\u6846/\u539F\u70B9\u5047\u8BBE\u4E0E\u771F\u5B9E\u9875\u9762\u4E0D\u4E00\u81F4 | \u7528 eda_get_page_info \u7684\u771F\u5B9E\u503C + eda_capture \u5BFC PNG \u770B\u5B9E\u9645\u7248\u9762 |",
@@ -272,6 +272,13 @@ var SKILL_PLAYBOOK_LINES = [
272
272
  '| `eda_delivery_checklist` | \u4E00\u6B21\u4E32\u5B8C ERC + PCB \u5BA1\u8BA1/DRC + Gerber + BOM\uFF0C\u9010\u6B65 PASS/FAIL\uFF1B`outPath` \u843D Markdown \u62A5\u544A | \u4EA4\u4ED8\u524D\uFF0C\u4EE3\u66FF"\u6211\u8BF4\u505A\u5B8C\u4E86" |',
273
273
  "",
274
274
  "**\u4EA4\u4ED8\u53E3\u5F84**\uFF1A`eda_delivery_checklist` \u8FD4\u56DE `ok:true` \u624D\u7B97\u53EF\u4EE5\u4E0B\u5355\uFF1B\u4EFB\u4F55\u4E00\u6B65 FAIL \u90FD\u8981\u5148\u4FEE\uFF0C\u4E0D\u8BB8\u53E3\u5934\u7ED5\u8FC7\u3002\u62A5\u544A\u91CC\u5E26\u65F6\u95F4\u6233\u4E0E\u8BA1\u6570\uFF0C\u53EF\u76F4\u63A5\u7ED9\u4EBA\u590D\u6838\u3002",
275
+ "### \u4EA4\u4ED8\u6E05\u5355\u600E\u4E48\u8BFB\uFF081.0.1 \u8D77\uFF09",
276
+ "",
277
+ "- \u4E00\u6B21\u8C03\u7528\u4F1A**\u81EA\u5DF1\u5207\u5230\u539F\u7406\u56FE\u518D\u5207\u5230 PCB**\uFF08`switchDocuments:false` \u6539\u6309\u5F53\u524D\u6587\u6863\u8DD1\u4E00\u534A\uFF0C\u53E6\u4E00\u534A\u6807\u6CE8\u8DF3\u8FC7\u539F\u56E0\uFF09\uFF1B",
278
+ "- \u8FD4\u56DE `verdict`\uFF1A`pass`\uFF08\u56DB\u6B65\u5168\u8FC7\uFF09/ `incomplete`\uFF08\u6709\u6B65\u9AA4\u56E0\u6587\u6863\u4E0D\u5BF9\u88AB\u8DF3\u8FC7\uFF09/ `fail`\uFF08\u6709\u6B65\u9AA4\u771F\u5931\u8D25\uFF09\uFF1B`ok` \u7B49\u4EF7\u4E8E `verdict==='pass'`\uFF1B",
279
+ '- `skippedSteps` \u4E0E `failedSteps` \u5206\u5F00\u5217 \u2014\u2014 **\u522B\u628A"\u6CA1\u8DD1"\u5F53\u6210"\u901A\u8FC7"**\uFF1B\u56DB\u6B65\u5168\u8FC7\u5B9E\u6D4B\u7EA6 **7.5 s**\uFF1B',
280
+ "- \u6295\u677F\u524D\u6700\u540E\u4E00\u6761\u547D\u4EE4\u5C31\u7528\u5B83\uFF0C`outPath` \u843D Markdown \u5B58\u6863\u3002",
281
+ "",
275
282
  "",
276
283
  "**\u53C2\u6570\u6301\u4E45\u5316**\uFF1A`eda_design_intake({answers})` \u4F1A\u628A\u7B54\u590D\u5199\u8FDB**\u5DE5\u7A0B\u63CF\u8FF0**\uFF08`[dsh-eda-spec] {...}`\uFF09\uFF0C\u4E0B\u6B21\u4F1A\u8BDD\u8C03\u7528 `eda_design_intake` \u4F1A\u81EA\u52A8\u8BFB\u56DE\uFF0C\u4E0D\u7528\u91CD\u65B0\u731C\uFF1B`reset:true` \u6E05\u7A7A\u3002",
277
284
  "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-eda-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "DeepSeek Harness (DSH) plugin — connects DSH to JLCEDA Pro (EasyEDA Pro) via a local MCP bridge and draws schematics through the JLCEDA extension API.",
5
5
  "license": "Apache-2.0",
6
6
  "author": {
@@ -3,7 +3,7 @@ import { createHash } from 'node:crypto'
3
3
  import { execFileSync, spawn } from 'node:child_process'
4
4
  import { existsSync, statSync } from 'node:fs'
5
5
  import { readFile } from 'node:fs/promises'
6
- import { unlink, writeFile } from 'node:fs/promises'
6
+ import { mkdir, unlink, writeFile } from 'node:fs/promises'
7
7
  import { tmpdir } from 'node:os'
8
8
  import { basename, join } from 'node:path'
9
9
  import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'
@@ -21,7 +21,7 @@ import {
21
21
  } from './design-rules.ts'
22
22
  import { WORKFLOW_GUIDE, WORKFLOW_SUMMARY } from './workflow-guide.ts'
23
23
 
24
- const VERSION = '0.4.6'
24
+ const VERSION = '0.4.9'
25
25
  const WS_PORT = Number(process.env.DSH_EDA_MCP_WS_PORT ?? 39009)
26
26
  const WS_HOST = '127.0.0.1'
27
27
  const ALLOW_RAW_API = (process.env.DSH_EDA_MCP_ALLOW_RAW_API ?? '1') !== '0'
@@ -318,7 +318,7 @@ const TOOLS = [
318
318
  { name: 'pcb_draw_text', description: 'Put text on a PCB layer - silkscreen by default (3 top silk / 4 bottom silk). Use it for pin functions, board markings, version and fabrication notes; silkscreen text must not cover pads.', inputSchema: objectSchema({ x: { type: 'number' }, y: { type: 'number' }, text: { type: 'string' }, layer: { type: 'string' }, fontSize: { type: 'number' }, lineWidth: { type: 'number' }, rotation: { type: 'number' }, mirror: { type: 'boolean' }, alignMode: { type: 'integer' }, locked: { type: 'boolean' } }, ['x', 'y', 'text']) },
319
319
  { name: 'pcb_draw_mounting_hole', description: 'Place a NON-plated mounting hole or a slot (pcb_PrimitivePad with metallization:false). shape "round" is a drill-sized hole, shape "slot" an oval cut-out. Use this for mounting holes and connector slots instead of hacking the board outline.', inputSchema: objectSchema({ x: { type: 'number' }, y: { type: 'number' }, diameter: { type: 'number' }, shape: { type: 'string', enum: ['round', 'slot'] }, slotLength: { type: 'number' }, rotation: { type: 'number' }, plated: { type: 'boolean' }, annulus: { type: 'number' }, padNumber: { type: 'string' }, layer: { type: 'string' } }, ['x', 'y', 'diameter']) },
320
320
  { name: 'eda_schematic_audit', description: "Geometry audit of the active schematic page (the netlist ERC only judges connectivity): symbols outside the frame, symbols overlapping the title block, body-on-body symbol overlap, off-grid placement, and labels anchored inside a symbol body. Read-only; pair it with eda_netlist_erc before routing.", inputSchema: objectSchema({ grid: { type: 'number' }, includeOk: { type: 'boolean' } }) },
321
- { name: 'eda_delivery_checklist', description: "Delivery checklist: run connectivity (eda_netlist_erc), PCB audit + DRC, Gerber export and BOM export in ONE read-only call and report per-step pass/fail with counts. Pass outPath to also write a Markdown report you can hand to a reviewer. Use it instead of claiming a board is finished.", inputSchema: objectSchema({ outPath: { type: 'string' }, pcbDrc: { type: 'boolean' } }) },
321
+ { name: 'eda_delivery_checklist', description: "Delivery checklist: run connectivity (eda_netlist_erc), PCB audit + DRC, Gerber export and BOM export in ONE read-only call and report per-step pass/fail with counts. It switches to the schematic and the PCB itself (switchDocuments:false keeps the current document and reports the other steps as skipped). verdict is pass / incomplete / fail; pass outPath to also write a Markdown report you can hand to a reviewer.", inputSchema: objectSchema({ outPath: { type: 'string' }, pcbDrc: { type: 'boolean' }, switchDocuments: { type: 'boolean' } }) },
322
322
  { name: 'eda_design_intake', description: "Requirement intake for a new board: call it BEFORE placing anything when the request is vague (for example: draw me an X board). Without answers it returns the questions in 6 sections (power, core, IO, mechanical, process, delivery), each marked critical or fallback-able with the reason it changes the drawing; pass answers:{questionId:value} to record them and see what is still missing. Ask the user with ask_user_question for the critical ones instead of guessing.", inputSchema: objectSchema({ answers: { type: 'object', additionalProperties: true }, reset: { type: 'boolean' } }) },
323
323
  { name: 'eda_netlist_erc', description: 'Read-only electrical smoke test over the schematic NETLIST (no application DRC needed): duplicate designators, unconnected pins, single-endpoint nets, shorted parts, power rails and decoupling, USB D+/D- pair, crystal (two nets + load caps + reaches the MCU), reset RC, BOOT pull resistor, Type-C CC pull-downs. DRC can be clean while the circuit is still wrong; these failures all show up in the netlist. Pass includeOk:true to see passing checks too.', inputSchema: objectSchema({ includeOk: { type: 'boolean' } }) },
324
324
  { name: 'eda_pcb_canvas_origin', description: 'Read (or align) the PCB canvas origin and convert points between the two coordinate spaces. JLCEDA displays CANVAS coordinates in its UI but every API takes DATA coordinates; the two differ by this offset, so a coordinate read off the screen will land somewhere else unless you convert or align. align:true calls setCanvasOrigin(0,0) so the display matches the API.', inputSchema: objectSchema({ align: { type: 'boolean' }, offsetX: { type: 'number' }, offsetY: { type: 'number' }, point: { type: 'object', additionalProperties: true } }) },
@@ -754,6 +754,15 @@ const BRIEF_TAG = '[dsh-eda-spec]'
754
754
  function briefBlock(brief) {
755
755
  return BRIEF_TAG + ' ' + JSON.stringify(brief)
756
756
  }
757
+ /** Sidecar file for the design brief, next to a local project directory. */
758
+ function specFilePath(project) {
759
+ const dir = String(project?.teamUuid ?? '')
760
+ const uuid = String(project?.uuid ?? '')
761
+ if (dir === '' || uuid === '') return ''
762
+ if (!/^[A-Za-z]:[\\/]/.test(dir) && !dir.startsWith('/')) return ''
763
+ return join(dir, '.dsh-eda-spec', uuid + '.json')
764
+ }
765
+
757
766
  function parseBriefBlock(description) {
758
767
  const text = typeof description === 'string' ? description : ''
759
768
  const index = text.indexOf(BRIEF_TAG)
@@ -845,7 +854,11 @@ async function auditPcb(args, timeoutMs) {
845
854
  const vias = await pcbPrimitiveList('pcb_PrimitiveVia.getAll', [netFilter, undefined], timeoutMs)
846
855
  const pours = await pcbPrimitiveList('pcb_PrimitivePour.getAll', [netFilter, undefined, undefined], timeoutMs)
847
856
  const outline = await pcbPrimitiveList('pcb_PrimitivePolyline.getAll', [undefined, PCB_LAYERS.boardOutline, undefined], timeoutMs)
848
- const pcbComponents = await pcbPrimitiveList('pcb_PrimitiveComponent.getAll', [undefined, undefined], timeoutMs)
857
+ // Same call shape as pcb_get_components: with a bare [undefined, undefined] the app
858
+ // returns an empty list here, which silently disabled the mechanical checks below.
859
+ const pcbComponents = await callExtension('pcb_PrimitiveComponent.getAll', [resolvePcbLayer(undefined), undefined], timeoutMs)
860
+ .then((raw) => (Array.isArray(raw) ? raw : []))
861
+ .catch(() => [])
849
862
 
850
863
  const violations = []
851
864
  const warnings = []
@@ -996,7 +1009,7 @@ async function auditPcb(args, timeoutMs) {
996
1009
 
997
1010
  return {
998
1011
  ok: violations.length === 0,
999
- counts: { tracks: tracks.length, vias: vias.length, pours: pours.length, outlinePolylines: outline.length, nets: netNames.length },
1012
+ counts: { tracks: tracks.length, vias: vias.length, pours: pours.length, outlinePolylines: outline.length, nets: netNames.length, componentsRead: pcbComponents.length },
1000
1013
  minWidthByNet: Object.fromEntries([...widthByNet.entries()].map(([net, width]) => [net, { widthMil: width, role: classifyNet(net), suggestedMil: suggestTrackWidthMil(net) }])),
1001
1014
  violations, warnings, checklist, drc,
1002
1015
  rules: {
@@ -2432,25 +2445,33 @@ async function executeTool(name, rawArgs, timeoutMs) {
2432
2445
 
2433
2446
  case 'eda_schematic_audit': {
2434
2447
  const paper = paperOf(args.paper)
2448
+ const frame = paper.frame
2449
+ const frameArea = (frame.right - frame.left) * (frame.top - frame.bottom)
2435
2450
  const rawList = await callExtension('sch_PrimitiveComponent.getAll', [], timeoutMs)
2436
2451
  const list = Array.isArray(rawList) ? rawList.slice(0, 200) : []
2437
2452
  const components = []
2453
+ let droppedSheetPrimitives = 0
2438
2454
  for (let index = 0; index < list.length; index += 1) {
2439
2455
  const entry = list[index] ?? {}
2440
- const designator = String(entry.designator ?? entry.Designator ?? entry.name ?? ('#' + index))
2456
+ const designator = String(entry.designator ?? entry.Designator ?? entry.Name ?? entry.name ?? ('#' + index))
2441
2457
  const id = String(entry.primitiveId ?? entry.PrimitiveId ?? '')
2442
2458
  let bounds
2443
2459
  if (id !== '') {
2444
- // Per-symbol box: the batch call returns one box for the whole selection,
2445
- // which cannot detect symbol-on-symbol overlap.
2446
2460
  const box = await bboxOf([id], timeoutMs)
2447
2461
  if (box !== undefined) bounds = { left: box.minX, right: box.maxX, bottom: box.minY, top: box.maxY }
2448
2462
  }
2463
+ // Frame / title-block primitives live in the same list and their box is the whole
2464
+ // sheet (measured: one entry reported 0..1170 x 0..825 and then 'overlapped'
2465
+ // every symbol and the title block).
2466
+ if (bounds !== undefined) {
2467
+ const boundsArea = (bounds.right - bounds.left) * (bounds.top - bounds.bottom)
2468
+ if (boundsArea > frameArea * 0.4) { bounds = undefined; droppedSheetPrimitives += 1 }
2469
+ }
2449
2470
  components.push({ designator, x: Number(entry.x ?? entry.X ?? 0), y: Number(entry.y ?? entry.Y ?? 0), bounds })
2450
2471
  }
2451
2472
  const report = auditSchematicGeometry({
2452
2473
  components,
2453
- frame: paper.frame,
2474
+ frame,
2454
2475
  titleBlock: paper.titleBlock,
2455
2476
  grid: args.grid === undefined ? 10 : Number(args.grid),
2456
2477
  })
@@ -2460,68 +2481,128 @@ async function executeTool(name, rawArgs, timeoutMs) {
2460
2481
  counts: report.counts,
2461
2482
  componentCount: report.componentCount,
2462
2483
  boxesRead: components.filter(component => component.bounds !== undefined).length,
2484
+ droppedSheetPrimitives,
2463
2485
  checks,
2486
+ notes: droppedSheetPrimitives > 0
2487
+ ? ['Skipped ' + droppedSheetPrimitives + ' primitive(s) whose box covers the whole sheet (frame/title block); counting them made every symbol look overlapped.']
2488
+ : [],
2464
2489
  }
2465
2490
  }
2466
2491
 
2467
2492
  case 'eda_delivery_checklist': {
2468
- // One call that proves the board is ready: connectivity, geometry, fabrication
2469
- // exports. Every step records what it actually returned, so the report can be
2470
- // handed to a reviewer instead of trusted as a claim.
2493
+ // One call that proves the board is ready. Two of the four steps only work with the
2494
+ // schematic active and two only with the PCB active, so the checklist switches
2495
+ // documents itself (switchDocuments:false keeps whatever is open).
2471
2496
  const steps = []
2472
2497
  const startedAt = new Date().toISOString()
2498
+ const project = await callExtension('dmt_Project.getCurrentProjectInfo', [], timeoutMs).catch(() => null)
2499
+ const boards = Array.isArray(project?.data) ? project.data : []
2500
+ const schematicPage = boards[0]?.schematic?.page?.[0]?.uuid ?? ''
2501
+ const pcbUuid = boards.find((board) => board?.pcb?.uuid)?.pcb?.uuid ?? ''
2502
+ // Document switching used to be the default and hung the call (openDocument + a
2503
+ // page load blocked the following netlist export). Default is now 'run with the
2504
+ // document this step needs active'; switchDocuments:true still tries to switch.
2505
+ const switchDocuments = args.switchDocuments !== false
2506
+ const documentType = async () => {
2507
+ try {
2508
+ const info = await callExtension('dmt_SelectControl.getCurrentDocumentInfo', [], timeoutMs)
2509
+ return Number(info?.documentType)
2510
+ } catch { return NaN }
2511
+ }
2512
+ const activate = async (uuid) => {
2513
+ if (typeof uuid !== 'string' || uuid === '') return false
2514
+ try {
2515
+ await callExtension('dmt_EditorControl.openDocument', [uuid], timeoutMs)
2516
+ // Give the editor a beat before the next API needs the page.
2517
+ await new Promise(resolve => setTimeout(resolve, 1500))
2518
+ return true
2519
+ } catch { return false }
2520
+ }
2473
2521
 
2474
2522
  let netlistErc = null
2475
- try {
2476
- const parsedNetlist = await fetchParsedNetlist(timeoutMs)
2477
- netlistErc = runNetlistErc(parsedNetlist)
2478
- steps.push({ step: 'netlist-erc', ok: netlistErc.ok, detail: netlistErc.counts, failing: netlistErc.checks.filter(check => check.status === 'fail').map(check => check.id) })
2479
- } catch (error) {
2480
- steps.push({ step: 'netlist-erc', ok: false, error: error instanceof Error ? error.message : String(error) })
2523
+ // Step documents: schematic steps need documentType 1, PCB steps need 3.
2524
+ const onSchematic = switchDocuments ? await activate(schematicPage) : (await documentType()) === 1
2525
+ if (!onSchematic) {
2526
+ steps.push({ step: 'netlist-erc', document: 'schematic', ok: false, skipped: 'schematic page is not the active document' })
2527
+ steps.push({ step: 'bom', document: 'schematic', ok: false, skipped: 'schematic page is not the active document' })
2528
+ } else {
2529
+ try {
2530
+ const parsedNetlist = await fetchParsedNetlist(timeoutMs)
2531
+ netlistErc = runNetlistErc(parsedNetlist)
2532
+ steps.push({
2533
+ step: 'netlist-erc',
2534
+ document: 'schematic',
2535
+ ok: netlistErc.ok,
2536
+ detail: netlistErc.counts,
2537
+ failing: netlistErc.checks.filter(check => check.status === 'fail').map(check => check.id),
2538
+ })
2539
+ } catch (error) {
2540
+ steps.push({ step: 'netlist-erc', document: 'schematic', ok: false, error: error instanceof Error ? error.message : String(error) })
2541
+ }
2481
2542
  }
2482
2543
 
2544
+ let bom = null
2545
+ if (onSchematic) {
2546
+ try {
2547
+ const payload = await callExtension('sch_ManufactureData.getBomFile', [undefined, 'csv'], timeoutMs)
2548
+ const text = typeof payload === 'string' ? payload : (typeof payload?.text === 'string' ? payload.text : '')
2549
+ const lines = text === '' ? 0 : text.split(/\r?\n/).filter(line => line.trim() !== '').length
2550
+ bom = { rows: Math.max(0, lines - 1), ok: lines > 1 }
2551
+ steps.push({ step: 'bom', document: 'schematic', ok: bom.ok, detail: { rows: bom.rows }, ...(bom.ok ? {} : { error: 'no BOM rows exported' }) })
2552
+ } catch (error) {
2553
+ steps.push({ step: 'bom', document: 'schematic', ok: false, error: error instanceof Error ? error.message : String(error) })
2554
+ }
2555
+ }
2556
+ const onPcb = switchDocuments ? await activate(pcbUuid) : (await documentType()) === 3
2483
2557
  let pcbAudit = null
2484
- try {
2485
- pcbAudit = await auditPcb({ drc: args.pcbDrc !== false }, timeoutMs)
2486
- steps.push({
2487
- step: 'pcb-audit',
2488
- ok: Array.isArray(pcbAudit.violations) && pcbAudit.violations.length === 0,
2489
- detail: { violations: pcbAudit.violations?.length ?? null, warnings: pcbAudit.warnings?.length ?? null, drc: pcbAudit.drc },
2490
- failing: (pcbAudit.violations ?? []).map(entry => entry.rule),
2491
- })
2492
- } catch (error) {
2493
- steps.push({ step: 'pcb-audit', ok: false, error: error instanceof Error ? error.message : String(error) })
2558
+ if (!onPcb) {
2559
+ steps.push({ step: 'pcb-audit', document: 'pcb', ok: false, skipped: 'PCB is not the active document' })
2560
+ steps.push({ step: 'gerber', document: 'pcb', ok: false, skipped: 'PCB is not the active document' })
2561
+ } else {
2562
+ try {
2563
+ pcbAudit = await auditPcb({ drc: args.pcbDrc !== false }, timeoutMs)
2564
+ steps.push({
2565
+ step: 'pcb-audit',
2566
+ document: 'pcb',
2567
+ ok: (pcbAudit.violations ?? []).length === 0,
2568
+ detail: { violations: pcbAudit.violations?.length ?? null, warnings: pcbAudit.warnings?.length ?? null, drc: pcbAudit.drc },
2569
+ failing: (pcbAudit.violations ?? []).map(entry => entry.rule),
2570
+ })
2571
+ } catch (error) {
2572
+ steps.push({ step: 'pcb-audit', document: 'pcb', ok: false, error: error instanceof Error ? error.message : String(error) })
2573
+ }
2494
2574
  }
2495
2575
 
2496
2576
  let gerber = null
2497
- try {
2498
- const payload = await callExtension('pcb_ManufactureData.getGerberFile', [undefined, undefined, undefined, undefined, undefined, undefined], timeoutMs)
2499
- const bytes = payload && typeof payload === 'object'
2500
- ? Number(payload.size ?? payload.bytes ?? 0)
2501
- : (typeof payload === 'string' ? payload.length : 0)
2502
- gerber = { bytes, ok: bytes > 1000 }
2503
- steps.push({ step: 'gerber', ok: gerber.ok, detail: { bytes } })
2504
- } catch (error) {
2505
- steps.push({ step: 'gerber', ok: false, error: error instanceof Error ? error.message : String(error) })
2506
- }
2507
-
2508
- let bom = null
2509
- try {
2510
- const payload = await callExtension('sch_ManufactureData.getBomFile', [undefined, 'csv'], timeoutMs)
2511
- const text = typeof payload === 'string' ? payload : (typeof payload?.text === 'string' ? payload.text : '')
2512
- const lines = text === '' ? 0 : text.split(/\r?\n/).filter(line => line.trim() !== '').length
2513
- bom = { rows: Math.max(0, lines - 1), ok: lines > 1 }
2514
- steps.push({ step: 'bom', ok: bom.ok, detail: { rows: bom.rows } })
2515
- } catch (error) {
2516
- steps.push({ step: 'bom', ok: false, error: error instanceof Error ? error.message : String(error) })
2577
+ if (onPcb) {
2578
+ try {
2579
+ const payload = await callExtension('pcb_ManufactureData.getGerberFile', [undefined, undefined, undefined, undefined, undefined, undefined], timeoutMs)
2580
+ const bytes = payload && typeof payload === 'object'
2581
+ ? Number(payload.size ?? payload.bytes ?? 0)
2582
+ : (typeof payload === 'string' ? payload.length : 0)
2583
+ gerber = { bytes, ok: bytes > 1000 }
2584
+ steps.push({ step: 'gerber', document: 'pcb', ok: gerber.ok, detail: { bytes } })
2585
+ } catch (error) {
2586
+ steps.push({ step: 'gerber', document: 'pcb', ok: false, error: error instanceof Error ? error.message : String(error) })
2587
+ }
2517
2588
  }
2518
2589
 
2519
- const failed = steps.filter(step => step.ok !== true)
2590
+ const failedSteps = steps.filter(step => step.ok !== true && step.skipped === undefined)
2591
+ const skippedSteps = steps.filter(step => step.ok !== true && step.skipped !== undefined)
2592
+ const verdict = failedSteps.length > 0 ? 'fail' : (skippedSteps.length > 0 ? 'incomplete' : 'pass')
2520
2593
  const report = {
2521
- ok: failed.length === 0,
2594
+ ok: verdict === 'pass',
2595
+ verdict,
2596
+ failedSteps: failedSteps.map(step => step.step),
2597
+ skippedSteps: skippedSteps.map(step => step.step),
2598
+ next: verdict === 'pass'
2599
+ ? 'Every step passed: the board can be ordered.'
2600
+ : (verdict === 'incomplete'
2601
+ ? 'Run the checklist again with the other document active (' + skippedSteps.map(step => step.document).filter((value, index, all) => all.indexOf(value) === index).join(', ') + ' needed).'
2602
+ : 'Fix the failing steps first; skipped steps are listed separately.'),
2522
2603
  startedAt,
2523
2604
  finishedAt: new Date().toISOString(),
2524
- steps,
2605
+ documents: { schematicPage, pcb: pcbUuid, switched: switchDocuments },
2525
2606
  netlistErc: netlistErc === null ? null : { ok: netlistErc.ok, counts: netlistErc.counts, componentCount: netlistErc.componentCount, netCount: netlistErc.netCount },
2526
2607
  pcbAudit: pcbAudit === null ? null : { violations: pcbAudit.violations, warnings: pcbAudit.warnings },
2527
2608
  gerber,
@@ -2536,10 +2617,12 @@ async function executeTool(name, rawArgs, timeoutMs) {
2536
2617
  '- 开始:' + report.startedAt,
2537
2618
  '- 结束:' + report.finishedAt,
2538
2619
  '- 结论:' + (report.ok ? 'PASS(可以下单)' : 'FAIL(有未通过项)'),
2620
+ '- 原理图页:' + (schematicPage || '未取到'),
2621
+ '- PCB:' + (pcbUuid || '未取到'),
2539
2622
  '',
2540
- '| 步骤 | 结果 | 说明 |',
2541
- '|---|---|---|',
2542
- ...steps.map(step => '| ' + step.step + ' | ' + (step.ok === true ? 'PASS' : 'FAIL') + ' | ' + JSON.stringify(step.detail ?? step.error ?? {}) + ' |'),
2623
+ '| 步骤 | 文档 | 结果 | 说明 |',
2624
+ '|---|---|---|---|',
2625
+ ...steps.map(step => '| ' + step.step + ' | ' + step.document + ' | ' + (step.ok === true ? 'PASS' : 'FAIL') + ' | ' + JSON.stringify(step.detail ?? step.error ?? {}) + ' |'),
2543
2626
  '',
2544
2627
  '## 电气检查(eda_netlist_erc)',
2545
2628
  '',
@@ -2566,6 +2649,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
2566
2649
  return report
2567
2650
  }
2568
2651
 
2652
+
2569
2653
  case 'eda_design_intake': {
2570
2654
  if (args.reset === true) designBrief = null
2571
2655
  if (designBrief === null) {
@@ -2580,17 +2664,41 @@ async function executeTool(name, rawArgs, timeoutMs) {
2580
2664
  const merged = { ...(designBrief ?? {}), ...answersReport.brief }
2581
2665
  const mergedReport = buildIntake(merged)
2582
2666
  let persisted = false
2667
+ let persistVia = 'none'
2668
+ let persistPath = ''
2583
2669
  if (Object.keys(merged).length > 0) {
2584
2670
  designBrief = merged
2585
2671
  if (answers !== null) {
2586
2672
  try {
2587
2673
  const project = await callExtension('dmt_Project.getCurrentProjectInfo', [], timeoutMs)
2674
+ const projectUuid = String(project?.uuid ?? '')
2588
2675
  const existing = typeof project?.description === 'string' ? project.description : ''
2589
2676
  const index = existing.indexOf(BRIEF_TAG)
2590
2677
  const base = index < 0 ? existing : existing.slice(0, index).trimEnd()
2591
2678
  const next = (base === '' ? '' : base + '\n') + briefBlock(merged)
2592
- await callExtension('dmt_Project.modifyProjectDescription', [next], timeoutMs)
2593
- persisted = true
2679
+ // The description write is inert on this build (measured: returns false and
2680
+ // the description stays empty), so fall back to a sidecar file next to the
2681
+ // local project directory and verify whichever path was used.
2682
+ let stored = false
2683
+ if (projectUuid !== '') {
2684
+ try {
2685
+ await callExtension('dmt_Project.modifyProjectDescription', [projectUuid, next], timeoutMs)
2686
+ const readBack = await callExtension('dmt_Project.getCurrentProjectInfo', [], timeoutMs)
2687
+ stored = String(readBack?.description ?? '').includes(BRIEF_TAG)
2688
+ if (stored) persistVia = 'description'
2689
+ } catch { stored = false }
2690
+ }
2691
+ if (!stored) {
2692
+ const sidecar = specFilePath(project)
2693
+ if (sidecar !== '') {
2694
+ await mkdir(join(sidecar, '..'), { recursive: true })
2695
+ await writeFile(sidecar, JSON.stringify({ projectUuid, brief: merged }, null, 2) + '\n')
2696
+ const check = JSON.parse(await readFile(sidecar, 'utf8'))
2697
+ stored = check !== null && typeof check === 'object' && Object.keys(merged).every(key => check.brief?.[key] === merged[key])
2698
+ if (stored) { persistVia = 'file'; persistPath = sidecar }
2699
+ }
2700
+ }
2701
+ persisted = stored === true
2594
2702
  } catch { persisted = false }
2595
2703
  }
2596
2704
  }
@@ -2604,6 +2712,8 @@ async function executeTool(name, rawArgs, timeoutMs) {
2604
2712
  brief: mergedReport.brief,
2605
2713
  hydratedFromProject: answers === null && designBrief !== null,
2606
2714
  persisted,
2715
+ persistVia,
2716
+ persistPath,
2607
2717
  sections: mergedReport.sections.filter(section => section.questions.some(question => mergedReport.brief[question.id] === undefined)),
2608
2718
  next: mergedReport.ready
2609
2719
  ? 'All critical parameters are known: lay out the schematic, then eda_netlist_erc before routing.'
@@ -3026,6 +3136,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
3026
3136
  case 'eda_pcb_delete_via':
3027
3137
  return await callExtension('pcb_PrimitiveVia.delete', [String(args.primitiveId)], timeoutMs)
3028
3138
 
3139
+
3029
3140
  case 'eda_pcb_create_pour': {
3030
3141
  const layer = resolveRegionLayer(args.layer, 'eda_pcb_create_pour')
3031
3142
  const points = requireFlatPoints(args.points, 'eda_pcb_create_pour')
@@ -3065,7 +3176,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
3065
3176
  (firstFailure instanceof Error ? firstFailure.message : String(firstFailure)) + '; retry: ' + detail + '). ' +
3066
3177
  'polygonStage=' + (polygonOk ? 'ok' : 'failed') + ': ' +
3067
3178
  (polygonOk
3068
- ? 'the app accepted the outline but refused the pour itself, so this is an application limit, not a coordinate problem. ' +
3179
+ ? 'the app accepted the outline but refused the pour itself, and so does the complex polygon the docs require (createComplexPolygon); single and complex polygons, every net/layer/fill combination, all refused, so pour creation is an application limit on this build, not a coordinate problem. ' +
3069
3180
  'Pour by hand in the PCB editor (覆铜 → select GND → top + bottom); eda_design_audit reports it as a warn until then.'
3070
3181
  : 'the outline polygon was rejected too: check that every point is inside the board outline and flat [x1,y1,...].') +
3071
3182
  ' retried=' + retried)
@@ -153,7 +153,7 @@ export const SKILL_PLAYBOOK_LINES: readonly string[] = [
153
153
  "| eda_launch 带 project 没打开工程 | 命令行路径被应用忽略 | 用 eda_open_project({ name }) 打开 |",
154
154
  "| 关应用后改动丢失 | eda_close 走了强制路径 | 关前先保存;看返回的 forced |",
155
155
  "| run_code 读大 JSON 被截断 | 宿主 read 有单行上限 | 用 node:fs 读盘或分片读 |",
156
- "| 覆铜创建失败(无法创建覆铜边框图元) | 应用侧限制:polygonStage=ok(轮廓被接受)但 pours 被拒,与网络/填充方式/坐标无关 | 工具会重试一次并给出该诊断;手工在 PCB 页框选覆铜(GND,顶层+底层),design_audit 记为 warn |",
156
+ "| 覆铜/填充一律被拒(无法创建覆铜边框图元 / 无法创建填充图元) | 应用侧限制:静态+实例入口、单/复杂多边形、原始源码数组、6 种填充枚举、全参数组合全部被拒;同一多边形管线在本构建上连丝印折线都写不出来 | 不要在 API 层反复试:覆铜请在界面手工铺(GND、顶层+底层);design_audit 持续记 copper-pour warn |",
157
157
  "| pcb_draw_mounting_hole 报“无法创建焊盘图元” | pad 形状必填,且槽孔只能用 ELLIPSE | 已内建:圆孔 ELLIPSE(d,d),槽孔 ELLIPSE(长,宽)+hole[SLOT,宽,长];OBLONG/RECTANGLE 焊盘会被拒 |",
158
158
  "| pcb_PrimitiveString.getAll / pcb_PrimitivePad.getAll 返回 0 | 这两个 API 按层查询 | 传 layer:丝印 3/4,多层焊盘 12;不传参恒为 0 |",
159
159
  "| 按坐标算的越界没被发现 | 图框/原点假设与真实页面不一致 | 用 eda_get_page_info 的真实值 + eda_capture 导 PNG 看实际版面 |",
@@ -246,6 +246,13 @@ export const SKILL_PLAYBOOK_LINES: readonly string[] = [
246
246
  "| `eda_delivery_checklist` | 一次串完 ERC + PCB 审计/DRC + Gerber + BOM,逐步 PASS/FAIL;`outPath` 落 Markdown 报告 | 交付前,代替\"我说做完了\" |",
247
247
  "",
248
248
  "**交付口径**:`eda_delivery_checklist` 返回 `ok:true` 才算可以下单;任何一步 FAIL 都要先修,不许口头绕过。报告里带时间戳与计数,可直接给人复核。",
249
+ "### 交付清单怎么读(1.0.1 起)",
250
+ "",
251
+ "- 一次调用会**自己切到原理图再切到 PCB**(`switchDocuments:false` 改按当前文档跑一半,另一半标注跳过原因);",
252
+ "- 返回 `verdict`:`pass`(四步全过)/ `incomplete`(有步骤因文档不对被跳过)/ `fail`(有步骤真失败);`ok` 等价于 `verdict==='pass'`;",
253
+ "- `skippedSteps` 与 `failedSteps` 分开列 —— **别把\"没跑\"当成\"通过\"**;四步全过实测约 **7.5 s**;",
254
+ "- 投板前最后一条命令就用它,`outPath` 落 Markdown 存档。",
255
+ "",
249
256
  "",
250
257
  "**参数持久化**:`eda_design_intake({answers})` 会把答复写进**工程描述**(`[dsh-eda-spec] {...}`),下次会话调用 `eda_design_intake` 会自动读回,不用重新猜;`reset:true` 清空。",
251
258
  "",
Binary file