@huaqiu/dsh-eda-host 0.3.24 → 0.4.1

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
@@ -1,16 +1,30 @@
1
1
  # @huaqiu/dsh-eda-host
2
2
 
3
- DSH plugin exposing the current EDA host's schematic netlist to agents through
4
- hq-edge.
3
+ DSH plugin exposing the current EDA host to agents through hq-edge: its
4
+ schematic netlist, and — without reimplementing anything — the environment and
5
+ capability facts the host already knows.
5
6
 
6
7
  ## What it provides
7
8
 
9
+ Netlist:
10
+
8
11
  | Tool | Scope |
9
12
  |---|---|
10
13
  | `get_project_netlist` | complete logical netlist of the current project |
11
14
  | `get_selection_netlist` | netlist of the currently selected components |
12
15
  | `get_active_page_netlist` | netlist of the active schematic page |
13
16
 
17
+ EDA host discovery:
18
+
19
+ | Tool | Returns |
20
+ |---|---|
21
+ | `get_eda_host_info` | host identity, version, installation paths, `kicad-cli` path |
22
+ | `get_eda_host_capabilities` | capabilities the *current* host can actually provide |
23
+
24
+ Discovery is not implementation: a capability is advertised only when the host
25
+ can already provide it, and never because the EDA application is generally
26
+ known for it.
27
+
14
28
  Each tool returns lossless JSON:
15
29
 
16
30
  ```json
@@ -42,7 +56,12 @@ NOT an error. On failure the tool returns `ok: false` with a semantic
42
56
  - `UNIMPLEMENTED` — the host does not support this scope (e.g. active page on
43
57
  KiCad). Do not retry.
44
58
  - `UNAVAILABLE` — hq-edge host unreachable.
45
- - `INTERNAL` — host-side failure.
59
+ - `DEADLINE_EXCEEDED` — the host did not answer within the request budget.
60
+ - `INTERNAL` — host-side failure or a malformed response.
61
+
62
+ `ok:true` with empty `components`/`nets` means exactly one thing: a genuinely
63
+ empty design. It never means "the parse failed" or "the host is unavailable" —
64
+ those are always `ok:false` with a `kind`.
46
65
 
47
66
  ## Architecture
48
67
 
@@ -58,9 +77,45 @@ EDA Host (hq.ir.schematic.v1.NetListService — KiCad / HQ EDA)
58
77
  native EDA model
59
78
  ```
60
79
 
61
- - No `@hqedge/*` dependency: the base URL arrives as overlay config
62
- (`hqEdgeBaseUrl`, delivered by the hq-edge supervisor) or `HQ_EDGE_BASE_URL`
63
- env fallback.
80
+ - No `@hqedge/*` dependency: the base URL is read from `ctx.hqEdge.baseUrl`
81
+ (provided by the edge-bridge), falling back to overlay config
82
+ (`hqEdgeBaseUrl`) or `HQ_EDGE_BASE_URL`.
64
83
  - No KiCad code, no schematic parsing, no host IPC in this package.
65
84
  - Errors are propagated with semantic kinds — never converted into fake empty
66
85
  netlists.
