easyeda-mcp-pro 0.30.0 → 0.32.0

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 (70) hide show
  1. package/dist/bridge/manager.js +1 -1
  2. package/dist/bridge/manager.js.map +1 -1
  3. package/dist/bridge/protocol.d.ts +4 -0
  4. package/dist/bridge/protocol.js +1 -0
  5. package/dist/bridge/protocol.js.map +1 -1
  6. package/dist/bridge/types.d.ts +4 -0
  7. package/dist/bridge/types.js +4 -0
  8. package/dist/bridge/types.js.map +1 -1
  9. package/dist/config/profiles.js +5 -5
  10. package/dist/config/profiles.js.map +1 -1
  11. package/dist/config/version.d.ts +1 -1
  12. package/dist/config/version.js +1 -1
  13. package/dist/schematic-model/audit.d.ts +112 -0
  14. package/dist/schematic-model/audit.js +331 -0
  15. package/dist/schematic-model/audit.js.map +1 -0
  16. package/dist/schematic-model/index.d.ts +5 -0
  17. package/dist/schematic-model/index.js +6 -0
  18. package/dist/schematic-model/index.js.map +1 -0
  19. package/dist/schematic-model/model.d.ts +206 -0
  20. package/dist/schematic-model/model.js +84 -0
  21. package/dist/schematic-model/model.js.map +1 -0
  22. package/dist/schematic-model/net-names.d.ts +16 -0
  23. package/dist/schematic-model/net-names.js +176 -0
  24. package/dist/schematic-model/net-names.js.map +1 -0
  25. package/dist/schematic-model/normalization-plan.d.ts +194 -0
  26. package/dist/schematic-model/normalization-plan.js +669 -0
  27. package/dist/schematic-model/normalization-plan.js.map +1 -0
  28. package/dist/schematic-model/normalize.d.ts +26 -0
  29. package/dist/schematic-model/normalize.js +293 -0
  30. package/dist/schematic-model/normalize.js.map +1 -0
  31. package/dist/tools/L1_drc_erc.js +10 -12
  32. package/dist/tools/L1_drc_erc.js.map +1 -1
  33. package/dist/tools/L1_schematic_batch.d.ts +5 -0
  34. package/dist/tools/L1_schematic_batch.js +556 -0
  35. package/dist/tools/L1_schematic_batch.js.map +1 -0
  36. package/dist/tools/L1_schematic_read.js +411 -25
  37. package/dist/tools/L1_schematic_read.js.map +1 -1
  38. package/dist/tools/L1_schematic_write.js +76 -11
  39. package/dist/tools/L1_schematic_write.js.map +1 -1
  40. package/dist/tools/L1_transactions.d.ts +5 -0
  41. package/dist/tools/L1_transactions.js +350 -0
  42. package/dist/tools/L1_transactions.js.map +1 -0
  43. package/dist/tools/L2_workflows.js +129 -1
  44. package/dist/tools/L2_workflows.js.map +1 -1
  45. package/dist/tools/register.js +4 -0
  46. package/dist/tools/register.js.map +1 -1
  47. package/dist/tools/registry.js +31 -29
  48. package/dist/tools/registry.js.map +1 -1
  49. package/dist/transactions/easyeda.d.ts +25 -0
  50. package/dist/transactions/easyeda.js +129 -0
  51. package/dist/transactions/easyeda.js.map +1 -0
  52. package/dist/transactions/index.d.ts +4 -0
  53. package/dist/transactions/index.js +5 -0
  54. package/dist/transactions/index.js.map +1 -0
  55. package/dist/transactions/manager.d.ts +38 -0
  56. package/dist/transactions/manager.js +475 -0
  57. package/dist/transactions/manager.js.map +1 -0
  58. package/dist/transactions/model.d.ts +111 -0
  59. package/dist/transactions/model.js +2 -0
  60. package/dist/transactions/model.js.map +1 -0
  61. package/dist/transactions/stable.d.ts +4 -0
  62. package/dist/transactions/stable.js +57 -0
  63. package/dist/transactions/stable.js.map +1 -0
  64. package/dist/workflows/rp2040-servo-module-scaffold.d.ts +65 -0
  65. package/dist/workflows/rp2040-servo-module-scaffold.js +280 -0
  66. package/dist/workflows/rp2040-servo-module-scaffold.js.map +1 -0
  67. package/dist/workflows/schematic-safe-region.js +26 -0
  68. package/dist/workflows/schematic-safe-region.js.map +1 -1
  69. package/easyeda-bridge-extension.eext +0 -0
  70. package/package.json +4 -3
