@intelligentelectron/universal-netlist 0.0.12

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.
Files changed (105) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/LICENSE +190 -0
  3. package/NOTICE +4 -0
  4. package/README.md +246 -0
  5. package/dist/circuit-traversal.d.ts +73 -0
  6. package/dist/circuit-traversal.d.ts.map +1 -0
  7. package/dist/circuit-traversal.js +299 -0
  8. package/dist/circuit-traversal.js.map +1 -0
  9. package/dist/cli/commands.d.ts +23 -0
  10. package/dist/cli/commands.d.ts.map +1 -0
  11. package/dist/cli/commands.js +140 -0
  12. package/dist/cli/commands.js.map +1 -0
  13. package/dist/cli/prompts.d.ts +10 -0
  14. package/dist/cli/prompts.d.ts.map +1 -0
  15. package/dist/cli/prompts.js +22 -0
  16. package/dist/cli/prompts.js.map +1 -0
  17. package/dist/cli/shell.d.ts +15 -0
  18. package/dist/cli/shell.d.ts.map +1 -0
  19. package/dist/cli/shell.js +66 -0
  20. package/dist/cli/shell.js.map +1 -0
  21. package/dist/cli/updater.d.ts +46 -0
  22. package/dist/cli/updater.d.ts.map +1 -0
  23. package/dist/cli/updater.js +319 -0
  24. package/dist/cli/updater.js.map +1 -0
  25. package/dist/index.d.ts +16 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +63 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/parsers/altium/connectivity.d.ts +32 -0
  30. package/dist/parsers/altium/connectivity.d.ts.map +1 -0
  31. package/dist/parsers/altium/connectivity.js +308 -0
  32. package/dist/parsers/altium/connectivity.js.map +1 -0
  33. package/dist/parsers/altium/discovery.d.ts +30 -0
  34. package/dist/parsers/altium/discovery.d.ts.map +1 -0
  35. package/dist/parsers/altium/discovery.js +174 -0
  36. package/dist/parsers/altium/discovery.js.map +1 -0
  37. package/dist/parsers/altium/hierarchy.d.ts +29 -0
  38. package/dist/parsers/altium/hierarchy.d.ts.map +1 -0
  39. package/dist/parsers/altium/hierarchy.js +94 -0
  40. package/dist/parsers/altium/hierarchy.js.map +1 -0
  41. package/dist/parsers/altium/index.d.ts +53 -0
  42. package/dist/parsers/altium/index.d.ts.map +1 -0
  43. package/dist/parsers/altium/index.js +404 -0
  44. package/dist/parsers/altium/index.js.map +1 -0
  45. package/dist/parsers/altium/net-extractor.d.ts +24 -0
  46. package/dist/parsers/altium/net-extractor.d.ts.map +1 -0
  47. package/dist/parsers/altium/net-extractor.js +295 -0
  48. package/dist/parsers/altium/net-extractor.js.map +1 -0
  49. package/dist/parsers/altium/ole-reader.d.ts +91 -0
  50. package/dist/parsers/altium/ole-reader.d.ts.map +1 -0
  51. package/dist/parsers/altium/ole-reader.js +304 -0
  52. package/dist/parsers/altium/ole-reader.js.map +1 -0
  53. package/dist/parsers/altium/record-parser.d.ts +21 -0
  54. package/dist/parsers/altium/record-parser.d.ts.map +1 -0
  55. package/dist/parsers/altium/record-parser.js +117 -0
  56. package/dist/parsers/altium/record-parser.js.map +1 -0
  57. package/dist/parsers/altium/schemas.d.ts +277 -0
  58. package/dist/parsers/altium/schemas.d.ts.map +1 -0
  59. package/dist/parsers/altium/schemas.js +246 -0
  60. package/dist/parsers/altium/schemas.js.map +1 -0
  61. package/dist/parsers/altium/types.d.ts +213 -0
  62. package/dist/parsers/altium/types.d.ts.map +1 -0
  63. package/dist/parsers/altium/types.js +180 -0
  64. package/dist/parsers/altium/types.js.map +1 -0
  65. package/dist/parsers/cadence/discovery.d.ts +45 -0
  66. package/dist/parsers/cadence/discovery.d.ts.map +1 -0
  67. package/dist/parsers/cadence/discovery.js +277 -0
  68. package/dist/parsers/cadence/discovery.js.map +1 -0
  69. package/dist/parsers/cadence/index.d.ts +41 -0
  70. package/dist/parsers/cadence/index.d.ts.map +1 -0
  71. package/dist/parsers/cadence/index.js +139 -0
  72. package/dist/parsers/cadence/index.js.map +1 -0
  73. package/dist/parsers/cadence/pstchip-parser.d.ts +23 -0
  74. package/dist/parsers/cadence/pstchip-parser.d.ts.map +1 -0
  75. package/dist/parsers/cadence/pstchip-parser.js +82 -0
  76. package/dist/parsers/cadence/pstchip-parser.js.map +1 -0
  77. package/dist/parsers/cadence/pstxnet-parser.d.ts +15 -0
  78. package/dist/parsers/cadence/pstxnet-parser.d.ts.map +1 -0
  79. package/dist/parsers/cadence/pstxnet-parser.js +55 -0
  80. package/dist/parsers/cadence/pstxnet-parser.js.map +1 -0
  81. package/dist/parsers/cadence/pstxprt-parser.d.ts +24 -0
  82. package/dist/parsers/cadence/pstxprt-parser.d.ts.map +1 -0
  83. package/dist/parsers/cadence/pstxprt-parser.js +75 -0
  84. package/dist/parsers/cadence/pstxprt-parser.js.map +1 -0
  85. package/dist/parsers/index.d.ts +33 -0
  86. package/dist/parsers/index.d.ts.map +1 -0
  87. package/dist/parsers/index.js +49 -0
  88. package/dist/parsers/index.js.map +1 -0
  89. package/dist/server.d.ts +16 -0
  90. package/dist/server.d.ts.map +1 -0
  91. package/dist/server.js +277 -0
  92. package/dist/server.js.map +1 -0
  93. package/dist/service.d.ts +129 -0
  94. package/dist/service.d.ts.map +1 -0
  95. package/dist/service.js +759 -0
  96. package/dist/service.js.map +1 -0
  97. package/dist/types.d.ts +242 -0
  98. package/dist/types.d.ts.map +1 -0
  99. package/dist/types.js +27 -0
  100. package/dist/types.js.map +1 -0
  101. package/dist/version.d.ts +10 -0
  102. package/dist/version.d.ts.map +1 -0
  103. package/dist/version.js +25 -0
  104. package/dist/version.js.map +1 -0
  105. package/package.json +74 -0