86
+
87
+ ## EDA host discovery
88
+
89
+ Host info crosses the same boundary as the netlist, using the EDA-independent
90
+ `hq.host.v1` contract — not KiCad-shaped messages:
91
+
92
+ ```text
93
+ get_eda_host_info ─┐
94
+ ├─→ hq-edge /api/v1/host/* ─→ hq.host.v1.EdaHostInfoService ─→ host
95
+ get_eda_host_capabilities ─┘
96
+ ```
97
+
98
+ The host adapts its native facts (version, install path, `kicad-cli` location,
99
+ which editors are open) into that contract. hq-edge performs no host-specific
100
+ translation, so KiCad and HQ EDA implement the same service.
101
+
102
+ **`hq.host.v1` has no availability fields.** A host that answers is available,
103
+ and every executable it lists is one it can run — so unavailability is never a
104
+ flag to check, it is a failed request (`ok:false` with `UNAVAILABLE` /
105
+ `FAILED_PRECONDITION` / `DEADLINE_EXCEEDED`). Because proto3 omits default
106
+ values, a host reporting nothing arrives as `{}`; the client fills the proto3
107
+ defaults so a missing field can never be misread as "unavailable". An empty
108
+ executable `path` means the host did not resolve an absolute location, not that
109
+ the tool is missing.
110
+
111
+ ## Request budget
112
+
113
+ Every request has a single bounded budget (`requestTimeoutMs`, default 30 s),
114
+ enforced here and mirrored by the KiCad UI-dispatch bound. A request that
115
+ exceeds it fails with `DEADLINE_EXCEEDED`; it never returns an empty result.
116
+
117
+ ## Dependency boundary
118
+
119
+ This plugin **requires hq-edge**. It is not a standalone DSH plugin: `hqEdge` is
120
+ a required inject, and `apply()` throws if the bridge did not provide a usable
121
+ endpoint. See `docs/tasks/expose-capability.md` in hq-edge.
package/lib/index.d.mts CHANGED
@@ -16,6 +16,19 @@ interface EdaHostConfig {
16
16
  hqEdgeBaseUrl?: string;
17
17
  /** Path prefix on the host; default "/api/v1/netlist". */
18
18
  netlistPathPrefix?: string;
19
+ /** Path prefix for PCB selection; default "/api/v1/pcb-selection". */
20
+ pcbSelectionPathPrefix?: string;
21
+ /** Path prefix for host discovery; default "/api/v1/host". */
22
+ hostPathPrefix?: string;
23
+ /**
24
+ * End-to-end budget for one EDA-host request, in milliseconds.
25
+ *
26
+ * This is the SINGLE request budget for the whole chain — the plugin does
27
+ * not define separate per-layer timeouts. It is enforced here (outermost)
28
+ * and mirrored by the KiCad UI-dispatch bound, so no agent request can wait
29
+ * indefinitely. See docs/tasks/expose-capability.md §6.
30
+ */
31
+ requestTimeoutMs?: number;
19
32
  }
20
33
  //#endregion
21
34
  //#region src/types.d.ts
@@ -57,6 +70,123 @@ interface SchematicNetlist {
57
70
  components: SchematicComponent[];
58
71
  nets: ElectricalNet[];
59
72
  }
73
+ /**
74
+ * Semantic PCB selection types.
75
+ *
76
+ * Self-contained structural mirror of `hq.pcb.v1` (the hq-edge-owned semantic
77
+ * protobuf contract for `PcbSelectionService.GetSelection`). Units follow the
78
+ * hq.pcb.v1 convention: positions/sizes/lengths in mm, rotations in degrees,
79
+ * `id` is the KiCad native object identity.
80
+ */
81
+ interface PcbPoint {
82
+ x: number;
83
+ y: number;
84
+ }
85
+ interface PcbNetRef {
86
+ name: string;
87
+ code: number;
88
+ }
89
+ interface PcbPad {
90
+ id: string;
91
+ pin: string;
92
+ type: string;
93
+ shape: string;
94
+ position?: PcbPoint;
95
+ widthMm: number;
96
+ heightMm: number;
97
+ rotationDeg: number;
98
+ layer: string;
99
+ net?: PcbNetRef;
100
+ }
101
+ interface PcbFootprint {
102
+ id: string;
103
+ reference: string;
104
+ value: string;
105
+ footprint: string;
106
+ position?: PcbPoint;
107
+ rotationDeg: number;
108
+ pads: PcbPad[];
109
+ }
110
+ interface PcbTrack {
111
+ id: string;
112
+ layer: string;
113
+ start?: PcbPoint;
114
+ end?: PcbPoint;
115
+ widthMm: number;
116
+ lengthMm: number;
117
+ net?: PcbNetRef;
118
+ }
119
+ interface PcbArc {
120
+ id: string;
121
+ layer: string;
122
+ start?: PcbPoint;
123
+ end?: PcbPoint;
124
+ mid?: PcbPoint;
125
+ net?: PcbNetRef;
126
+ }
127
+ interface PcbVia {
128
+ id: string;
129
+ layers: string[];
130
+ drillMm: number;
131
+ viaType: string;
132
+ start?: PcbPoint;
133
+ end?: PcbPoint;
134
+ }
135
+ interface PcbSegment {
136
+ start?: PcbPoint;
137
+ end?: PcbPoint;
138
+ }
139
+ interface PcbZone {
140
+ id: string;
141
+ layer: string;
142
+ net?: PcbNetRef;
143
+ outline: PcbSegment[];
144
+ }
145
+ interface PcbShape {
146
+ id: string;
147
+ layer: string;
148
+ shapeType: string;
149
+ start?: PcbPoint;
150
+ end?: PcbPoint;
151
+ center?: PcbPoint;
152
+ radiusMm: number;
153
+ mid?: PcbPoint;
154
+ widthMm: number;
155
+ }
156
+ interface PcbText {
157
+ id: string;
158
+ layer: string;
159
+ text: string;
160
+ position?: PcbPoint;
161
+ rotationDeg: number;
162
+ hJustify: string;
163
+ vJustify: string;
164
+ }
165
+ interface PcbDimension {
166
+ id: string;
167
+ layer: string;
168
+ start?: PcbPoint;
169
+ end?: PcbPoint;
170
+ value: string;
171
+ dimType: string;
172
+ }
173
+ interface PcbGroup {
174
+ id: string;
175
+ itemIds: string[];
176
+ }
177
+ interface PcbSelection {
178
+ footprints: PcbFootprint[];
179
+ pads: PcbPad[];
180
+ tracks: PcbTrack[];
181
+ arcs: PcbArc[];
182
+ vias: PcbVia[];
183
+ zones: PcbZone[];
184
+ shapes: PcbShape[];
185
+ texts: PcbText[];
186
+ dimensions: PcbDimension[];
187
+ groups: PcbGroup[];
188
+ nets: PcbNetRef[];
189
+ }
60
190
  /**
61
191
  * Semantic error categories for netlist retrieval. Mirrors the gRPC status
62
192
  * contract of `hq.ir.schematic.v1.NetListService` so an agent can distinguish
@@ -70,21 +200,117 @@ type NetlistErrorKind =
70
200
  /** Host-side runtime failure. */