@@ -0,0 +1,669 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { z } from 'zod';
3
+ import { auditImportedDesign } from './audit.js';
4
+ export const NormalizationOperationKindSchema = z.enum([
5
+ 'normalize-net-name',
6
+ 'annotate-reference',
7
+ 'set-component-value',
8
+ 'set-component-footprint',
9
+ 'classify-bom',
10
+ ]);
11
+ function isAsciiLetter(char) {
12
+ return (char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z');
13
+ }
14
+ function isAsciiDigit(char) {
15
+ return char >= '0' && char <= '9';
16
+ }
17
+ function isAsciiAlphaNumeric(char) {
18
+ return isAsciiLetter(char) || isAsciiDigit(char);
19
+ }
20
+ function trailingDigitStart(value) {
21
+ let index = value.length;
22
+ while (index > 0 && isAsciiDigit(value.charAt(index - 1)))
23
+ index -= 1;
24
+ return index;
25
+ }
26
+ function isValidReference(value) {
27
+ const digitStart = trailingDigitStart(value);
28
+ if (digitStart === value.length || digitStart === 0 || !isAsciiLetter(value.charAt(0)))
29
+ return false;
30
+ for (let index = 1; index < digitStart; index += 1) {
31
+ if (!isAsciiAlphaNumeric(value.charAt(index)))
32
+ return false;
33
+ }
34
+ return true;
35
+ }
36
+ function splitReference(value) {
37
+ const digitStart = trailingDigitStart(value);
38
+ if (digitStart === value.length)
39
+ return undefined;
40
+ const prefix = value.slice(0, digitStart);
41
+ if (!isValidReference(value))
42
+ return undefined;
43
+ return { prefix, number: Number(value.slice(digitStart)) };
44
+ }
45
+ export const NormalizationComponentOverrideSchema = z
46
+ .object({
47
+ componentId: z.string().min(1),
48
+ reference: z
49
+ .string()
50
+ .trim()
51
+ .refine(isValidReference, { message: 'Reference must end in a numeric designator' })
52
+ .optional(),
53
+ value: z.string().trim().min(1).optional(),
54
+ footprint: z.string().trim().min(1).optional(),
55
+ bomEligible: z.boolean().optional(),
56
+ })
57
+ .strict();
58
+ export const NormalizationPlanOptionsSchema = z
59
+ .object({
60
+ normalizeNetNames: z.boolean().default(true),
61
+ annotateReferences: z.boolean().default(true),
62
+ resolveMetadataExpressions: z.boolean().default(true),
63
+ sourceTruncated: z.boolean().default(false),
64
+ componentOverrides: z.array(NormalizationComponentOverrideSchema).max(500).default([]),
65
+ })
66
+ .strict()
67
+ .superRefine((value, ctx) => {
68
+ const seen = new Set();
69
+ value.componentOverrides.forEach((override, index) => {
70
+ if (seen.has(override.componentId)) {
71
+ ctx.addIssue({
72
+ code: 'custom',
73
+ message: `Duplicate component override for ${override.componentId}`,
74
+ path: ['componentOverrides', index, 'componentId'],
75
+ });
76
+ }
77
+ seen.add(override.componentId);
78
+ });
79
+ });
80
+ const NormalizationOperationSchema = z.object({
81
+ operationId: z.string().min(1),
82
+ kind: NormalizationOperationKindSchema,
83
+ targetType: z.enum(['net', 'component']),
84
+ targetId: z.string().min(1),
85
+ selector: z.record(z.string(), z.string()),
86
+ before: z.record(z.string(), z.unknown()),
87
+ after: z.record(z.string(), z.unknown()),
88
+ automatic: z.boolean(),
89
+ requiresConfirmation: z.boolean(),
90
+ confidence: z.enum(['high', 'medium', 'low']),
91
+ risk: z.enum(['low', 'medium', 'high']),
92
+ reason: z.string().min(1),
93
+ validationGates: z.array(z.string().min(1)).min(1),
94
+ });
95
+ const NormalizationPlanDiagnosticSchema = z.object({
96
+ code: z.string().min(1),
97
+ severity: z.enum(['warning', 'error']),
98
+ message: z.string().min(1),
99
+ componentId: z.string().optional(),
100
+ componentRef: z.string().optional(),
101
+ netName: z.string().optional(),
102
+ suggestedAction: z.string().min(1),
103
+ confidence: z.enum(['high', 'medium', 'low']),
104
+ });
105
+ export const ImportedNormalizationPlanSchema = z.object({
106
+ schemaVersion: z.literal('imported-normalization-plan/v1'),
107
+ planId: z.string().regex(/^norm_[a-f0-9]{16}$/),
108
+ modelHash: z.string().regex(/^[a-f0-9]{64}$/),
109
+ readOnly: z.literal(true),
110
+ status: z.enum(['noop', 'ready', 'review', 'blocked']),
111
+ applicationReady: z.boolean(),
112
+ safeToAutoApply: z.boolean(),
113
+ requiresConfirmation: z.boolean(),
114
+ options: NormalizationPlanOptionsSchema,
115
+ summary: z.object({
116
+ operationCount: z.number().int().nonnegative(),
117
+ automaticOperationCount: z.number().int().nonnegative(),
118
+ confirmationOperationCount: z.number().int().nonnegative(),
119
+ netRenameCount: z.number().int().nonnegative(),
120
+ referenceAnnotationCount: z.number().int().nonnegative(),
121
+ valueUpdateCount: z.number().int().nonnegative(),
122
+ footprintUpdateCount: z.number().int().nonnegative(),
123
+ bomClassificationCount: z.number().int().nonnegative(),
124
+ blockerCount: z.number().int().nonnegative(),
125
+ warningCount: z.number().int().nonnegative(),
126
+ }),
127
+ operations: z.array(NormalizationOperationSchema),
128
+ blockers: z.array(NormalizationPlanDiagnosticSchema),
129
+ warnings: z.array(NormalizationPlanDiagnosticSchema),
130
+ expectedPostconditions: z.array(z.string().min(1)),
131
+ });
132
+ function stableValue(value) {
133
+ if (Array.isArray(value))
134
+ return value.map(stableValue);
135
+ if (value && typeof value === 'object') {
136
+ return Object.fromEntries(Object.entries(value)
137
+ .sort(([a], [b]) => a.localeCompare(b))
138
+ .map(([key, child]) => [key, stableValue(child)]));
139
+ }
140
+ return value;
141
+ }
142
+ function stableJson(value) {
143
+ return JSON.stringify(stableValue(value));
144
+ }
145
+ function sha256(value) {
146
+ return createHash('sha256').update(stableJson(value)).digest('hex');
147
+ }
148
+ function unresolvedExpression(value) {
149
+ return /^=\{[^{}]+\}$/.test(value.trim());
150
+ }
151
+ function operationId(operation) {
152
+ return `nrmop_${sha256(operation).slice(0, 16)}`;
153
+ }
154
+ function createOperation(operation) {
155
+ return NormalizationOperationSchema.parse({
156
+ operationId: operationId(operation),
157
+ ...operation,
158
+ });
159
+ }
160
+ function placeholderReferencePrefix(rawReference) {
161
+ const value = rawReference.trim();
162
+ if (!value.endsWith('?'))
163
+ return undefined;
164
+ const prefix = value.slice(0, -1);
165
+ if (!prefix || !isAsciiLetter(prefix.charAt(0)))
166
+ return undefined;
167
+ for (let index = 1; index < prefix.length; index += 1) {
168
+ if (!isAsciiAlphaNumeric(prefix.charAt(index)))
169
+ return undefined;
170
+ }
171
+ return prefix.toUpperCase();
172
+ }
173
+ function isReferenceTokenCharacter(char) {
174
+ return (isAsciiAlphaNumeric(char) ||
175
+ char === '.' ||
176
+ char === '_' ||
177
+ char === '+' ||
178
+ char === '-' ||
179
+ char === 'Μ' ||
180
+ char === 'Ω');
181
+ }
182
+ function referenceTokens(text) {
183
+ const tokens = [];
184
+ let current = '';
185
+ for (const char of text) {
186
+ if (isReferenceTokenCharacter(char)) {
187
+ current += char;
188
+ continue;
189
+ }
190
+ if (current)
191
+ tokens.push(current);
192
+ current = '';
193
+ }
194
+ if (current)
195
+ tokens.push(current);
196
+ return tokens;
197
+ }
198
+ function containsAnyToken(tokens, candidates) {
199
+ return candidates.some((candidate) => tokens.has(candidate));
200
+ }
201
+ function numericSuffix(token) {
202
+ const normalized = token.replaceAll('Ω', '');
203
+ let index = normalized.startsWith('+') || normalized.startsWith('-') ? 1 : 0;
204
+ const numberStart = index;
205
+ while (isAsciiDigit(normalized.charAt(index)))
206
+ index += 1;
207
+ if (normalized.charAt(index) === '.') {
208
+ index += 1;
209
+ while (isAsciiDigit(normalized.charAt(index)))
210
+ index += 1;
211
+ }
212
+ if (index === numberStart)
213
+ return undefined;
214
+ return normalized.slice(index);
215
+ }
216
+ function passiveValuePrefix(tokens) {
217
+ for (const token of tokens) {
218
+ const suffix = numericSuffix(token);
219
+ if (suffix === undefined)
220
+ continue;
221
+ if (['PF', 'NF', 'UF', 'ΜF'].includes(suffix))
222
+ return 'C';
223
+ if (['', 'K', 'M', 'R'].includes(suffix))
224
+ return 'R';
225
+ }
226
+ return undefined;
227
+ }
228
+ function inferReferencePrefix(component) {
229
+ const explicit = placeholderReferencePrefix(component.rawReference);
230
+ if (explicit)
231
+ return explicit;
232
+ const text = [component.deviceName, component.symbolName, component.value]
233
+ .filter((value) => Boolean(value))
234
+ .join(' ')
235
+ .toUpperCase();
236
+ const tokens = referenceTokens(text);
237
+ const tokenSet = new Set(tokens);
238
+ const rules = [
239
+ [['LED', 'WS2812', 'SK6812'], 'LED'],
240
+ [['RESISTOR', 'RES'], 'R'],
241
+ [['CAPACITOR', 'CAP'], 'C'],
242
+ [['INDUCTOR', 'CHOKE'], 'L'],
243
+ [['DIODE', 'TVS', 'ZENER'], 'D'],
244
+ [['TRANSISTOR', 'MOSFET', 'BJT'], 'Q'],
245
+ [['CONNECTOR', 'HEADER', 'SOCKET', 'USB-C', 'USB_C'], 'J'],
246
+ [['SWITCH', 'BUTTON'], 'SW'],
247
+ [['FUSE', 'POLYFUSE'], 'F'],
248
+ [['CRYSTAL', 'OSCILLATOR', 'XTAL'], 'Y'],
249
+ [['TESTPOINT'], 'TP'],
250
+ [
251
+ ['IC', 'MCU', 'CPU', 'RP2040', 'STM32', 'ESP32', 'REGULATOR', 'DRIVER', 'OPAMP', 'FLASH'],
252
+ 'U',
253
+ ],
254
+ ];
255
+ for (const [keywords, prefix] of rules) {
256
+ if (containsAnyToken(tokenSet, keywords))
257
+ return prefix;
258
+ }
259
+ if (text.includes('TEST POINT'))
260
+ return 'TP';
261
+ return passiveValuePrefix(tokens);
262
+ }
263
+ function referenceState(components) {
264
+ const used = new Set();
265
+ const nextByPrefix = new Map();
266
+ for (const component of components) {
267
+ if (!component.annotated || component.componentKind !== 'part')
268
+ continue;
269
+ const reference = component.reference.toUpperCase();
270
+ used.add(reference);
271
+ const parsed = splitReference(reference);
272
+ if (!parsed)
273
+ continue;
274
+ nextByPrefix.set(parsed.prefix, Math.max(nextByPrefix.get(parsed.prefix) ?? 1, parsed.number + 1));
275
+ }
276
+ return { used, nextByPrefix };
277
+ }
278
+ function allocateReference(prefix, state) {
279
+ let number = state.nextByPrefix.get(prefix) ?? 1;
280
+ let candidate = `${prefix}${number}`;
281
+ while (state.used.has(candidate)) {
282
+ number += 1;
283
+ candidate = `${prefix}${number}`;
284
+ }
285
+ state.used.add(candidate);
286
+ state.nextByPrefix.set(prefix, number + 1);
287
+ return candidate;
288
+ }
289
+ function diagnosticSort(a, b) {
290
+ return (a.code.localeCompare(b.code) ||
291
+ (a.componentRef ?? '').localeCompare(b.componentRef ?? '') ||
292
+ (a.netName ?? '').localeCompare(b.netName ?? '') ||
293
+ (a.componentId ?? '').localeCompare(b.componentId ?? ''));
294
+ }
295
+ function operationSort(a, b) {
296
+ return (a.kind.localeCompare(b.kind) ||
297
+ a.targetId.localeCompare(b.targetId) ||
298
+ stableJson(a.before).localeCompare(stableJson(b.before)) ||
299
+ a.operationId.localeCompare(b.operationId));
300
+ }
301
+ function parsePlanOptions(inputOptions) {
302
+ const parsed = NormalizationPlanOptionsSchema.parse(inputOptions);
303
+ return {
304
+ ...parsed,
305
+ componentOverrides: [...parsed.componentOverrides].sort((a, b) => a.componentId.localeCompare(b.componentId)),
306
+ };
307
+ }
308
+ function addSourceTruncationBlocker(options, blockers) {
309
+ if (!options.sourceTruncated)
310
+ return;
311
+ blockers.push({
312
+ code: 'SOURCE_COMPONENTS_TRUNCATED',
313
+ severity: 'error',
314
+ message: 'A complete normalization plan cannot be produced from a truncated component list.',
315
+ suggestedAction: 'Repeat the preview with a complete live component inventory.',
316
+ confidence: 'high',
317
+ });
318
+ }
319
+ function addOverrideTargetBlockers(model, options, blockers) {
320
+ const knownComponentIds = new Set(model.components.map((component) => component.canonicalComponentId));
321
+ for (const override of options.componentOverrides) {
322
+ if (knownComponentIds.has(override.componentId))
323
+ continue;
324
+ blockers.push({
325
+ code: 'OVERRIDE_COMPONENT_NOT_FOUND',
326
+ severity: 'error',
327
+ message: `Override target ${override.componentId} is not present in the canonical model.`,
328
+ componentId: override.componentId,
329
+ suggestedAction: 'Refresh the audit and use a current canonical component ID.',
330
+ confidence: 'high',
331
+ });
332
+ }
333
+ }
334
+ function addDuplicateReferenceBlockers(model, options, blockers) {
335
+ const audit = auditImportedDesign(model, {
336
+ includeInfo: true,
337
+ sourceTruncated: options.sourceTruncated,
338
+ });
339
+ for (const finding of audit.findings) {
340
+ if (finding.code !== 'DUPLICATE_COMPONENT_REFERENCE')
341
+ continue;
342
+ blockers.push({
343
+ code: 'DUPLICATE_COMPONENT_REFERENCE',
344
+ severity: 'error',
345
+ message: finding.message,
346
+ componentRef: finding.componentRef,
347
+ suggestedAction: finding.suggestedAction,
348
+ confidence: finding.confidence,
349
+ });
350
+ }
351
+ }
352
+ function netRenameOperation(net, rawNetName) {
353
+ const aliasesMerged = net.rawNetNames.length > 1;
354
+ return createOperation({
355
+ kind: 'normalize-net-name',
356
+ targetType: 'net',
357
+ targetId: `net:${rawNetName}`,
358
+ selector: { rawNetName },
359
+ before: { netName: rawNetName },
360
+ after: { netName: net.canonicalNetName },
361
+ automatic: true,
362
+ requiresConfirmation: aliasesMerged,
363
+ confidence: aliasesMerged ? 'medium' : 'high',
364
+ risk: aliasesMerged ? 'medium' : 'low',
365
+ reason: aliasesMerged
366
+ ? 'This imported alias resolves to a canonical net that also has other raw names.'
367
+ : 'This is a recognized imported power or ground alias.',
368
+ validationGates: [
369
+ 'canonical-net-membership-unchanged',
370
+ 'connected-pin-set-unchanged',
371
+ 'native-erc-no-new-errors',
372
+ ],
373
+ });
374
+ }
375
+ function netRenameOperations(net) {
376
+ if (!net.imported || net.kind === 'power-flag')
377
+ return [];
378
+ return net.rawNetNames
379
+ .filter((rawNetName) => rawNetName !== net.canonicalNetName)
380
+ .map((rawNetName) => netRenameOperation(net, rawNetName));
381
+ }
382
+ function addNetRenameOperations(model, options, operations) {
383
+ if (!options.normalizeNetNames)
384
+ return;
385
+ operations.push(...model.nets.flatMap(netRenameOperations));
386
+ }
387
+ function componentSelector(component) {
388
+ const selector = { componentId: component.canonicalComponentId };
389
+ if (component.runtimePrimitiveId)
390
+ selector.runtimePrimitiveId = component.runtimePrimitiveId;
391
+ return selector;
392
+ }
393
+ function addReferencePlan(component, override, options, state, collections) {
394
+ if (component.componentKind !== 'part' || component.annotated || !options.annotateReferences) {
395
+ return;
396
+ }
397
+ const proposedReference = override?.reference;
398
+ const prefix = proposedReference ? undefined : inferReferencePrefix(component);
399
+ let reference = proposedReference;
400
+ if (!reference && prefix)
401
+ reference = allocateReference(prefix, state);
402
+ if (!reference) {
403
+ collections.blockers.push({
404
+ code: 'REFERENCE_PREFIX_UNRESOLVED',
405
+ severity: 'error',
406
+ message: 'A deterministic reference prefix could not be inferred for this component.',
407
+ componentId: component.canonicalComponentId,
408
+ componentRef: component.reference,
409
+ suggestedAction: 'Provide a component override with an explicit unique reference.',
410
+ confidence: 'high',
411
+ });
412
+ return;
413
+ }
414
+ if (proposedReference && state.used.has(reference.toUpperCase())) {
415
+ collections.blockers.push({
416
+ code: 'REFERENCE_OVERRIDE_CONFLICT',
417
+ severity: 'error',
418
+ message: `Reference override ${reference} conflicts with an existing or proposed reference.`,
419
+ componentId: component.canonicalComponentId,
420
+ componentRef: component.reference,
421
+ suggestedAction: 'Choose a unique reference designator.',
422
+ confidence: 'high',
423
+ });
424
+ return;
425
+ }
426
+ if (proposedReference)
427
+ state.used.add(proposedReference.toUpperCase());
428
+ const reason = proposedReference
429
+ ? 'The user supplied an explicit reference override.'
430
+ : `The ${prefix} prefix was inferred and the next unused reference was allocated.`;
431
+ collections.operations.push(createOperation({
432
+ kind: 'annotate-reference',
433
+ targetType: 'component',
434
+ targetId: component.canonicalComponentId,
435
+ selector: componentSelector(component),
436
+ before: { reference: component.rawReference },
437
+ after: { reference },
438
+ automatic: !proposedReference,
439
+ requiresConfirmation: Boolean(proposedReference),
440
+ confidence: proposedReference ? 'high' : 'medium',
441
+ risk: 'medium',
442
+ reason,
443
+ validationGates: [
444
+ 'component-reference-unique',
445
+ 'component-connectivity-unchanged',
446
+ 'native-erc-no-new-errors',
447
+ ],
448
+ }));
449
+ }
450
+ function resolvedMetadataAvailable(rawValue, resolvedValue, options) {
451
+ return (options.resolveMetadataExpressions &&
452
+ unresolvedExpression(rawValue) &&
453
+ !unresolvedExpression(resolvedValue) &&
454
+ resolvedValue.length > 0);
455
+ }
456
+ function addValuePlan(component, override, options, collections) {
457
+ if (component.componentKind !== 'part')
458
+ return;
459
+ const valueOverride = override?.value;
460
+ const overrideChangesState = valueOverride !== undefined && valueOverride !== component.rawValue;
461
+ const resolvedAvailable = resolvedMetadataAvailable(component.rawValue, component.value, options);
462
+ if (overrideChangesState || resolvedAvailable) {
463
+ const value = valueOverride ?? component.value;
464
+ collections.operations.push(createOperation({
465
+ kind: 'set-component-value',
466
+ targetType: 'component',
467
+ targetId: component.canonicalComponentId,
468
+ selector: { componentId: component.canonicalComponentId },
469
+ before: { value: component.rawValue },
470
+ after: { value },
471
+ automatic: !valueOverride,
472
+ requiresConfirmation: Boolean(valueOverride),
473
+ confidence: 'high',
474
+ risk: 'low',
475
+ reason: valueOverride
476
+ ? 'The user supplied an explicit value override.'
477
+ : 'The imported value expression resolves to concrete metadata.',
478
+ validationGates: ['component-identity-unchanged', 'bom-value-resolved'],
479
+ }));
480
+ return;
481
+ }
482
+ if (component.value && !unresolvedExpression(component.value))
483
+ return;
484
+ collections.blockers.push({
485
+ code: 'COMPONENT_VALUE_REQUIRES_INPUT',
486
+ severity: 'error',
487
+ message: 'The component value cannot be normalized without explicit metadata.',
488
+ componentId: component.canonicalComponentId,
489
+ componentRef: component.reference,
490
+ suggestedAction: 'Provide a component override with the intended value.',
491
+ confidence: 'high',
492
+ });
493
+ }
494
+ function addFootprintPlan(component, override, options, collections) {
495
+ if (component.componentKind !== 'part')
496
+ return;
497
+ const footprintOverride = override?.footprint;
498
+ const overrideChangesState = footprintOverride !== undefined && footprintOverride !== component.rawFootprint;
499
+ const resolvedAvailable = resolvedMetadataAvailable(component.rawFootprint, component.footprint, options);
500
+ if (overrideChangesState || resolvedAvailable) {
501
+ const footprint = footprintOverride ?? component.footprint;
502
+ collections.operations.push(createOperation({
503
+ kind: 'set-component-footprint',
504
+ targetType: 'component',
505
+ targetId: component.canonicalComponentId,
506
+ selector: { componentId: component.canonicalComponentId },
507
+ before: { footprint: component.rawFootprint },
508
+ after: { footprint },
509
+ automatic: !footprintOverride,
510
+ requiresConfirmation: Boolean(footprintOverride),
511
+ confidence: 'high',
512
+ risk: 'medium',
513
+ reason: footprintOverride
514
+ ? 'The user supplied an explicit footprint override.'
515
+ : 'The imported footprint expression resolves to concrete metadata.',
516
+ validationGates: [
517
+ 'component-identity-unchanged',
518
+ 'footprint-resolved',
519
+ 'pcb-linkage-reviewed',
520
+ ],
521
+ }));
522
+ return;
523
+ }
524
+ if (component.footprint && !unresolvedExpression(component.footprint))
525
+ return;
526
+ collections.blockers.push({
527
+ code: 'COMPONENT_FOOTPRINT_REQUIRES_INPUT',
528
+ severity: 'error',
529
+ message: 'The component footprint cannot be normalized without explicit metadata.',
530
+ componentId: component.canonicalComponentId,
531
+ componentRef: component.reference,
532
+ suggestedAction: 'Provide a component override with the intended EasyEDA footprint.',
533
+ confidence: 'high',
534
+ });
535
+ }
536
+ function addBomClassificationPlan(component, override, collections) {
537
+ if (!['unknown', 'helper'].includes(component.componentKind))
538
+ return;
539
+ if (override?.bomEligible !== undefined) {
540
+ collections.operations.push(createOperation({
541
+ kind: 'classify-bom',
542
+ targetType: 'component',
543
+ targetId: component.canonicalComponentId,
544
+ selector: { componentId: component.canonicalComponentId },
545
+ before: { bomEligible: component.bomEligible, componentKind: component.componentKind },
546
+ after: { bomEligible: override.bomEligible },
547
+ automatic: false,
548
+ requiresConfirmation: true,
549
+ confidence: 'high',
550
+ risk: 'medium',
551
+ reason: 'The user supplied an explicit BOM-classification override.',
552
+ validationGates: ['bom-count-reviewed', 'component-connectivity-unchanged'],
553
+ }));
554
+ return;
555
+ }
556
+ if (!component.reference && !component.value && !component.footprint)
557
+ return;
558
+ collections.blockers.push({
559
+ code: 'BOM_CLASSIFICATION_REQUIRES_REVIEW',
560
+ severity: 'error',
561
+ message: 'A part-like helper primitive requires an explicit BOM classification.',
562
+ componentId: component.canonicalComponentId,
563
+ componentRef: component.reference,
564
+ suggestedAction: 'Provide a component override with bomEligible true or false.',
565
+ confidence: 'medium',
566
+ });
567
+ }
568
+ function addComponentPlans(model, options, collections) {
569
+ const overrides = new Map(options.componentOverrides.map((override) => [override.componentId, override]));
570
+ const state = referenceState(model.components);
571
+ const components = [...model.components].sort((a, b) => a.canonicalComponentId.localeCompare(b.canonicalComponentId));
572
+ for (const component of components) {
573
+ const override = overrides.get(component.canonicalComponentId);
574
+ addReferencePlan(component, override, options, state, collections);
575
+ addValuePlan(component, override, options, collections);
576
+ addFootprintPlan(component, override, options, collections);
577
+ addBomClassificationPlan(component, override, collections);
578
+ }
579
+ }
580
+ function addPowerFlagWarnings(model, warnings) {
581
+ for (const net of model.nets) {
582
+ if (net.kind !== 'power-flag')
583
+ continue;
584
+ warnings.push({
585
+ code: 'POWER_FLAG_PRESERVED',
586
+ severity: 'warning',
587
+ message: 'Power-flag helper names are intentionally excluded from automatic net renaming.',
588
+ netName: net.canonicalNetName,
589
+ suggestedAction: 'Preserve the helper unless a later runtime-specific operation proves safe.',
590
+ confidence: 'high',
591
+ });
592
+ }
593
+ }
594
+ function determinePlanStatus(blockerCount, requiresConfirmation, operationCount) {
595
+ if (blockerCount > 0)
596
+ return 'blocked';
597
+ if (requiresConfirmation)
598
+ return 'review';
599
+ if (operationCount === 0)
600
+ return 'noop';
601
+ return 'ready';
602
+ }
603
+ function countOperations(operations, kind) {
604
+ return operations.filter((operation) => operation.kind === kind).length;
605
+ }
606
+ function canonicalModelHash(model) {
607
+ return sha256({
608
+ ...model,
609
+ components: [...model.components].sort((a, b) => a.canonicalComponentId.localeCompare(b.canonicalComponentId)),
610
+ nets: [...model.nets].sort((a, b) => a.canonicalNetName.localeCompare(b.canonicalNetName)),
611
+ diagnostics: [...model.diagnostics].sort((a, b) => stableJson(a).localeCompare(stableJson(b))),
612
+ });
613
+ }
614
+ export function previewImportedNormalization(model, inputOptions = {}) {
615
+ const options = parsePlanOptions(inputOptions);
616
+ const collections = { operations: [], blockers: [], warnings: [] };
617
+ addSourceTruncationBlocker(options, collections.blockers);
618
+ addOverrideTargetBlockers(model, options, collections.blockers);
619
+ addDuplicateReferenceBlockers(model, options, collections.blockers);
620
+ addNetRenameOperations(model, options, collections.operations);
621
+ addComponentPlans(model, options, collections);
622
+ addPowerFlagWarnings(model, collections.warnings);
623
+ collections.operations.sort(operationSort);
624
+ collections.blockers.sort(diagnosticSort);
625
+ collections.warnings.sort(diagnosticSort);
626
+ const confirmationOperationCount = collections.operations.filter((operation) => operation.requiresConfirmation).length;
627
+ const applicationReady = collections.blockers.length === 0;
628
+ const requiresConfirmation = confirmationOperationCount > 0 || collections.warnings.length > 0;
629
+ const status = determinePlanStatus(collections.blockers.length, requiresConfirmation, collections.operations.length);
630
+ const modelHash = canonicalModelHash(model);
631
+ const planPayload = {
632
+ schemaVersion: 'imported-normalization-plan/v1',
633
+ modelHash,
634
+ options,
635
+ operations: collections.operations,
636
+ blockers: collections.blockers,
637
+ warnings: collections.warnings,
638
+ };
639
+ return ImportedNormalizationPlanSchema.parse({
640
+ ...planPayload,
641
+ planId: `norm_${sha256(planPayload).slice(0, 16)}`,
642
+ readOnly: true,
643
+ status,
644
+ applicationReady,
645
+ safeToAutoApply: applicationReady && !requiresConfirmation,
646
+ requiresConfirmation,
647
+ summary: {
648
+ operationCount: collections.operations.length,
649
+ automaticOperationCount: collections.operations.filter((operation) => operation.automatic)
650
+ .length,
651
+ confirmationOperationCount,
652
+ netRenameCount: countOperations(collections.operations, 'normalize-net-name'),
653
+ referenceAnnotationCount: countOperations(collections.operations, 'annotate-reference'),
654
+ valueUpdateCount: countOperations(collections.operations, 'set-component-value'),
655
+ footprintUpdateCount: countOperations(collections.operations, 'set-component-footprint'),
656
+ bomClassificationCount: countOperations(collections.operations, 'classify-bom'),
657
+ blockerCount: collections.blockers.length,
658
+ warningCount: collections.warnings.length,
659
+ },
660
+ expectedPostconditions: [
661
+ 'Canonical net membership is unchanged.',
662
+ 'Connected component-pin sets are unchanged.',
663
+ 'Component references are unique after annotation.',
664
+ 'No new native ERC errors are introduced.',
665
+ 'A post-apply canonical model hash is captured for comparison.',
666
+ ],
667
+ });
668
+ }
669
+ //# sourceMappingURL=normalization-plan.js.map