@huaqiu/dsh-tool-kicad-schematic-gen 0.6.8 → 0.6.9

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/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  One installable plugin containing twelve tools and the kicad-schematic-design skill. Internal modules preserve the former component-library, module-catalog and schematic-tool contracts. Component search is provided by the KiCad built-in plugin @huaqiu/dsh-tool-part-search and its search_hqsch_parts tool.
6
6
 
7
+ 布局交付:简单项目默认首版、一次集中修复、验证并交付;首版达标可直接交付。额外修复必须有具体阻塞缺陷,真实错误不受轮数限制。不为装饰偏好或清零保守警告重复重建。流程和验收条件见 [布局修复与交付停止条件](skills/kicad-schematic-design/references/layout-delivery.md),启动器摘要返回 nextAction 提示,不自动宣称方案或电气验证通过。
8
+
7
9
  Configuration is grouped under components, modules.search, modules.download and schematic. Templates ship under skills/kicad-schematic-design; the public scripting API is available at ./builder. Module downloads resolve the current HQ Edge host at execution time, so a missing host does not prevent other tools or the skill from loading.
8
10
 
9
11
  Imports remain workspace scoped and use DSH sandbox policy. Model inspection reports pin/pad summaries; paired import checks unique pin/pad number sets, but does not verify electrical suitability or package dimensions. Placement and routing are explicitly authored in design scripts. Datasheet extraction preserves missing/truncated status.
@@ -18,6 +20,10 @@ Power symbols: declare actual shared nets with the builder's `requiredPowerSymbo
18
20
 
19
21
  Standalone common-only symbols with pins (e.g. SW-PTB-2_0_1) are automatically normalized in project copies to unit 1 without changing pins or graphics. Model summaries and import results report builderCompatibility.normalization=common_only_to_unit_1. Inherited, empty, malformed or alternate-only definitions are not normalized. Unsupported imports report part, symbol, sections and source URL.
20
22
 
21
- 最终概览卡片:正式预览传入 `delivery: true` 和包含全部本地库的 `projectDir`,显示页面切换、缩放、ZIP 下载。复用已有 PNG,不重新生成电路,无重新生成/详情按钮。需 KiCad Harness 的 webServer 与前端加载支持;不可用时仍返回本地预览路径。快照保存在插件 `.overview-cache/`,重启后可读取;移除该目录后历史卡片下载将失效。ZIP 为工程目录内相关设计文件与库的快照,不收集脚本、预览或外部库。
23
+ 最终概览卡片:正式预览传入 `delivery: true` 和包含全部本地库的 `projectDir`,待本轮正常完成后,在最终回复后的轮次尾部显示页面切换、缩放、ZIP 下载;工具执行期间不展示正式卡片。正式概览必须是本轮最后一个工具操作,之后调用其他工具或普通预览时须重新正式交付;失败、取消、中断的轮次不显示正式卡片。卡片从所属轮次的工具记录恢复,切换会话无需全局缓存。复用已有 PNG,不重新生成电路,无重新生成/详情按钮。需 KiCad Harness 的 webServer、conversation.chat.turnTail 和 useChat(ChatSnapshot 的 locations/nodes)支持;不可用时仍返回本地预览路径。快照保存在插件 `.overview-cache/`,重启后可读取;移除该目录后历史卡片下载将失效。ZIP 为工程目录内相关设计文件与库的快照,不收集脚本、预览或外部库。
22
24
 
23
25
  Module analysis queries use `analyze_kicad_module({schematicPath, view, reference?, pin?, net?, offset?, kicadCliPath?})`, with explicit `view`. Downloads already return a summary: query only needed components, grouped nets or grouped BOM. CLI/project options are resolved internally; optional absolute `kicadCliPath` is for recovery using the existing `get_eda_host_info` result. On `CLI_START_FAILED`, retry analysis with that path and `view:summary`, without downloading again. Successful paths are reused; default CLI command names preserve automatic discovery. Probe errors return `error:{code,executable}` and retain causes; capability failures are distinct from startup failures. Matching same-stem projects and downloaded `.source.kicad_pro` files are discovered. Detail output contains `status`, `items`, and optional `nextOffset`/`warnings`; missing `nextOffset` means completion. Component custom fields require a reference filter. Partial results are reused for detail queries; after fixing configuration use `view:summary` to retry. Full local artifacts are preserved. This replaces the previous analysis query arguments and response contract; existing callers must migrate. See [module analysis](skills/kicad-schematic-design/references/module-analysis.md).
26
+
27
+ 方案先行:先保存硬件方案,用 `scripts/run-design.mjs --read-plan <方案.md>` 读回,再按文档编写脚本。运行时必须传 `--plan <path> --plan-sha256 <sha256>`;构建库直接调用也要求 `designPlan:{path,sha256}`。平铺 create 与层次生成工具同样检查方案。缺失、空文档或版本变化时阻止生成;只读模块分析和预览不受影响。构建报告保存方案全文、方案 hash 和脚本 hash。此检查保证文件和版本前置,不代替方案电气完整性及文图一致性审查。
28
+
29
+ 电气栅格:默认 50 mil(1.27 mm)。构建器吸附器件放置、导线坐标、标签和模块平移量;`gridPoint/snapGrid` 可用于脚本规划。引脚库必须兼容同一格点,原理图生成与正式交付检查实际引脚及连接点,偏离时阻止交付。已有工程需从脚本重新构建,不通过独立舍入引脚/连线修补。
package/lib/builder.d.ts CHANGED
@@ -180,6 +180,17 @@ interface PowerPlanningOptions extends Pick<PowerLabelSpec, 'libraryPath' | 'net
180
180
  maxMoveMm?: number;
181
181
  }
182
182
  //#endregion