71
201
  'INTERNAL' |
72
202
  /** Host unavailable (connection refused). */
73
- 'UNAVAILABLE';
203
+ 'UNAVAILABLE' |
204
+ /** The host did not answer within the request budget. */
205
+ 'DEADLINE_EXCEEDED';
74
206
  declare class NetlistError extends Error {
75
207
  readonly kind: NetlistErrorKind;
76
208
  constructor(kind: NetlistErrorKind, message: string);
77
209
  }
210
+ /**
211
+ * `hq.host.v1` carries **no availability flags**.
212
+ *
213
+ * A host that answers `GetEdaHostInfo` is, by definition, available, and every
214
+ * executable it lists is one it can actually run. Unavailability is therefore
215
+ * never a field to check — it is a failed request, surfaced as `ok:false` with
216
+ * `error.kind` `UNAVAILABLE` / `FAILED_PRECONDITION` / `DEADLINE_EXCEEDED`.
217
+ *
218
+ * Consequence for consumers: never infer "unavailable" from a missing or empty
219
+ * value. Absence of `identity`/`installation` just means proto3 omitted
220
+ * defaults (see `parseEdaHostInfo`), and empty `path` only means the host could
221
+ * not resolve an absolute location — the tool is still runnable by name.
222
+ */
223
+ /**
224
+ * Which EDA application sits behind the semantic host boundary.
225
+ *
226
+ * Deliberately EDA-independent: a new host adds a value here rather than
227
+ * introducing host-specific messages or tools.
228
+ */
229
+ type EdaHostType = 'EDA_HOST_TYPE_UNSPECIFIED' | 'EDA_HOST_TYPE_KICAD' | 'EDA_HOST_TYPE_HQ_EDA';
230
+ /**
231
+ * A capability an EDA host may provide.
232
+ *
233
+ * A capability is advertised only when the host can actually provide it —
234
+ * discovering a capability is not the same as implementing it. Unknown values
235
+ * MUST be treated as "not supported" so a newer host cannot confuse an older
236
+ * plugin.
237
+ */
238
+ type EdaHostCapability = 'EDA_HOST_CAPABILITY_UNSPECIFIED' | 'EDA_HOST_CAPABILITY_SCHEMATIC' | 'EDA_HOST_CAPABILITY_PCB' | 'EDA_HOST_CAPABILITY_NETLIST' | 'EDA_HOST_CAPABILITY_NETLIST_SELECTION' | 'EDA_HOST_CAPABILITY_NETLIST_ACTIVE_PAGE' | 'EDA_HOST_CAPABILITY_ERC' | 'EDA_HOST_CAPABILITY_DRC' | 'EDA_HOST_CAPABILITY_BOM' | 'EDA_HOST_CAPABILITY_PLACEMENT';
239
+ /** A host-provided command line tool. */
240
+ interface EdaHostExecutable {
241
+ /** Stable tool name, e.g. "kicad-cli". */
242
+ name: string;
243
+ /**
244
+ * Absolute path when the host could resolve one.
245
+ *
246
+ * Empty means "resolvable by name only" (e.g. found on `PATH` but the host
247
+ * did not report a location) — never "not installed" or "unusable".
248
+ */
249
+ path: string;
250
+ }
251
+ /** Where the host application lives on disk. */
252
+ interface EdaHostInstallation {
253
+ applicationPath: string;
254
+ /**
255
+ * Executables the host can run. Being listed here IS the availability
256
+ * signal: there is no per-executable availability flag.
257
+ */
258
+ executables: EdaHostExecutable[];
259
+ }
260
+ /** Which EDA host is connected. */
261
+ interface EdaHostIdentity {
262
+ hostType: EdaHostType;
263
+ hostName: string;
264
+ version: string;
265
+ }
266
+ /**
267
+ * EDA-independent description of the connected host.
268
+ *
269
+ * Answering this message is the host's way of saying it is available — there is
270
+ * no `available` field to inspect.
271
+ */
272
+ interface EdaHostInfo {
273
+ identity: EdaHostIdentity;
274
+ installation: EdaHostInstallation;
275
+ }
78
276
  //#endregion