@@ -0,0 +1,759 @@
1
+ /**
2
+ * Netlist Service
3
+ *
4
+ * Query methods for Cadence and Altium netlists using absolute paths.
5
+ * All methods take an absolute path to the design FILE as input.
6
+ */
7
+ import { exec } from "child_process";
8
+ import * as fs from "fs";
9
+ import path from "path";
10
+ import { promisify } from "util";
11
+ import { discoverDesigns, findHandler, parseDesign } from "./parsers/index.js";
12
+ // =============================================================================
13
+ // Path Normalization
14
+ // =============================================================================
15
+ /**
16
+ * Normalize a file path to use native separators.
17
+ * This ensures paths work correctly regardless of whether forward or
18
+ * backward slashes are provided (important for cross-platform compatibility).
19
+ *
20
+ * On Windows, path.normalize() converts / to \
21
+ * On Unix, we must manually convert \ to / since path.normalize() doesn't
22
+ * (backslash is a valid filename character on Unix, but agents often send
23
+ * Windows-style paths regardless of platform).
24
+ *
25
+ * Examples:
26
+ * Windows: "C:/Users/foo/bar" -> "C:\\Users\\foo\\bar"
27
+ * Unix: "\\Users\\foo\\bar" -> "/Users/foo/bar"
28
+ */
29
+ const normalizePath = (inputPath) => {
30
+ if (process.platform === "win32") {
31
+ return path.normalize(inputPath);
32
+ }
33
+ // On Unix, convert backslashes to forward slashes before normalizing
34
+ return path.normalize(inputPath.replace(/\\/g, "/"));
35
+ };
36
+ import { naturalSort, traverseCircuitFromNet, computeCircuitHash, isDnsComponent, matchesRefdesType, getRefdesPrefix, isValidRefdes, isGroundNet, } from "./circuit-traversal.js";
37
+ import { compactArray, getPinNet, isErrorResult, } from "./types.js";
38
+ // =============================================================================
39
+ // Design Loading
40
+ // =============================================================================
41
+ /**
42
+ * Normalize unconnected pins to "NC" (No Connect).
43
+ */
44
+ const normalizeUnconnectedPins = (netlist) => {
45
+ for (const component of Object.values(netlist.components)) {
46
+ for (const [pin, net] of Object.entries(component.pins)) {
47
+ if (typeof net === "string") {
48
+ if (net === "") {
49
+ component.pins[pin] = "NC";
50
+ }
51
+ continue;
52
+ }
53
+ if (net?.net === "") {
54
+ net.net = "NC";
55
+ }
56
+ }
57
+ }
58
+ };
59
+ /**
60
+ * Load netlist from an absolute design file path.
61
+ * Delegates to the appropriate handler based on file extension.
62
+ */
63
+ export const loadNetlist = async (designPath) => {
64
+ const normalizedPath = normalizePath(designPath);
65
+ const handler = findHandler(normalizedPath);
66
+ if (!handler) {
67
+ const ext = path.extname(normalizedPath);
68
+ return {
69
+ error: `Unsupported design file format '${ext}'. Supported: .dsn, .cpm (Cadence), .PrjPcb (Altium)`,
70
+ };
71
+ }
72
+ try {
73
+ const parsed = await parseDesign(normalizedPath);
74
+ normalizeUnconnectedPins(parsed);
75
+ return parsed;
76
+ }
77
+ catch (error) {
78
+ const message = error instanceof Error ? error.message : "Unknown error occurred";
79
+ return { error: message };
80
+ }
81
+ };
82
+ // =============================================================================
83
+ // Component Grouping
84
+ // =============================================================================
85
+ const MPN_MISSING_NOTE = "MPN not found in exported netlist data. Tell user to update symbol properties in library, or to point you to the BOM";
86
+ /**
87
+ * Group components by MPN for compact output.
88
+ */
89
+ const groupComponentsByMpn = (entries, includeDns) => {
90
+ const groups = new Map();
91
+ for (const [refdes, component] of entries) {
92
+ const dns = isDnsComponent(component);
93
+ if (!includeDns && dns) {
94
+ continue;
95
+ }
96
+ const mpnTrimmed = component.mpn?.trim() || null;
97
+ const descriptionValue = component.description?.trim() || undefined;
98
+ const commentValue = component.comment?.trim() || undefined;
99
+ const valueValue = component.value?.trim() || undefined;
100
+ const keyBase = mpnTrimmed ? `mpn:${mpnTrimmed}` : `refdes:${refdes}`;
101
+ const groupKey = `${keyBase}||dns:${dns ? "1" : "0"}`;
102
+ if (!groups.has(groupKey)) {
103
+ groups.set(groupKey, {
104
+ mpn: mpnTrimmed,
105
+ description: descriptionValue,
106
+ comment: commentValue,
107
+ value: valueValue,
108
+ dns: dns || undefined,
109
+ notes: mpnTrimmed ? undefined : [MPN_MISSING_NOTE],
110
+ refdes: [],
111
+ });
112
+ }
113
+ else if (valueValue && !groups.get(groupKey).value) {
114
+ groups.get(groupKey).value = valueValue;
115
+ }
116
+ groups.get(groupKey).refdes.push(refdes);
117
+ }
118
+ return Array.from(groups.values())
119
+ .map((group) => {
120
+ const entry = {
121
+ mpn: group.mpn,
122
+ count: group.refdes.length,
123
+ refdes: compactArray(group.refdes.sort(naturalSort)),
124
+ };
125
+ if (group.description !== undefined) {
126
+ entry.description = group.description;
127
+ }
128
+ if (group.comment !== undefined) {
129
+ entry.comment = group.comment;
130
+ }
131
+ if (group.value !== undefined) {
132
+ entry.value = group.value;
133
+ }
134
+ if (group.dns !== undefined) {
135
+ entry.dns = group.dns;
136
+ }
137
+ if (group.notes !== undefined) {
138
+ entry.notes = group.notes;
139
+ }
140
+ return entry;
141
+ })
142
+ .sort((a, b) => (a.mpn ?? "").localeCompare(b.mpn ?? ""));
143
+ };
144
+ /**
145
+ * Aggregate circuit components by MPN for compact output.
146
+ */
147
+ const aggregateCircuitByMpn = (components) => {
148
+ const groups = new Map();
149
+ const unaggregatable = [];
150
+ for (const comp of components) {
151
+ const mpn = comp.mpn?.trim() || null;
152
+ const description = comp.description?.trim() || "";
153
+ const value = comp.value?.trim() || undefined;
154
+ const dnsFlag = comp.dns ? true : undefined;
155
+ let aggregationKey;
156
+ if (mpn) {
157
+ aggregationKey = `mpn:${mpn}`;
158
+ }
159
+ else if (description) {
160
+ aggregationKey = `desc:${description}`;
161
+ }
162
+ else {
163
+ unaggregatable.push(comp);
164
+ continue;
165
+ }
166
+ const nets = comp.connections.map((p) => p.net);
167
+ const netPair = [...nets].sort().join("|");
168
+ const groupKey = `${aggregationKey}||${netPair}||dns:${dnsFlag ? "1" : "0"}`;
169
+ if (!groups.has(groupKey)) {
170
+ groups.set(groupKey, {
171
+ mpn,
172
+ description: description || undefined,
173
+ comment: comp.comment,
174
+ value,
175
+ dns: dnsFlag,
176
+ notes: mpn ? undefined : [MPN_MISSING_NOTE],
177
+ orientations: new Map(),
178
+ });
179
+ }
180
+ else if (value && !groups.get(groupKey).value) {
181
+ groups.get(groupKey).value = value;
182
+ }
183
+ const orientationKey = comp.connections
184
+ .map((p) => `${p.pins.join(",")}:${p.net}`)
185
+ .join("|");
186
+ const group = groups.get(groupKey);
187
+ if (!group.orientations.has(orientationKey)) {
188
+ group.orientations.set(orientationKey, {
189
+ count: 0,
190
+ refdes: [],
191
+ connections: comp.connections,
192
+ });
193
+ }
194
+ const orientation = group.orientations.get(orientationKey);
195
+ orientation.count++;
196
+ if (comp.refdes) {
197
+ orientation.refdes.push(comp.refdes);
198
+ }
199
+ }
200
+ const compactConnections = (connections) => connections.map((c) => ({ net: c.net, pins: compactArray(c.pins) }));
201
+ const result = [];
202
+ for (const group of groups.values()) {
203
+ const orientationsList = Array.from(group.orientations.values()).sort((a, b) => b.count - a.count);
204
+ const totalCount = orientationsList.reduce((sum, o) => sum + o.count, 0);
205
+ const aggregated = {
206
+ mpn: group.mpn,
207
+ total_count: totalCount,
208
+ };
209
+ if (group.description !== undefined) {
210
+ aggregated.description = group.description;
211
+ }
212
+ if (group.comment !== undefined) {
213
+ aggregated.comment = group.comment;
214
+ }
215
+ if (group.value !== undefined) {
216
+ aggregated.value = group.value;
217
+ }
218
+ if (group.dns !== undefined) {
219
+ aggregated.dns = group.dns;
220
+ }
221
+ if (group.notes !== undefined) {
222
+ aggregated.notes = group.notes;
223
+ }
224
+ if (orientationsList.length === 1) {
225
+ aggregated.refdes = compactArray(orientationsList[0].refdes.sort(naturalSort));
226
+ aggregated.connections = compactConnections(orientationsList[0].connections);
227
+ }
228
+ else {
229
+ aggregated.orientations = orientationsList.map((o) => ({
230
+ count: o.count,
231
+ refdes: compactArray(o.refdes.sort(naturalSort)),
232
+ connections: compactConnections(o.connections),
233
+ }));
234
+ }
235
+ result.push(aggregated);
236
+ }
237
+ for (const comp of unaggregatable) {
238
+ const unagg = {
239
+ refdes: comp.refdes,
240
+ mpn: null,
241
+ notes: [MPN_MISSING_NOTE],
242
+ total_count: 1,
243
+ connections: compactConnections(comp.connections),
244
+ };
245
+ if (comp.description !== undefined) {
246
+ unagg.description = comp.description;
247
+ }
248
+ if (comp.comment !== undefined) {
249
+ unagg.comment = comp.comment;
250
+ }
251
+ if (comp.value !== undefined) {
252
+ unagg.value = comp.value;
253
+ }
254
+ if (comp.dns) {
255
+ unagg.dns = true;
256
+ }
257
+ result.push(unagg);
258
+ }
259
+ return result.sort((a, b) => b.total_count - a.total_count);
260
+ };
261
+ // =============================================================================
262
+ // Public API
263
+ // =============================================================================
264
+ /**
265
+ * List all designs in a directory.
266
+ *
267
+ * @param searchPath - Absolute path to search (defaults to CWD)
268
+ * @param pattern - Regex pattern to filter design names
269
+ */
270
+ export const listDesigns = async (searchPath, pattern = ".*") => {
271
+ const resolvedPath = normalizePath(searchPath ?? process.cwd());
272
+ let regex;
273
+ try {
274
+ regex = new RegExp(pattern);
275
+ }
276
+ catch {
277
+ return { error: `Invalid regex pattern '${pattern}'` };
278
+ }
279
+ const designs = await discoverDesigns(resolvedPath);
280
+ return designs
281
+ .filter((design) => regex.test(design.name))
282
+ .map((design) => ({
283
+ name: design.name,
284
+ path: design.sourcePath,
285
+ error: design.error,
286
+ }));
287
+ };
288
+ /**
289
+ * List components of a specific type in a design.
290
+ *
291
+ * @param design - Absolute path to design file
292
+ * @param type - Component type prefix (e.g., "U", "R", "C")
293
+ * @param includeDns - Include DNS (Do Not Stuff) components
294
+ */
295
+ export const listComponents = async (design, type, includeDns = false) => {
296
+ const netlist = await loadNetlist(design);
297
+ if (isErrorResult(netlist)) {
298
+ return netlist;
299
+ }
300
+ const prefix = type.trim().toUpperCase();
301
+ if (!prefix) {
302
+ return { error: "Missing required parameter: type" };
303
+ }
304
+ const entries = Object.entries(netlist.components).filter(([refdes]) => matchesRefdesType(refdes, prefix));
305
+ if (entries.length === 0) {
306
+ const availablePrefixes = Array.from(new Set(Object.keys(netlist.components)
307
+ .filter(isValidRefdes)
308
+ .map(getRefdesPrefix))).sort((a, b) => a.localeCompare(b));
309
+ const designName = path.basename(design, path.extname(design));
310
+ return {
311
+ error: `No components with prefix '${prefix}' found in design '${designName}'. Available prefixes: [${availablePrefixes.join(", ")}]`,
312
+ };
313
+ }
314
+ return {
315
+ components: groupComponentsByMpn(entries, includeDns),
316
+ };
317
+ };
318
+ /**
319
+ * List all nets within a design.
320
+ *
321
+ * @param design - Absolute path to design file
322
+ */
323
+ export const listNets = async (design) => {
324
+ const netlist = await loadNetlist(design);
325
+ if (isErrorResult(netlist)) {
326
+ return netlist;
327
+ }
328
+ const nets = Object.keys(netlist.nets).sort((a, b) => a.localeCompare(b));
329
+ return { nets };
330
+ };
331
+ /**
332
+ * Search nets by regex pattern.
333
+ *
334
+ * @param pattern - Regex pattern
335
+ * @param design - Absolute path to design file
336
+ */
337
+ export const searchNets = async (pattern, design) => {
338
+ let regex;
339
+ try {
340
+ regex = new RegExp(pattern);
341
+ }
342
+ catch {
343
+ return { error: `Invalid regex pattern '${pattern}'` };
344
+ }
345
+ const netlist = await loadNetlist(design);
346
+ if (isErrorResult(netlist)) {
347
+ return netlist;
348
+ }
349
+ const designName = path.basename(design, path.extname(design));
350
+ const nets = Object.keys(netlist.nets).filter((net) => regex.test(net));
351
+ const sorted = nets.sort((a, b) => a.localeCompare(b));
352
+ if (sorted.length === 0) {
353
+ return {
354
+ results: { [designName]: [] },
355
+ notes: [`No nets matched pattern '${pattern}'`],
356
+ };
357
+ }
358
+ return { results: { [designName]: sorted } };
359
+ };
360
+ /**
361
+ * Search components by refdes pattern.
362
+ *
363
+ * @param pattern - Regex pattern
364
+ * @param design - Absolute path to design file
365
+ * @param includeDns - Include DNS components
366
+ */
367
+ export const searchComponentsByRefdes = async (pattern, design, includeDns = false) => {
368
+ // TODO: Support (?i) inline flag for case-insensitive matching
369
+ let regex;
370
+ try {
371
+ regex = new RegExp(pattern, "i");
372
+ }
373
+ catch {
374
+ return { error: `Invalid regex pattern '${pattern}'` };
375
+ }
376
+ const netlist = await loadNetlist(design);
377
+ if (isErrorResult(netlist)) {
378
+ return netlist;
379
+ }
380
+ const designName = path.basename(design, path.extname(design));
381
+ const entries = Object.entries(netlist.components).filter(([refdes]) => regex.test(refdes));
382
+ const grouped = groupComponentsByMpn(entries, includeDns);
383
+ if (grouped.length === 0) {
384
+ return {
385
+ results: { [designName]: [] },
386
+ notes: [`No components matched refdes pattern '${pattern}'`],
387
+ };
388
+ }
389
+ return { results: { [designName]: grouped } };
390
+ };
391
+ /**
392
+ * Search components by MPN pattern.
393
+ *
394
+ * @param pattern - Regex pattern
395
+ * @param design - Absolute path to design file
396
+ * @param includeDns - Include DNS components
397
+ */
398
+ export const searchComponentsByMpn = async (pattern, design, includeDns = false) => {
399
+ // TODO: Support (?i) inline flag for case-insensitive matching
400
+ let regex;
401
+ try {
402
+ regex = new RegExp(pattern, "i");
403
+ }
404
+ catch {
405
+ return { error: `Invalid regex pattern '${pattern}'` };
406
+ }
407
+ const netlist = await loadNetlist(design);
408
+ if (isErrorResult(netlist)) {
409
+ return netlist;
410
+ }
411
+ const designName = path.basename(design, path.extname(design));
412
+ const allComponents = Object.entries(netlist.components);
413
+ const componentsWithMpn = allComponents.filter(([, c]) => c.mpn?.trim());
414
+ const entries = componentsWithMpn.filter(([, component]) => regex.test(component.mpn));
415
+ const grouped = groupComponentsByMpn(entries, includeDns);
416
+ // Case 1: No MPN data exists at all
417
+ if (componentsWithMpn.length === 0) {
418
+ return {
419
+ results: { [designName]: [] },
420
+ notes: [
421
+ "This netlist has no MPN data. Ask user for BOM or schematic PDF",
422
+ ],
423
+ };
424
+ }
425
+ // Case 2: MPN data exists but pattern didn't match
426
+ if (grouped.length === 0) {
427
+ return {
428
+ results: { [designName]: [] },
429
+ notes: [
430
+ `No components matched pattern '${pattern}'. Try a broader pattern or use search_components_by_refdes instead`,
431
+ ],
432
+ };
433
+ }
434
+ return { results: { [designName]: grouped } };
435
+ };
436
+ /**
437
+ * Search components by description pattern.
438
+ *
439
+ * @param pattern - Regex pattern
440
+ * @param design - Absolute path to design file
441
+ * @param includeDns - Include DNS components
442
+ */
443
+ export const searchComponentsByDescription = async (pattern, design, includeDns = false) => {
444
+ // TODO: Support (?i) inline flag for case-insensitive matching
445
+ let regex;
446
+ try {
447
+ regex = new RegExp(pattern, "i");
448
+ }
449
+ catch {
450
+ return { error: `Invalid regex pattern '${pattern}'` };
451
+ }
452
+ const netlist = await loadNetlist(design);
453
+ if (isErrorResult(netlist)) {
454
+ return netlist;
455
+ }
456
+ const designName = path.basename(design, path.extname(design));
457
+ const allComponents = Object.entries(netlist.components);
458
+ const componentsWithDescription = allComponents.filter(([, c]) => c.description?.trim());
459
+ const entries = componentsWithDescription.filter(([, component]) => regex.test(component.description));
460
+ const grouped = groupComponentsByMpn(entries, includeDns);
461
+ // Case 1: No description data exists at all
462
+ if (componentsWithDescription.length === 0) {
463
+ return {
464
+ results: { [designName]: [] },
465
+ notes: [
466
+ "This netlist has no description data. Ask user for BOM or schematic PDF",
467
+ ],
468
+ };
469
+ }
470
+ // Case 2: Description data exists but pattern didn't match
471
+ if (grouped.length === 0) {
472
+ return {
473
+ results: { [designName]: [] },
474
+ notes: [
475
+ `No components matched pattern '${pattern}'. Try a broader pattern or use search_components_by_refdes instead`,
476
+ ],
477
+ };
478
+ }
479
+ return { results: { [designName]: grouped } };
480
+ };
481
+ /**
482
+ * Query component details by reference designator.
483
+ *
484
+ * @param design - Absolute path to design file
485
+ * @param refdes - Component reference designator
486
+ */
487
+ export const queryComponent = async (design, refdes) => {
488
+ const netlist = await loadNetlist(design);
489
+ if (isErrorResult(netlist)) {
490
+ return netlist;
491
+ }
492
+ const targetRefdes = refdes.trim();
493
+ const componentEntry = Object.entries(netlist.components).find(([key]) => key.toLowerCase() === targetRefdes.toLowerCase());
494
+ if (!componentEntry) {
495
+ const designName = path.basename(design, path.extname(design));
496
+ return {
497
+ error: `Component '${refdes}' not found in design '${designName}'. Use list_components() or search_components_by_refdes() to find available components.`,
498
+ };
499
+ }
500
+ const [resolvedRefdes, component] = componentEntry;
501
+ const mpn = component.mpn?.trim() || null;
502
+ const dns = isDnsComponent(component);
503
+ const result = {
504
+ refdes: resolvedRefdes,
505
+ mpn,
506
+ pins: component.pins,
507
+ };
508
+ if (component.description !== undefined) {
509
+ result.description = component.description;
510
+ }
511
+ if (component.comment !== undefined) {
512
+ result.comment = component.comment;
513
+ }
514
+ if (component.value !== undefined) {
515
+ result.value = component.value;
516
+ }
517
+ if (dns) {
518
+ result.dns = true;
519
+ }
520
+ if (!mpn) {
521
+ result.notes = [MPN_MISSING_NOTE];
522
+ }
523
+ return result;
524
+ };
525
+ /**
526
+ * Query circuit starting from a net name.
527
+ *
528
+ * @param design - Absolute path to design file
529
+ * @param netName - Net name
530
+ * @param skipTypes - Component types to skip
531
+ * @param includeDns - Include DNS components
532
+ */
533
+ export const queryXnetByNetName = async (design, netName, skipTypes = [], includeDns = false) => {
534
+ const netlist = await loadNetlist(design);
535
+ if (isErrorResult(netlist)) {
536
+ return netlist;
537
+ }
538
+ const { nets, components } = netlist;
539
+ if (!nets[netName]) {
540
+ const designName = path.basename(design, path.extname(design));
541
+ return {
542
+ error: `Net '${netName}' not found in design '${designName}'. Use search_nets() to find available nets.`,
543
+ };
544
+ }
545
+ if (isGroundNet(netName)) {
546
+ return {
547
+ error: `${netName} is a ground net and cannot be queried.`,
548
+ };
549
+ }
550
+ const traversal = traverseCircuitFromNet(netName, nets, components, {
551
+ skipTypes,
552
+ includeDns,
553
+ });
554
+ const circuitHash = computeCircuitHash(traversal.components);
555
+ const aggregated = aggregateCircuitByMpn(traversal.components);
556
+ const response = {
557
+ starting_point: netName,
558
+ total_components: traversal.components.length,
559
+ unique_configurations: aggregated.length,
560
+ components_by_mpn: aggregated,
561
+ visited_nets: traversal.visited_nets,
562
+ circuit_hash: circuitHash,
563
+ };
564
+ if (Object.keys(traversal.skipped).length > 0) {
565
+ response.skipped = traversal.skipped;
566
+ }
567
+ return response;
568
+ };
569
+ /**
570
+ * Query circuit starting from a component pin.
571
+ *
572
+ * @param design - Absolute path to design file
573
+ * @param pinSpec - Pin specification in "REFDES.PIN" format
574
+ * @param skipTypes - Component types to skip
575
+ * @param includeDns - Include DNS components
576
+ */
577
+ export const queryXnetByPinName = async (design, pinSpec, skipTypes = [], includeDns = false) => {
578
+ const netlist = await loadNetlist(design);
579
+ if (isErrorResult(netlist)) {
580
+ return netlist;
581
+ }
582
+ const parts = pinSpec.split(".");
583
+ if (parts.length !== 2) {
584
+ return {
585
+ error: `Invalid pin name '${pinSpec}'. Expected 'REFDES.PIN'.`,
586
+ };
587
+ }
588
+ const [refdesInput, pinInput] = parts;
589
+ const refdesEntry = Object.entries(netlist.components).find(([refdes]) => refdes.toLowerCase() === refdesInput.trim().toLowerCase());
590
+ if (!refdesEntry) {
591
+ const designName = path.basename(design, path.extname(design));
592
+ return {
593
+ error: `Component '${refdesInput}' not found in design '${designName}'. Use list_components() or search_components_by_refdes() to find available components.`,
594
+ };
595
+ }
596
+ const [resolvedRefdes, component] = refdesEntry;
597
+ const pinKey = Object.keys(component.pins).find((pin) => pin.toLowerCase() === pinInput.trim().toLowerCase());
598
+ if (!pinKey) {
599
+ const pins = Object.keys(component.pins).sort(naturalSort);
600
+ return {
601
+ error: `Pin '${pinSpec}' not found. Component ${resolvedRefdes} has pins: [${pins.join(", ")}]`,
602
+ };
603
+ }
604
+ const connectedNet = getPinNet(component.pins[pinKey]);
605
+ if (isGroundNet(connectedNet)) {
606
+ return {
607
+ error: `Pin ${resolvedRefdes}.${pinKey} is connected to ${connectedNet} (ground) and cannot be queried.`,
608
+ };
609
+ }
610
+ if (connectedNet === "NC") {
611
+ return {
612
+ starting_point: `${resolvedRefdes}.${pinKey}`,
613
+ net: "NC",
614
+ total_components: 0,
615
+ unique_configurations: 0,
616
+ components_by_mpn: [],
617
+ visited_nets: ["NC"],
618
+ circuit_hash: `nc-${resolvedRefdes}.${pinKey}`,
619
+ };
620
+ }
621
+ const { nets, components } = netlist;
622
+ const traversal = traverseCircuitFromNet(connectedNet, nets, components, {
623
+ skipTypes,
624
+ includeDns,
625
+ });
626
+ const circuitHash = computeCircuitHash(traversal.components);
627
+ const aggregated = aggregateCircuitByMpn(traversal.components);
628
+ const response = {
629
+ starting_point: `${resolvedRefdes}.${pinKey}`,
630
+ net: connectedNet,
631
+ total_components: traversal.components.length,
632
+ unique_configurations: aggregated.length,
633
+ components_by_mpn: aggregated,
634
+ visited_nets: traversal.visited_nets,
635
+ circuit_hash: circuitHash,
636
+ };
637
+ if (Object.keys(traversal.skipped).length > 0) {
638
+ response.skipped = traversal.skipped;
639
+ }
640
+ return response;
641
+ };
642
+ // =============================================================================
643
+ // Cadence Netlist Export (Windows Only)
644
+ // =============================================================================
645
+ const execAsync = promisify(exec);
646
+ /**
647
+ * Convert Windows path to bash-compatible path for GitBash/WSL compatibility.
648
+ * Example: C:\foo\bar -> /c/foo/bar
649
+ */
650
+ const toBashPath = (winPath) => winPath
651
+ .replace(/\\/g, "/")
652
+ .replace(/^([A-Za-z]):/, (_, drive) => `/${drive.toLowerCase()}`);
653
+ /**
654
+ * Detect installed Cadence SPB versions from the standard installation directory.
655
+ *
656
+ * @param cadenceBase - Base Cadence installation directory (default: C:/Cadence)
657
+ * @returns Array of detected Cadence installations, sorted by version descending
658
+ */
659
+ export const detectCadenceVersions = async (cadenceBase = "C:/Cadence") => {
660
+ const installs = [];
661
+ try {
662
+ const entries = await fs.promises.readdir(cadenceBase);
663
+ for (const entry of entries) {
664
+ const match = entry.match(/^SPB_(\d+\.\d+)$/);
665
+ if (!match)
666
+ continue;
667
+ const version = match[1];
668
+ const root = path.join(cadenceBase, entry);
669
+ const pstswp = path.join(root, "tools", "bin", "pstswp.exe");
670
+ const config = path.join(root, "tools", "capture", "allegro.cfg");
671
+ // Verify the executables exist
672
+ if (fs.existsSync(pstswp) && fs.existsSync(config)) {
673
+ installs.push({ version, root, pstswp, config });
674
+ }
675
+ }
676
+ // Sort by version descending (latest first)
677
+ installs.sort((a, b) => parseFloat(b.version) - parseFloat(a.version));
678
+ }
679
+ catch {
680
+ // Cadence directory doesn't exist or isn't accessible
681
+ }
682
+ return installs;
683
+ };
684
+ /**
685
+ * Get the latest installed Cadence version.
686
+ *
687
+ * @returns The latest Cadence installation, or null if none found
688
+ */
689
+ export const getLatestCadence = async () => {
690
+ const versions = await detectCadenceVersions();
691
+ return versions[0] ?? null;
692
+ };
693
+ /**
694
+ * Export Cadence schematic netlist to Allegro PCB format.
695
+ * Uses the pstswp utility from Cadence SPB installation.
696
+ *
697
+ * @param dsnPath - Absolute path to .DSN schematic file
698
+ * @returns Export result with output directory and generated files, or error
699
+ */
700
+ export const exportCadenceNetlist = async (dsnPath) => {
701
+ // Platform check
702
+ if (process.platform !== "win32") {
703
+ return {
704
+ error: "Cadence export tools are only available on Windows. The pstswp utility requires a Windows environment with Cadence SPB installed. Manual export: Open Cadence, then: Tools → Create Netlist → PCB Editor format.",
705
+ };
706
+ }
707
+ // Find Cadence installation
708
+ const cadence = await getLatestCadence();
709
+ if (!cadence) {
710
+ return {
711
+ error: "No Cadence SPB installation found in C:/Cadence. Ensure Cadence Design Entry CIS or HDL is installed. Manual export: Open Cadence, then: Tools → Create Netlist → PCB Editor format.",
712
+ };
713
+ }
714
+ const dsnDir = path.dirname(dsnPath);
715
+ const dsnFile = path.basename(dsnPath);
716
+ const outputDir = path.join(dsnDir, "Allegro");
717
+ // Convert to bash paths for command execution (GitBash compatibility)
718
+ const bashDsnDir = toBashPath(dsnDir);
719
+ const pstswp = toBashPath(cadence.pstswp);
720
+ const config = toBashPath(cadence.config);
721
+ const command = `cd "${bashDsnDir}" && "${pstswp}" -pst -d "${dsnFile}" -n "Allegro" -c "${config}" -v 3 -l 255 -j "PCB Footprint"`;
722
+ try {
723
+ const { stdout, stderr } = await execAsync(command, {
724
+ shell: "bash",
725
+ timeout: 120000,
726
+ });
727
+ // List generated files
728
+ let generatedFiles;
729
+ try {
730
+ const files = await fs.promises.readdir(outputDir);
731
+ generatedFiles = files.sort();
732
+ }
733
+ catch {
734
+ // Output directory may not exist if export failed silently
735
+ }
736
+ return {
737
+ success: true,
738
+ outputDir,
739
+ log: (stdout + stderr).trim() || undefined,
740
+ cadenceVersion: cadence.version,
741
+ generatedFiles,
742
+ };
743
+ }
744
+ catch (err) {
745
+ const execError = err;
746
+ return {
747
+ error: `Cadence pstswp failed: ${execError.message ?? "Unknown error"}`,
748
+ };
749
+ }
750
+ };
751
+ // =============================================================================
752
+ // Test Exports
753
+ // =============================================================================
754
+ /**
755
+ * Internal exports for testing purposes only.
756
+ * @internal
757
+ */
758
+ export { MPN_MISSING_NOTE, groupComponentsByMpn, aggregateCircuitByMpn };
759
+ //# sourceMappingURL=service.js.map