183
+ //#region src/schematic/design-plan.d.ts
184
+ interface DesignPlanReference {
185
+ path: string;
186
+ sha256: string;
187
+ }
188
+ interface DesignPlan extends DesignPlanReference {
189
+ text: string;
190
+ }
191
+ declare function readDesignPlan(path: string): DesignPlan;
192
+ declare function loadDesignPlan(input?: DesignPlanReference): DesignPlan;
193
+ //#endregion
183
194
  //#region src/schematic/builder.d.ts
184
195
  /** Explicit instance placement in millimetres; library files must be trusted project inputs. */
185
196
  interface SymbolPlacement {
@@ -200,6 +211,8 @@ interface SymbolPlacement {
200
211
  }
201
212
  /** Explicit deployment templates and page choice; no implicit file discovery. */
202
213
  interface SchematicOptions {
214
+ /** Saved plan version reviewed before generation; runner supplies this via environment. */
215
+ designPlan?: DesignPlanReference;
203
216
  projectName: string;
204
217
  templateDir: string;
205
218
  paper: 'A4' | 'A3' | 'A2' | 'A1' | 'A0';
@@ -312,6 +325,7 @@ declare class Schematic {
312
325
  private readonly objectModules;
313
326
  private readonly requiredPowerSymbols;
314
327
  private readonly templates;
328
+ private readonly designPlan;
315
329
  /** Load validated templates. @param options Project identity, page and template directory. */
316
330
  constructor(options: SchematicOptions);
317
331
  /** Reserve a non-overlapping fixed module frame before placing its circuit.
@@ -419,6 +433,7 @@ declare class Schematic {
419
433
  projectPath: string;
420
434
  frames: ResolvedFlatFrame[];
421
435
  layoutWarnings: PowerLayoutWarning[];
436
+ designPlan: DesignPlanReference;
422
437
  };
423
438
  }
424
439
  //#endregion
@@ -483,4 +498,10 @@ declare function attachPinLabels(target: LabelTarget, spec: {
483
498
  })[];
484
499
  };
485
500
  //#endregion
486
- export { ModuleRegion, ModuleRegionSpec, PowerLabelEntry, PowerLabelReplacement, PowerLabelSpec, PowerLayoutWarning, type PowerPlacementPlan, type PowerPlanningOptions, PowerSymbolStatus, Schematic, SchematicOptions, SymbolPlacement, attachPinLabel, attachPinLabels, resolveKiCadPaths };
501
+ //#region src/schematic/grid.d.ts
502
+ /** Electrical schematic coordinates use KiCad's 50 mil connection grid. */
503
+ declare const SCHEMATIC_GRID_MM = 1.27;
504
+ declare function snapGrid(n: number): number;
505
+ declare function gridPoint(p: readonly number[]): [number, number];
506
+ //#endregion
507
+ export { ModuleRegion, ModuleRegionSpec, PowerLabelEntry, PowerLabelReplacement, PowerLabelSpec, PowerLayoutWarning, type PowerPlacementPlan, type PowerPlanningOptions, PowerSymbolStatus, SCHEMATIC_GRID_MM, Schematic, SchematicOptions, SymbolPlacement, attachPinLabel, attachPinLabels, gridPoint, loadDesignPlan, readDesignPlan, resolveKiCadPaths, snapGrid };
package/lib/builder.js CHANGED
@@ -1,4 +1,4 @@
1
- import { C as resolveKiCadPaths, _ as children, a as flatTextSize, b as required, c as validatePreviewSchematic, f as alignVendorSymbolIds, g as applyEdits, h as normalizeCommonUnits, l as assignFootprintText, n as listFlatObjects, r as loadFlatTemplates, t as buildFlatCopy, v as parse, x as value, y as quote } from "./flat--E2zWV_r.js";
1
+ import { A as required, D as children, E as applyEdits, N as resolveKiCadPaths, O as parse, S as alignVendorSymbolIds, T as normalizeCommonUnits, a as snapGrid, c as loadDesignPlan, d as buildFlatCopy, f as listFlatObjects, h as flatTextSize, i as onGrid, j as value, k as quote, n as SCHEMATIC_GRID_MM, p as loadFlatTemplates, r as gridPoint, t as assertSchematicGrid, u as readDesignPlan, v as validatePreviewSchematic, y as assignFootprintText } from "./grid-check-BgWU7a8j.js";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { createHash, randomUUID } from "node:crypto";
@@ -402,7 +402,7 @@ var ModuleRegion = class {
402
402
  };
403
403
  function point(at) {
404
404
  if (at.length !== 2 || at.some((n) => !Number.isFinite(n))) throw new Error("coordinates require two finite millimetre values");
405
- return at;
405
+ return gridPoint(at);
406
406
  }
407
407
  function definition(source, name, chain = []) {
408
408
  const root = parse(source);
@@ -549,9 +549,19 @@ var Schematic = class Schematic {
549
549
  objectModules = /* @__PURE__ */ new Map();
550
550
  requiredPowerSymbols;
551
551
  templates;
552
+ designPlan;
552
553
  /** Load validated templates. @param options Project identity, page and template directory. */
553
554
  constructor(options) {
554
555
  this.options = options;
556
+ const { path, sha256 } = loadDesignPlan(options.designPlan);
557
+ if (options.designPlan && process.env.DSH_DESIGN_BRIEF_PATH) {
558
+ const runnerPlan = loadDesignPlan();
559
+ if (path !== runnerPlan.path || sha256 !== runnerPlan.sha256) throw new Error("DESIGN_PLAN_MISMATCH: script and runner must use the same saved plan version");
560
+ }
561
+ this.designPlan = {
562
+ path,
563
+ sha256
564
+ };
555
565
  if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/u.test(options.projectName)) throw new Error("projectName must be an ASCII filename without an extension");
556
566
  if (![
557
567
  "A4",
@@ -606,9 +616,14 @@ var Schematic = class Schematic {
606
616
  * @returns Reference map, translated label anchors, net names and object UUIDs for frames.
607
617
  */
608
618
  importModule(spec) {
619
+ spec = {
620
+ ...spec,
621
+ offset: gridPoint(spec.offset)
622
+ };
609
623
  if (this.importedModules.has(spec.name)) throw new Error(`duplicate imported module: ${spec.name}`);
610
624
  const used = new Set([...this.references.keys()].map((key) => key.slice(0, key.lastIndexOf(":"))));
611
625
  const sourceText = readFileSync(spec.sourcePath, "utf8");
626
+ assertSchematicGrid(sourceText);
612
627
  const prepared = prepareFlatImport(sourceText, spec, this.options.projectName, this.rootId, used);
613
628
  const footprints = {};
614
629
  if (spec.footprintMappingPath) {
@@ -656,6 +671,14 @@ var Schematic = class Schematic {
656
671
  const symbolName = spec.libId.slice(separator + 1);
657
672
  const raw = normalizeCommonUnits(definition(readFileSync(spec.libraryPath, "utf8"), symbolName)), form = parse(raw);
658
673
  const sections = children(form, "symbol");
674
+ for (const section of sections) {
675
+ const suffix = /_(\d+)_(\d+)$/u.exec(value(section));
676
+ if (!suffix || ![0, unit].includes(Number(suffix[1])) || ![0, 1].includes(Number(suffix[2]))) continue;
677
+ for (const pin of children(section, "pin")) {
678
+ const at = children(pin, "at")[0];
679
+ if (!at || ![Number(value(at, 1)), Number(value(at, 2))].every(onGrid)) throw new Error(`SYMBOL_PIN_OFF_GRID: ${spec.libId} pin ${value(children(pin, "number")[0])} requires a 50 mil (1.27 mm) compatible library symbol. Do not round pin geometry independently of the circuit.`);
680
+ }
681
+ }
659
682
  if (!sections.some((s) => {
660
683
  const m = /_(\d+)_(\d+)$/u.exec(value(s));
661
684
  return m && Number(m[1]) === unit && [0, 1].includes(Number(m[2]));
@@ -741,7 +764,8 @@ var Schematic = class Schematic {
741
764
  this.endpoint(from),
742
765
  ...via.map(point),
743
766
  this.endpoint(to)
744
- ];
767
+ ].filter((p, i, all) => i === 0 || p[0] !== all[i - 1][0] || p[1] !== all[i - 1][1]);
768
+ if (points.length < 2) throw new Error("wire collapses on the 50 mil (1.27 mm) grid; increase the lead length");
745
769
  const segments = [], ids = [];
746
770
  for (let i = 1; i < points.length; i++) {
747
771
  const a = points[i - 1], b = points[i];
@@ -1004,7 +1028,9 @@ var Schematic = class Schematic {
1004
1028
  * @returns Schematic text, project JSON text, resolved frames and edit counts.
1005
1029
  */
1006
1030
  render() {
1031
+ loadDesignPlan(this.designPlan);
1007
1032
  this.checkPowerSymbols();
1033
+ assertSchematicGrid(this.source());
1008
1034
  if (!this.instances.length) throw new Error("place at least one symbol before rendering");
1009
1035
  return buildFlatCopy(this.source(), this.options.projectName, {
1010
1036
  removeReferences: [],
@@ -1030,7 +1056,8 @@ var Schematic = class Schematic {
1030
1056
  schematicPath,
1031
1057
  projectPath,
1032
1058
  frames: result.resolvedFrames,
1033
- layoutWarnings: this.powerLayoutWarnings()
1059
+ layoutWarnings: this.powerLayoutWarnings(),
1060
+ designPlan: { ...this.designPlan }
1034
1061
  };
1035
1062
  }
1036
1063
  };
@@ -1131,4 +1158,4 @@ function attachPinLabels(target, spec) {
1131
1158
  };
1132
1159
  }
1133
1160
  //#endregion
1134
- export { ModuleRegion, Schematic, attachPinLabel, attachPinLabels, resolveKiCadPaths };
1161
+ export { ModuleRegion, SCHEMATIC_GRID_MM, Schematic, attachPinLabel, attachPinLabels, gridPoint, loadDesignPlan, readDesignPlan, resolveKiCadPaths, snapGrid };
package/lib/client.js CHANGED
@@ -47,6 +47,32 @@
47
47
  }
48
48
  }
49
49
  //#endregion
50
+ //#region src/client/turn-overview.ts
51
+ const NO_OVERVIEW = { kind: "review" };
52
+ /** Read only this completed turn's persisted tool nodes; no global/session cache or DOM timing. */
53
+ function overviewForTurn(turn, chat) {
54
+ if (turn?.status !== "closed" || turn.end?.data.reason.kind !== "completed" || !chat) return NO_OVERVIEW;
55
+ const calls = [];
56
+ const visited = /* @__PURE__ */ new Set();
57
+ function walk(value, depth = 0) {
58
+ if (!value || typeof value !== "object" || visited.has(value) || depth > 64) return;
59
+ visited.add(value);
60
+ const block = value;
61
+ if (block.kind === "tool-result" && !block.subCalls?.length && typeof block.seq === "number") calls.push({
62
+ seq: block.seq,
63
+ block,
64
+ name: block.call?.name
65
+ });
66
+ if (Array.isArray(block.subCalls)) for (const child of block.subCalls) walk(child, depth + 1);
67
+ }
68
+ for (const key of chat.locations.getTurn(turn.turn)) {
69
+ const node = chat.nodes.get(key);
70
+ if (node?.kind === "tool-call") walk(node.data?.root);
71
+ }
72
+ const latest = calls.sort((a, b) => a.seq - b.seq).at(-1);
73
+ return latest?.name === "render_kicad_module_preview" ? parseCard(latest.block) : NO_OVERVIEW;
74
+ }
75
+ //#endregion
50
76
  //#region src/client/place.ts
51
77
  function placementHost(get) {
52
78
  try {
@@ -71,18 +97,6 @@
71
97
  if (result?.ok === false) throw new Error(result.message ?? "编辑器拒绝放置。");
72
98
  }
73
99
  //#endregion
74
- //#region src/client/latest-overview.ts
75
- /** Choose the newest mounted snapshot for each session/project; never compare titles. */
76
- function supersededOverviews(rows) {
77
- const latest = /* @__PURE__ */ new Map();
78
- rows.forEach((row, index) => {
79
- if (!row.session || !row.project) return;
80
- const key = JSON.stringify([row.session, row.project]), previous = latest.get(key);
81
- if (previous === void 0 || row.createdAt >= rows[previous].createdAt) latest.set(key, index);
82
- });
83
- return rows.map((row, index) => !!row.session && !!row.project && latest.get(JSON.stringify([row.session, row.project])) !== index);
84
- }
85
- //#endregion
86
100
  //#region ../dsh-sidebar-entry/lib/index.mjs
87
101
  /** Find the sidebar shell root element, or undefined while not yet mounted. */
88
102
  function sidebarRoot() {
@@ -262,11 +276,11 @@
262
276
  * family row means adding its selector here (and to no plugin-local list).
263
277
  */
264
278
  const HUAQIU_SIDEBAR_ENTRY_FAMILY = [
265
- "[data-hq-plugin-update-entry]",
266
279
  "[data-hqcg-footprint-entry]",
267
280
  "[data-hqcg-symbol-entry]",
268
281
  "[data-hqcg-schematic-entry]",
269
- "[data-hq-dummy-entry]"
282
+ "[data-hq-dummy-entry]",
283
+ "[data-hq-plugin-update-entry]"
270
284
  ];
271
285
  /**
272
286
  * Read the host UI language from `<html lang>`.
@@ -1057,52 +1071,14 @@
1057
1071
  }
1058
1072
  //#endregion
1059
1073
  //#region src/client/index.ts
1060
- /** Harness compacts finished tool calls; keep only our delivery card visible. */
1061
- function keepDeliveryVisible() {
1062
- if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {};
1063
- let frame = 0, disposed = false;
1064
- const reveal = () => {
1065
- frame = 0;
1066
- if (disposed) return;
1067
- const cards = Array.from(document.querySelectorAll(".dsh-local-overview"));
1068
- const hidden = supersededOverviews(cards.map((card) => ({
1069
- session: card.dataset.overviewSession ?? "",
1070
- project: card.dataset.overviewProject ?? "",
1071
- createdAt: Number(card.dataset.overviewCreated ?? 0)
1072
- })));
1073
- for (const [index, card] of cards.entries()) {
1074
- if (card.hidden !== hidden[index]) card.hidden = hidden[index];
1075
- if (hidden[index]) continue;
1076
- let parent = card.parentElement;
1077
- while (parent) {
1078
- if (parent.hasAttribute("data-turn-process-hidden")) parent.removeAttribute("hidden");
1079
- parent = parent.parentElement;
1080
- }
1081
- }
1082
- };
1083
- const observer = new MutationObserver(() => {
1084
- if (!frame) frame = requestAnimationFrame(reveal);
1085
- });
1086
- observer.observe(document.documentElement, {
1087
- subtree: true,
1088
- childList: true,
1089
- attributes: true,
1090
- attributeFilter: ["hidden", "data-turn-process-hidden"]
1091
- });
1092
- reveal();
1093
- return () => {
1094
- disposed = true;
1095
- observer.disconnect();
1096
- if (frame) cancelAnimationFrame(frame);
1097
- };
1098
- }
1099
1074
  window.__ModuleLoader__.load({
1100
1075
  id: "@huaqiu/dsh-tool-kicad-schematic-gen",
1101
1076
  factory: (require) => {
1102
1077
  const { createElement: h, useState, useEffect } = require("react");
1103
1078
  let getHost;
1104
1079
  function Card(props) {
1105
- const state = parseCard(props.block), result = state.result;
1080
+ const chat = props.useChat?.((snapshot) => snapshot);
1081
+ const state = chat ? overviewForTurn(props.turn, chat) : NO_OVERVIEW, result = state.result;
1106
1082
  const [page, setPage] = useState(0), [zoom, setZoom] = useState(1), [message, setMessage] = useState(""), [busy, setBusy] = useState(false);
1107
1083
  useEffect(() => {
1108
1084
  setPage(0);
@@ -1247,7 +1223,6 @@
1247
1223
  inject: ["slots", "inputTriggers"],
1248
1224
  apply: (ctx) => {
1249
1225
  getHost = () => ctx.get?.("hqEdge");
1250
- const stopVisibility = keepDeliveryVisible();
1251
1226
  const source = registerSchematicGenSourceWhenReady(() => ctx.get?.("inputTriggers"));
1252
1227
  try {
1253
1228
  console.debug("[schematic-gen:client] apply", {
@@ -1283,12 +1258,12 @@
1283
1258
  try {
1284
1259
  console.debug("[schematic-gen:client] sidebar row", { rows: document.querySelectorAll("[data-hqcg-schematic-entry]").length });
1285
1260
  } catch {}
1286
- const dispose = ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
1287
- name: "tool.call.toolview",
1288
- key: "render_kicad_module_preview"
1261
+ const dispose = ctx.slots.inject("conversation.chat.turnTail", () => ctx.slots.register({
1262
+ name: "conversation.chat.turnTail",
1263
+ id: "huaqiu-schematic-overview",
1264
+ order: 100
1289
1265
  }, Card));
1290
1266
  ctx.effect?.(() => () => {
1291
- stopVisibility();
1292
1267
  source.dispose();
1293
1268
  stopEntry();
1294
1269
  if (typeof dispose === "function") dispose();
@@ -1,6 +1,6 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
2
2
  import { delimiter, dirname, join } from "node:path";
3
- import { randomUUID } from "node:crypto";
3
+ import { createHash, randomUUID } from "node:crypto";
4
4
  //#region src/runtime-paths.ts
5
5
  const isDefaultCommand = (value) => !value || /^kicad-cli(?:\.exe)?$/i.test(value.trim());
6
6
  /** Explicit paths override discovery; default command names still discover the host and PATH. */
@@ -602,7 +602,8 @@ function buildProject(edited, projectName, sheetName, expectedPorts, drivenInput
602
602
  schematic: {
603
603
  legacy_lib_dir: "",
604
604
  legacy_lib_list: [],
605
- ...imported.schematic
605
+ ...imported.schematic,
606
+ connection_grid_size: 50
606
607
  },
607
608
  sheets: [],
608
609
  text_variables: {},
@@ -1504,4 +1505,108 @@ function buildFlatCopy(source, projectName, plan, templates) {
1504
1505
  };
1505
1506
  }
1506
1507
  //#endregion
1507
- export { resolveKiCadPaths as C, getKiCadPaths as S, children as _, flatTextSize as a, required as b, validatePreviewSchematic as c, footprintRequirements as d, alignVendorSymbolIds as f, applyEdits as g, normalizeCommonUnits as h, summarizeFlatLayout as i, assignFootprintText as l, isCommonOnlyUnit as m, listFlatObjects as n, PreviewError as o, buildProject as p, loadFlatTemplates as r, previewExportError as s, buildFlatCopy as t, auditFootprints as u, parse as v, value as x, quote as y };
1508
+ //#region src/schematic/design-plan.ts
1509
+ /** Required, version-bound design document input for schematic generation. */
1510
+ const DESIGN_PLAN_MAX_BYTES = 1048576;
1511
+ function designPlanReference(input) {
1512
+ const plan = input;
1513
+ if (!plan || typeof plan.path !== "string" || !plan.path.trim() || typeof plan.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(plan.sha256)) throw new Error("DESIGN_PLAN_REQUIRED: save the hardware design document, read it with run-design.mjs --read-plan <path>, then supply designPlan:{path,sha256}. Do not generate first and write the document afterwards.");
1514
+ return {
1515
+ path: plan.path,
1516
+ sha256: plan.sha256
1517
+ };
1518
+ }
1519
+ /** Structural check only; the model must still review engineering completeness. */
1520
+ function inspectDesignPlan(path, text) {
1521
+ if (Buffer.byteLength(text, "utf8") > 1048576 || text.trim().length < 200 || !/^#\s+\S/m.test(text) || !/^##\s+\S/m.test(text)) throw new Error("DESIGN_PLAN_INCOMPLETE: provide a saved Markdown hardware plan with a title, substantive sections, and at least 200 characters. Review requirements, parts, circuits, connections and budgets before generation.");
1522
+ return {
1523
+ path,
1524
+ sha256: createHash("sha256").update(text).digest("hex"),
1525
+ text
1526
+ };
1527
+ }
1528
+ function matchDesignPlan(reference, text) {
1529
+ const plan = inspectDesignPlan(reference.path, text);
1530
+ if (plan.sha256 !== reference.sha256) throw new Error("DESIGN_PLAN_CHANGED: the document changed after it was read. Read the latest document and update the design script/connection plan before rebuilding with its new sha256.");
1531
+ return plan;
1532
+ }
1533
+ function readDesignPlan(path) {
1534
+ try {
1535
+ const canonical = realpathSync(path), info = statSync(canonical);
1536
+ if (!info.isFile() || info.size > 1048576) throw new Error("expected a document file no larger than 1 MiB");
1537
+ return inspectDesignPlan(canonical, new TextDecoder("utf-8", { fatal: true }).decode(readFileSync(canonical)));
1538
+ } catch (error) {
1539
+ throw new Error(`DESIGN_PLAN_UNAVAILABLE: ${path}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
1540
+ }
1541
+ }
1542
+ function loadDesignPlan(input) {
1543
+ const reference = designPlanReference(input ?? {
1544
+ path: process.env.DSH_DESIGN_BRIEF_PATH,
1545
+ sha256: process.env.DSH_DESIGN_BRIEF_SHA256
1546
+ });
1547
+ const plan = readDesignPlan(reference.path);
1548
+ return matchDesignPlan({
1549
+ ...reference,
1550
+ path: plan.path
1551
+ }, plan.text);
1552
+ }
1553
+ //#endregion
1554
+ //#region src/schematic/grid.ts
1555
+ /** Electrical schematic coordinates use KiCad's 50 mil connection grid. */
1556
+ const SCHEMATIC_GRID_MM = 1.27;
1557
+ function snapGrid(n) {
1558
+ if (!Number.isFinite(n)) throw new Error("GRID_COORDINATE_INVALID: expected finite millimetres");
1559
+ return Number((Math.round(n / SCHEMATIC_GRID_MM) * SCHEMATIC_GRID_MM).toFixed(6));
1560
+ }
1561
+ function gridPoint(p) {
1562
+ if (p.length !== 2) throw new Error("grid point requires two coordinates");
1563
+ return [snapGrid(p[0]), snapGrid(p[1])];
1564
+ }
1565
+ function onGrid(n) {
1566
+ return Number.isFinite(n) && Math.abs(n - snapGrid(n)) < 1e-6;
1567
+ }
1568
+ //#endregion
1569
+ //#region src/schematic/grid-check.ts
1570
+ /** Never round an existing circuit's wires and library pins independently: that can break nets. */
1571
+ function assertSchematicGrid(source) {
1572
+ const failures = [];
1573
+ const root = parse(source), sheets = children(root, "sheet");
1574
+ const check = (name, p) => {
1575
+ if (!p.every(onGrid) && failures.length < 16) failures.push(`${name} (${p.join(", ")})`);
1576
+ };
1577
+ const flat = applyEdits(source, sheets.map((sheet) => ({
1578
+ start: sheet.start,
1579
+ end: sheet.end,
1580
+ text: ""
1581
+ })));
1582
+ for (const object of listFlatObjects(flat)) if (object.type === "symbol") {
1583
+ if (object.pinError) throw new Error(`GRID_PIN_UNRESOLVED: ${object.reference}: ${object.pinError}`);
1584
+ if (object.at) check(object.reference ?? "symbol", object.at);
1585
+ for (const pin of object.pins ?? []) check(`${object.reference}.${pin.number}`, pin.at);
1586
+ } else if (["wire", "bus"].includes(object.type)) {
1587
+ if (object.from) check(`${object.type} start`, object.from);
1588
+ if (object.to) check(`${object.type} end`, object.to);
1589
+ } else if ([
1590
+ "label",
1591
+ "global_label",
1592
+ "hierarchical_label",
1593
+ "junction",
1594
+ "no_connect"
1595
+ ].includes(object.type) && object.at) check(object.type, object.at);
1596
+ for (const sheet of sheets) {
1597
+ for (const at of children(sheet, "at")) check("sheet", [Number(value(at, 1)), Number(value(at, 2))]);
1598
+ for (const pin of children(sheet, "pin")) for (const at of children(pin, "at")) check(`sheet pin ${value(pin)}`, [Number(value(at, 1)), Number(value(at, 2))]);
1599
+ }
1600
+ for (const kind of ["wire", "bus"]) for (const wire of children(root, kind)) for (const pts of children(wire, "pts")) for (const xy of children(pts, "xy")) check(kind, [Number(value(xy, 1)), Number(value(xy, 2))]);
1601
+ for (const entry of children(root, "bus_entry")) {
1602
+ const at = children(entry, "at")[0], size = children(entry, "size")[0];
1603
+ if (at && size) {
1604
+ const x = Number(value(at, 1)), y = Number(value(at, 2));
1605
+ check("bus entry", [x, y]);
1606
+ check("bus entry end", [x + Number(value(size, 1)), y + Number(value(size, 2))]);
1607
+ }
1608
+ }
1609
+ if (failures.length) throw new Error(`SCHEMATIC_OFF_GRID: expected 50 mil (1.27 mm). ${failures.join("; ")}. Fix source placement/routing or select a 50 mil compatible symbol; do not round library pins and connected wires separately.`);
1610
+ }
1611
+ //#endregion
1612
+ export { required as A, buildProject as C, children as D, applyEdits as E, getKiCadPaths as M, resolveKiCadPaths as N, parse as O, alignVendorSymbolIds as S, normalizeCommonUnits as T, previewExportError as _, snapGrid as a, auditFootprints as b, loadDesignPlan as c, buildFlatCopy as d, listFlatObjects as f, PreviewError as g, flatTextSize as h, onGrid as i, value as j, quote as k, matchDesignPlan as l, summarizeFlatLayout as m, SCHEMATIC_GRID_MM as n, DESIGN_PLAN_MAX_BYTES as o, loadFlatTemplates as p, gridPoint as r, designPlanReference as s, assertSchematicGrid as t, readDesignPlan as u, validatePreviewSchematic as v, isCommonOnlyUnit as w, footprintRequirements as x, assignFootprintText as y };
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { S as getKiCadPaths, _ as children, b as required, c as validatePreviewSchematic, d as footprintRequirements, g as applyEdits, h as normalizeCommonUnits, i as summarizeFlatLayout, l as assignFootprintText, m as isCommonOnlyUnit, n as listFlatObjects, o as PreviewError, p as buildProject, r as loadFlatTemplates, s as previewExportError, t as buildFlatCopy, u as auditFootprints, v as parse, x as value, y as quote } from "./flat--E2zWV_r.js";
1
+ import { A as required, C as buildProject, D as children, E as applyEdits, M as getKiCadPaths, O as parse, T as normalizeCommonUnits, _ as previewExportError, b as auditFootprints, d as buildFlatCopy, f as listFlatObjects, g as PreviewError, j as value, k as quote, l as matchDesignPlan, m as summarizeFlatLayout, o as DESIGN_PLAN_MAX_BYTES, p as loadFlatTemplates, r as gridPoint, s as designPlanReference, t as assertSchematicGrid, v as validatePreviewSchematic, w as isCommonOnlyUnit, x as footprintRequirements, y as assignFootprintText } from "./grid-check-BgWU7a8j.js";
2
2
  import { getLogger } from "@huaqiu/dsh-plugin-log";
3
3
  import { fileURLToPath, pathToFileURL } from "node:url";
4
4
  import { readFileSync } from "node:fs";
@@ -5218,7 +5218,7 @@ function text(value, field, max = 300) {
5218
5218
  }
5219
5219
  function point(value, field) {
5220
5220
  if (!Array.isArray(value) || value.length !== 2 || value.some((item) => typeof item !== "number" || !Number.isFinite(item) || Math.abs(item) > 2e3)) throw new Error(`${field} must be two finite coordinates in millimetres, each within +/-2000`);
5221
- return value;
5221
+ return gridPoint(value);
5222
5222
  }
5223
5223
  function endpoint(value, field) {
5224
5224
  if (Array.isArray(value)) return point(value, field);
@@ -5255,7 +5255,8 @@ function parseFlatRequest(args, listLimit) {
5255
5255
  "setValues",
5256
5256
  "setLabels",
5257
5257
  "addWires",
5258
- "frames"
5258
+ "frames",
5259
+ "designPlan"
5259
5260
  ], "edit_kicad_flat_schematic");
5260
5261
  const sourcePath = requiredString(input, "sourcePath");
5261
5262
  if (input.action === "connections") {
@@ -5394,7 +5395,8 @@ function parseFlatRequest(args, listLimit) {
5394
5395
  sourcePath,
5395
5396
  targetDir: requiredString(input, "targetDir"),
5396
5397
  projectName: requiredString(input, "projectName"),
5397
- plan
5398
+ plan,
5399
+ designPlan: designPlanReference(input.designPlan)
5398
5400
  };
5399
5401
  }
5400
5402
  //#endregion
@@ -5646,6 +5648,30 @@ function previewConfig(config, kicadCliPath) {
5646
5648
  //#endregion
5647
5649
  //#region src/schematic/tools.ts
5648
5650
  /** Read, preview, and generate KiCad schematic copies through workspace-scoped DSH tools. */
5651
+ const designPlanSchema = {
5652
+ type: "object",
5653
+ description: "Required saved hardware design document and exact version returned by run-design.mjs --read-plan. Read and implement this plan before generation.",
5654
+ additionalProperties: false,
5655
+ properties: {
5656
+ path: { type: "string" },
5657
+ sha256: { type: "string" }
5658
+ },
5659
+ required: ["path", "sha256"]
5660
+ };
5661
+ /** Read the exact plan version through workspace policy before any generation writes. */
5662
+ async function readWorkspaceDesignPlan(fs, workspace, input, signal) {
5663
+ const reference = designPlanReference(input);
5664
+ const root = await fs.resolve(workspace, { signal }), target = await fs.resolve(reference.path, {
5665
+ cwd: workspace,
5666
+ signal
5667
+ });
5668
+ if (!fs.contains(root, target) || (await fs.stat(target, signal))?.type !== "file") throw new Error("DESIGN_PLAN_UNAVAILABLE: designPlan.path must be an existing document inside the workspace");
5669
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(await fs.readBytes(target, signal, DESIGN_PLAN_MAX_BYTES));
5670
+ return matchDesignPlan({
5671
+ ...reference,
5672
+ path: target.displayPath
5673
+ }, text);
5674
+ }
5649
5675
  async function writeOutputs(fs, targets, policy, signal) {
5650
5676
  const written = [];
5651
5677
  for (const { target, content } of targets) {
@@ -6106,6 +6132,7 @@ function registerSchematicTools(ctx, config) {
6106
6132
  });
6107
6133
  for (const item of sources) {
6108
6134
  const sheet = item.schematicPath === source.path ? source : await readWorkspaceSchematic(ctx.fs, workspace, item.schematicPath, config.maxSchematicBytes, exec.signal);
6135
+ assertSchematicGrid(sheet.text);
6109
6136
  const footprints = await auditFootprints(sheet.text, resolved.displayPath, ctx.fs, exec.signal);
6110
6137
  if (footprints.status !== "ready") throw new Error(`FOOTPRINTS_UNRESOLVED: ${footprints.unresolved.length} of ${footprints.total} physical instances in ${item.schematicPath} lack project-local footprints. Run localize_kicad_module_footprints check/import and persist script mappings before delivery.`);
6111
6138
  }
@@ -6155,6 +6182,7 @@ function registerSchematicTools(ctx, config) {
6155
6182
  type: "object",
6156
6183
  additionalProperties: false,
6157
6184
  properties: {
6185
+ designPlan: designPlanSchema,
6158
6186
  projectName: {
6159
6187
  type: "string",
6160
6188
  description: "ASCII filename stem: letters, digits, underscores, and hyphens; starts with a letter."
@@ -6202,7 +6230,8 @@ function registerSchematicTools(ctx, config) {
6202
6230
  "projectName",
6203
6231
  "title",
6204
6232
  "targetDir",
6205
- "sheets"
6233
+ "sheets",
6234
+ "designPlan"
6206
6235
  ]
6207
6236
  },
6208
6237
  output: {
@@ -6210,6 +6239,7 @@ function registerSchematicTools(ctx, config) {
6210
6239
  type: "object",
6211
6240
  additionalProperties: false,
6212
6241
  properties: {
6242
+ designPlan: designPlanSchema,
6213
6243
  projectPath: { type: "string" },
6214
6244
  rootPath: { type: "string" },
6215
6245
  symbolLibraryPath: { type: "string" },
@@ -6276,7 +6306,8 @@ function registerSchematicTools(ctx, config) {
6276
6306
  "projectName",
6277
6307
  "title",
6278
6308
  "targetDir",
6279
- "sheets"
6309
+ "sheets",
6310
+ "designPlan"
6280
6311
  ], "create_kicad_hierarchy");
6281
6312
  const projectName = requiredString(input, "projectName", 64);
6282
6313
  if (!/^[A-Za-z][A-Za-z0-9_-]*$/u.test(projectName)) throw new Error("projectName must start with an ASCII letter and contain only letters, digits, underscores, or hyphens");
@@ -6284,6 +6315,7 @@ function registerSchematicTools(ctx, config) {
6284
6315
  const targetDir = requiredString(input, "targetDir");
6285
6316
  if (!Array.isArray(input.sheets) || input.sheets.length < 1 || input.sheets.length > 9) throw new Error("sheets must contain 1 to 9 modules");
6286
6317
  const workspace = workspaceRoot(exec.agent);
6318
+ const designPlan = await readWorkspaceDesignPlan(ctx.fs, workspace, input.designPlan, exec.signal);
6287
6319
  const workspacePath = await ctx.fs.resolve(workspace, { signal: exec.signal });
6288
6320
  const directory = await ctx.fs.resolve(targetDir, {
6289
6321
  cwd: workspace,
@@ -6311,8 +6343,14 @@ function registerSchematicTools(ctx, config) {
6311
6343
  };
6312
6344
  }));
6313
6345
  const files = buildAssembly(projectName, title, sheets);
6346
+ assertSchematicGrid(files.root);
6347
+ for (const child of files.children) assertSchematicGrid(child.text);
6314
6348
  const paths = await publishAssembly(ctx.fs, workspace, targetDir, projectName, files, void 0, exec.signal);
6315
6349
  return {
6350
+ designPlan: {
6351
+ path: designPlan.path,
6352
+ sha256: designPlan.sha256
6353
+ },
6316
6354
  projectPath: paths.projectPath,
6317
6355
  rootPath: paths.rootPath,
6318
6356
  symbolLibraryPath: paths.symbolLibraryPath,
@@ -6343,6 +6381,10 @@ function registerSchematicTools(ctx, config) {
6343
6381
  type: "object",
6344
6382
  additionalProperties: false,
6345
6383
  properties: {
6384
+ designPlan: {
6385
+ ...designPlanSchema,
6386
+ description: "Required for action:create. Saved and reviewed hardware plan path and exact sha256 returned by run-design.mjs --read-plan. Not used for list/connections."
6387
+ },
6346
6388
  action: {
6347
6389
  type: "string",
6348
6390
  enum: [
@@ -6512,6 +6554,7 @@ function registerSchematicTools(ctx, config) {
6512
6554
  type: "object",
6513
6555
  additionalProperties: false,
6514
6556
  properties: {
6557
+ designPlan: designPlanSchema,
6515
6558
  sha256: { type: "string" },
6516
6559
  connection: { type: "object" },
6517
6560
  status: { type: "string" },
@@ -6581,7 +6624,9 @@ function registerSchematicTools(ctx, config) {
6581
6624
  nextAction: "Reuse the requested information; fetch nextOffset only if needed. Planning reads do not require an edited copy. Only when implementing edits, use these UUIDs/coordinates and prefer frames autoBounds:true. Image sizes are in mm; bounds describe layout, not electrical connectivity."
6582
6625
  };
6583
6626
  }
6627
+ const designPlan = await readWorkspaceDesignPlan(ctx.fs, workspace, request.designPlan, exec.signal);
6584
6628
  const files = buildFlatCopy(source.text, request.projectName, request.plan, flatTemplates);
6629
+ assertSchematicGrid(files.schematic);
6585
6630
  const directory = await ctx.fs.resolve(request.targetDir, {
6586
6631
  cwd: workspace,
6587
6632
  signal: exec.signal
@@ -6599,6 +6644,10 @@ function registerSchematicTools(ctx, config) {
6599
6644
  }));
6600
6645
  await writeOutputs(ctx.fs, targets, void 0, exec.signal);
6601
6646
  return {
6647
+ designPlan: {
6648
+ path: designPlan.path,
6649
+ sha256: designPlan.sha256
6650
+ },
6602
6651
  status: "created",
6603
6652
  sourcePath: source.path,
6604
6653
  schematicPath: targets[0].target.displayPath,
@@ -7450,9 +7499,12 @@ const SCHEMATIC_GEN_TOKEN = "@schematic-gen";
7450
7499
  function schematicGenSectionText() {
7451
7500
  return [
7452
7501
  `Schematic Gen: the ${SCHEMATIC_GEN_TOKEN} token in a user message is the Schematic Gen chip — the user explicitly chose it for that request. Produce the design with the \`kicad-schematic-design\` skill, which starts from the hardware design brief when none exists yet.`,
7502
+ "For every new schematic project, first show the requirements summary, scope, key unknowns and recommended choices. Ask with ask_user_question and wait for an actual user reply BEFORE writing formal design documents or generation scripts. If the tool is unavailable, ask in chat and end the turn to wait. Silence, timeout, cancellation or a preselected option is not confirmation. Reuse an already confirmed requirements baseline for the same project; do not ask again unless core requirements change.",
7503
+ "Before writing or running a schematic generation script, save the hardware design document and read it back with run-design.mjs --read-plan. Implement that document; pass its path and sha256 to generation. Never postpone the plan until after drawing. ",
7453
7504
  "Never hand-write .kicad_sch files or KiCad project files yourself; go through the skill and its tools.",
7454
7505
  "Use the user's own words as the design description — if they have not described the circuit yet, ask for the requirements instead of assuming them.",
7455
- "Follow-up messages in the same conversation continue that work without repeating the token."
7506
+ "Follow-up messages in the same conversation continue that work without repeating the token.",
7507
+ "For a simple schematic, plan layout before the first build, collect concrete readability defects and fix them together once, then verify and deliver. Stop layout edits when connections, grid, readability, page fit and project files meet the delivery criteria. Further iterations require an identified blocking defect; do not rebuild for cosmetic preferences or to drive conservative layout warnings to zero. Real errors must still be fixed, regardless of iteration count."
7456
7508
  ].join("\n");
7457
7509
  }
7458
7510
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huaqiu/dsh-tool-kicad-schematic-gen",
3
- "version": "0.6.8",
3
+ "version": "0.6.9",
4
4
  "description": "KiCad 原理图设计:电路模块搜索、器件模型与手册、符号导入、编辑预览、脚本构建及设计技能",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -32,8 +32,8 @@
32
32
  "pdfjs-dist": "6.3.289",
33
33
  "sharp": "0.35.4",
34
34
  "undici": "^8.10.0",
35
- "@huaqiu/dsh-plugin-log": "0.6.8",
36
- "@huaqiu/dsh-sidebar-entry": "0.6.8"
35
+ "@huaqiu/dsh-plugin-log": "0.6.9",
36
+ "@huaqiu/dsh-sidebar-entry": "0.6.9"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@deepseek-ai/cordis": "^4.0.2",