79
277
  //#region src/client.d.ts
278
+ /** Per-call options. `signal` lets DSH cancel a request (see §6 of the task). */
279
+ interface EdaHostRequestOptions {
280
+ signal?: AbortSignal;
281
+ }
80
282
  interface EdaHostClient {
81
283
  /** Netlist of the currently selected components. */
82
- getSelectionNetlist(): Promise<SchematicNetlist>;
284
+ getSelectionNetlist(options?: EdaHostRequestOptions): Promise<SchematicNetlist>;
83
285
  /** Complete logical netlist for the current project. */
84
- getProjectNetlist(): Promise<SchematicNetlist>;
286
+ getProjectNetlist(options?: EdaHostRequestOptions): Promise<SchematicNetlist>;
85
287
  /** Netlist of the current active schematic page. */
86
- getActivePageNetlist(): Promise<SchematicNetlist>;
288
+ getActivePageNetlist(options?: EdaHostRequestOptions): Promise<SchematicNetlist>;
289
+ /**
290
+ * EDA-independent identity / installation of the host. Presence of a result
291
+ * is the availability signal — `hq.host.v1` has no availability field.
292
+ */
293
+ getEdaHostInfo(options?: EdaHostRequestOptions): Promise<EdaHostInfo>;
294
+ /** Capabilities the host currently provides. */
295
+ getEdaHostCapabilities(options?: EdaHostRequestOptions): Promise<EdaHostCapability[]>;
296
+ /**
297
+ * Semantic PCB selection of the current PCB editor (hq.pcb.v1
298
+ * PcbSelectionService.GetSelection bridged through hq-edge). An empty
299
+ * selection resolves to an all-empty `PcbSelection` — never an error.
300
+ */
301
+ getPcbSelection(options?: EdaHostRequestOptions): Promise<PcbSelection>;
87
302
  }
303
+ /**
304
+ * Extract the semantic `SchematicNetlist` from an hq-edge netlist body.
305
+ *
306
+ * hq-edge now emits a single-level body: `{ netlist: { components, nets } }`.
307
+ * Older hq-edge builds serialized the protobuf envelope
308
+ * (`GetNetListResponse.oneof result`), which produced a second `netlist` level
309
+ * and made every populated design look empty. That legacy shape is unwrapped
310
+ * EXPLICITLY — not silently — and anything else is a hard error, because
311
+ * "malformed" must never masquerade as "empty design".
312
+ */
313
+ declare function parseNetlistBody(body: unknown): SchematicNetlist;
88
314
  //#endregion
89
315
  //#region src/index.d.ts
90
316
  /** Plugin id — matches package.json. */
@@ -147,4 +373,4 @@ declare module '@deepseek-ai/cordis' {
147
373
  */
148
374
  declare function apply(ctx: Context, config?: Partial<EdaHostConfig>): () => void;
149
375
  //#endregion
150
- export { type EdaHostClient, type EdaHostConfig, type ElectricalNet, type ElectricalType, NetlistError, type NetlistErrorKind, type PinDefinition, type PinReference, type SchematicComponent, type SchematicNetlist, apply, inject, name };
376
+ export { type EdaHostCapability, type EdaHostClient, type EdaHostConfig, type EdaHostExecutable, type EdaHostIdentity, type EdaHostInfo, type EdaHostInstallation, type EdaHostRequestOptions, type EdaHostType, type ElectricalNet, type ElectricalType, NetlistError, type NetlistErrorKind, type PcbArc, type PcbDimension, type PcbFootprint, type PcbGroup, type PcbNetRef, type PcbPad, type PcbPoint, type PcbSegment, type PcbSelection, type PcbShape, type PcbText, type PcbTrack, type PcbVia, type PcbZone, type PinDefinition, type PinReference, type SchematicComponent, type SchematicNetlist, apply, inject, name, parseNetlistBody };