altium-toolkit 1.1.22 → 1.1.23

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 (99) hide show
  1. package/README.md +34 -5
  2. package/docs/api.md +134 -23
  3. package/docs/model-format.md +174 -20
  4. package/docs/schemas/altium_toolkit/embedded_assets_a1.schema.json +56 -0
  5. package/docs/schemas/altium_toolkit/fixture_coverage_matrix_a1.schema.json +89 -0
  6. package/docs/schemas/altium_toolkit/geometry_bounds_a1.schema.json +86 -0
  7. package/docs/schemas/altium_toolkit/library_catalog_a1.schema.json +65 -0
  8. package/docs/schemas/altium_toolkit/library_diff_a1.schema.json +54 -0
  9. package/docs/schemas/altium_toolkit/library_inspection_a1.schema.json +94 -0
  10. package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +4 -0
  11. package/docs/schemas/altium_toolkit/native_stream_inventory_a1.schema.json +66 -0
  12. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +511 -1
  13. package/docs/schemas/altium_toolkit/parameter_record_inventory_a1.schema.json +84 -0
  14. package/docs/schemas/altium_toolkit/parser_diagnostics_a1.schema.json +63 -0
  15. package/docs/schemas/altium_toolkit/parser_value_verification_a1.schema.json +74 -0
  16. package/docs/schemas/altium_toolkit/pcb_class_report_a1.schema.json +79 -0
  17. package/docs/schemas/altium_toolkit/pcb_inspection_a1.schema.json +65 -0
  18. package/docs/schemas/altium_toolkit/pcb_net_membership_a1.schema.json +98 -0
  19. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +3 -0
  20. package/docs/schemas/altium_toolkit/project_hierarchy_a1.schema.json +79 -0
  21. package/docs/schemas/altium_toolkit/unsupported_features_a1.schema.json +212 -0
  22. package/docs/testing.md +2 -0
  23. package/examples/README.md +21 -0
  24. package/examples/cli-utils.mjs +148 -0
  25. package/examples/corpus-smoke.mjs +523 -0
  26. package/examples/extract-bom.mjs +47 -0
  27. package/examples/generate-pnp.mjs +59 -0
  28. package/examples/inspect-board.mjs +70 -0
  29. package/examples/inspect-schematic.mjs +406 -0
  30. package/examples/library-catalog.mjs +115 -0
  31. package/examples/net-report.mjs +61 -0
  32. package/examples/validate-library.mjs +59 -0
  33. package/package.json +1 -1
  34. package/src/core/BinaryReader.mjs +213 -2
  35. package/src/core/altium/AltiumParser.mjs +352 -14
  36. package/src/core/altium/AltiumUnits.mjs +205 -0
  37. package/src/core/altium/AsciiRecordParser.mjs +9 -0
  38. package/src/core/altium/EmbeddedAssetReportBuilder.mjs +383 -0
  39. package/src/core/altium/FixtureCoverageMatrixBuilder.mjs +304 -0
  40. package/src/core/altium/GeometryBoundsReportBuilder.mjs +935 -0
  41. package/src/core/altium/LibraryCatalogArtifactBuilder.mjs +296 -0
  42. package/src/core/altium/LibraryDiffReportBuilder.mjs +260 -0
  43. package/src/core/altium/LibraryInspectionReportBuilder.mjs +156 -0
  44. package/src/core/altium/LibraryQaReportBuilder.mjs +374 -1
  45. package/src/core/altium/NativeStreamInventoryBuilder.mjs +177 -0
  46. package/src/core/altium/NormalizedModelSchema.mjs +3 -31
  47. package/src/core/altium/ParameterCollection.mjs +431 -0
  48. package/src/core/altium/ParameterRecordInventoryBuilder.mjs +274 -0
  49. package/src/core/altium/ParserCompatibilityFuzzer.mjs +106 -2
  50. package/src/core/altium/ParserDiagnosticNormalizer.mjs +213 -0
  51. package/src/core/altium/ParserErrors.mjs +90 -0
  52. package/src/core/altium/ParserFieldCoverageReportBuilder.mjs +656 -0
  53. package/src/core/altium/ParserUtils.mjs +24 -0
  54. package/src/core/altium/ParserValueVerificationReportBuilder.mjs +323 -0
  55. package/src/core/altium/PcbClassReportBuilder.mjs +366 -0
  56. package/src/core/altium/PcbInspectionReportBuilder.mjs +313 -0
  57. package/src/core/altium/PcbLayerGroups.mjs +308 -0
  58. package/src/core/altium/PcbLayerStackCustomDataParser.mjs +183 -0
  59. package/src/core/altium/PcbLayerStackInterchangeParser.mjs +473 -4
  60. package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +83 -15
  61. package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +74 -4
  62. package/src/core/altium/PcbLibModelParser.mjs +20 -4
  63. package/src/core/altium/PcbLibStreamExtractor.mjs +49 -6
  64. package/src/core/altium/PcbModelParser.mjs +223 -4
  65. package/src/core/altium/PcbNetMembershipReportBuilder.mjs +270 -0
  66. package/src/core/altium/PcbStreamExtractor.mjs +130 -6
  67. package/src/core/altium/PcbTrackPrimitiveParser.mjs +66 -2
  68. package/src/core/altium/ProjectDesignBundleBuilder.mjs +15 -0
  69. package/src/core/altium/ProjectHierarchyReportBuilder.mjs +660 -0
  70. package/src/core/altium/ProjectNetlistExporter.mjs +2 -0
  71. package/src/core/altium/RawDataPreservationReportBuilder.mjs +348 -0
  72. package/src/core/altium/SchLibModelParser.mjs +840 -0
  73. package/src/core/altium/SchLibStreamExtractor.mjs +586 -0
  74. package/src/core/altium/SchematicBusEntryParser.mjs +3 -2
  75. package/src/core/altium/SchematicCodeSymbolParser.mjs +663 -0
  76. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +177 -2
  77. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +10 -1
  78. package/src/core/altium/SchematicFieldCoverageReportBuilder.mjs +549 -0
  79. package/src/core/altium/SchematicHarnessParser.mjs +9 -3
  80. package/src/core/altium/SchematicHyperlinkParser.mjs +122 -0
  81. package/src/core/altium/SchematicNetlistBuilder.mjs +271 -8
  82. package/src/core/altium/SchematicOwnershipGraphParser.mjs +102 -3
  83. package/src/core/altium/SchematicPinParser.mjs +12 -45
  84. package/src/core/altium/SchematicPrimitiveParser.mjs +9 -14
  85. package/src/core/altium/SchematicQaReportBuilder.mjs +2 -0
  86. package/src/core/altium/SchematicRecordStreamParser.mjs +183 -0
  87. package/src/core/altium/SchematicRecordTypeRegistry.mjs +6 -1
  88. package/src/core/altium/SchematicSheetParser.mjs +8 -2
  89. package/src/core/altium/SchematicStreamExtractor.mjs +64 -25
  90. package/src/core/altium/SchematicTextOrientationResolver.mjs +76 -0
  91. package/src/core/altium/SchematicTextParser.mjs +28 -12
  92. package/src/core/altium/SchematicTextRunParser.mjs +81 -0
  93. package/src/core/altium/SchematicThumbnailParser.mjs +425 -0
  94. package/src/core/altium/UnsupportedFeatureReportBuilder.mjs +380 -0
  95. package/src/parser.mjs +35 -1
  96. package/src/renderers.mjs +1 -0
  97. package/src/ui/SchematicShapeRenderer.mjs +49 -6
  98. package/src/ui/SchematicSvgRenderer.mjs +37 -8
  99. package/src/ui/SchematicTypography.mjs +4 -3
@@ -0,0 +1,156 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { LibraryQaReportBuilder } from './LibraryQaReportBuilder.mjs'
6
+
7
+ /**
8
+ * Builds deterministic inspection reports for schematic and PCB libraries.
9
+ */
10
+ export class LibraryInspectionReportBuilder {
11
+ static SCHEMA = 'altium-toolkit.library.inspection.a1'
12
+
13
+ /**
14
+ * Builds a library inspection report from parsed library models.
15
+ * @param {{ schematicLibraries?: object[], pcbLibraries?: object[], qa?: object }} options Library collections.
16
+ * @returns {object}
17
+ */
18
+ static build(options = {}) {
19
+ const schematicLibraries = Array.isArray(options.schematicLibraries)
20
+ ? options.schematicLibraries
21
+ : []
22
+ const pcbLibraries = Array.isArray(options.pcbLibraries)
23
+ ? options.pcbLibraries
24
+ : []
25
+ const qa =
26
+ options.qa ||
27
+ LibraryQaReportBuilder.build({ schematicLibraries, pcbLibraries })
28
+ const libraries = [
29
+ ...schematicLibraries.map((library) =>
30
+ LibraryInspectionReportBuilder.#schematicLibraryRow(library)
31
+ ),
32
+ ...pcbLibraries.map((library) =>
33
+ LibraryInspectionReportBuilder.#pcbLibraryRow(library)
34
+ )
35
+ ]
36
+
37
+ return {
38
+ schema: LibraryInspectionReportBuilder.SCHEMA,
39
+ summary: LibraryInspectionReportBuilder.#summary(
40
+ schematicLibraries,
41
+ pcbLibraries,
42
+ libraries,
43
+ qa
44
+ ),
45
+ libraries,
46
+ qa: {
47
+ schema: qa.schema,
48
+ summary: qa.summary || {}
49
+ },
50
+ duplicates: qa.duplicates || {
51
+ symbols: qa.duplicateSymbols || [],
52
+ footprints: qa.duplicateFootprints || []
53
+ },
54
+ staleImplementations: qa.staleImplementations || [],
55
+ missingModels: qa.missingModels || [],
56
+ multipartMismatches: qa.multipartMismatches || [],
57
+ libraryLint: qa.libraryLint || {
58
+ summary: { issueCount: 0 },
59
+ issues: []
60
+ },
61
+ mergePlan: qa.mergePlan || {},
62
+ issues: qa.issues || []
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Builds one schematic-library inventory row.
68
+ * @param {object} library Schematic library model.
69
+ * @returns {object}
70
+ */
71
+ static #schematicLibraryRow(library) {
72
+ const symbols = Array.isArray(library?.schematicLibrary?.symbols)
73
+ ? library.schematicLibrary.symbols
74
+ : []
75
+
76
+ return {
77
+ fileName: library?.fileName || '',
78
+ kind: 'schematic-library',
79
+ symbolCount: symbols.length,
80
+ pinCount: symbols.reduce(
81
+ (total, symbol) =>
82
+ total +
83
+ (Array.isArray(symbol?.pins) ? symbol.pins.length : 0),
84
+ 0
85
+ )
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Builds one PCB-library inventory row.
91
+ * @param {object} library PCB library model.
92
+ * @returns {object}
93
+ */
94
+ static #pcbLibraryRow(library) {
95
+ const footprints = Array.isArray(library?.pcbLibrary?.footprints)
96
+ ? library.pcbLibrary.footprints
97
+ : []
98
+
99
+ return {
100
+ fileName: library?.fileName || '',
101
+ kind: 'pcb-library',
102
+ footprintCount: footprints.length,
103
+ padCount: footprints.reduce(
104
+ (total, footprint) =>
105
+ total +
106
+ (Array.isArray(footprint?.pads)
107
+ ? footprint.pads.length
108
+ : 0),
109
+ 0
110
+ )
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Builds top-level inspection summary counters.
116
+ * @param {object[]} schematicLibraries Schematic libraries.
117
+ * @param {object[]} pcbLibraries PCB libraries.
118
+ * @param {object[]} libraries Inventory rows.
119
+ * @param {object} qa Library QA report.
120
+ * @returns {object}
121
+ */
122
+ static #summary(schematicLibraries, pcbLibraries, libraries, qa) {
123
+ const qaSummary = qa.summary || {}
124
+
125
+ return {
126
+ schematicLibraryCount: schematicLibraries.length,
127
+ pcbLibraryCount: pcbLibraries.length,
128
+ libraryCount: libraries.length,
129
+ symbolCount: libraries.reduce(
130
+ (total, library) => total + Number(library.symbolCount || 0),
131
+ 0
132
+ ),
133
+ footprintCount: libraries.reduce(
134
+ (total, library) => total + Number(library.footprintCount || 0),
135
+ 0
136
+ ),
137
+ duplicateSymbolCount: Number(qaSummary.duplicateSymbolCount || 0),
138
+ duplicateFootprintCount: Number(
139
+ qaSummary.duplicateFootprintCount || 0
140
+ ),
141
+ staleImplementationCount: Number(
142
+ qaSummary.staleImplementationCount || 0
143
+ ),
144
+ missingModelCount: Number(qaSummary.missingModelCount || 0),
145
+ multipartMismatchCount: Number(
146
+ qaSummary.multipartMismatchCount || 0
147
+ ),
148
+ mergePlanConflictCount: Number(
149
+ qaSummary.mergePlanConflictCount || 0
150
+ ),
151
+ libraryLintIssueCount: Number(qaSummary.libraryLintIssueCount || 0),
152
+ issueCount: Number(qaSummary.issueCount || 0),
153
+ issuesBySeverity: qaSummary.issuesBySeverity || {}
154
+ }
155
+ }
156
+ }
@@ -29,6 +29,10 @@ export class LibraryQaReportBuilder {
29
29
  LibraryQaReportBuilder.#missingModels(pcbLibraries)
30
30
  const multipartMismatches =
31
31
  LibraryQaReportBuilder.#multipartMismatches(schematicLibraries)
32
+ const libraryLint = LibraryQaReportBuilder.#libraryLint(
33
+ schematicLibraries,
34
+ pcbLibraries
35
+ )
32
36
  const mergePlan =
33
37
  LibraryQaReportBuilder.#schematicLibraryMergePlan(
34
38
  schematicLibraries
@@ -69,8 +73,11 @@ export class LibraryQaReportBuilder {
69
73
  diagnostic.code,
70
74
  diagnostic.symbolName
71
75
  )
72
- )
76
+ ),
77
+ ...libraryLint.issues
73
78
  ]
79
+ const issuesBySeverity =
80
+ LibraryQaReportBuilder.#issueSeverityCounts(issues)
74
81
 
75
82
  return {
76
83
  schema: LibraryQaReportBuilder.SCHEMA_ID,
@@ -83,6 +90,8 @@ export class LibraryQaReportBuilder {
83
90
  missingModelCount: missingModels.length,
84
91
  multipartMismatchCount: multipartMismatches.length,
85
92
  mergePlanConflictCount: mergePlan.summary.conflictCount,
93
+ libraryLintIssueCount: libraryLint.summary.issueCount,
94
+ issuesBySeverity,
86
95
  issueCount: issues.length
87
96
  },
88
97
  duplicates: {
@@ -92,6 +101,7 @@ export class LibraryQaReportBuilder {
92
101
  staleImplementations,
93
102
  missingModels,
94
103
  multipartMismatches,
104
+ libraryLint,
95
105
  mergePlan,
96
106
  issues
97
107
  }
@@ -169,6 +179,10 @@ export class LibraryQaReportBuilder {
169
179
  * @returns {object[]}
170
180
  */
171
181
  static #staleImplementations(schematicLibraries, pcbLibraries) {
182
+ if (!Array.isArray(pcbLibraries) || pcbLibraries.length === 0) {
183
+ return []
184
+ }
185
+
172
186
  const availablePcbLibraries = new Set(
173
187
  (pcbLibraries || []).map((library) => library.fileName || '')
174
188
  )
@@ -307,6 +321,365 @@ export class LibraryQaReportBuilder {
307
321
  }
308
322
  }
309
323
 
324
+ /**
325
+ * Builds deterministic symbol and footprint lint diagnostics.
326
+ * @param {object[]} schematicLibraries Schematic libraries.
327
+ * @param {object[]} pcbLibraries PCB footprint libraries.
328
+ * @returns {object}
329
+ */
330
+ static #libraryLint(schematicLibraries, pcbLibraries) {
331
+ const issues = [
332
+ ...LibraryQaReportBuilder.#schematicLintIssues(schematicLibraries),
333
+ ...LibraryQaReportBuilder.#footprintLintIssues(pcbLibraries),
334
+ ...LibraryQaReportBuilder.#symbolFootprintMismatchIssues(
335
+ schematicLibraries,
336
+ pcbLibraries
337
+ )
338
+ ]
339
+
340
+ return {
341
+ schema: 'altium-toolkit.library-lint.a1',
342
+ summary: {
343
+ issueCount: issues.length,
344
+ issuesBySeverity:
345
+ LibraryQaReportBuilder.#issueSeverityCounts(issues)
346
+ },
347
+ issues
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Lints schematic library symbols.
353
+ * @param {object[]} schematicLibraries Schematic libraries.
354
+ * @returns {object[]}
355
+ */
356
+ static #schematicLintIssues(schematicLibraries) {
357
+ const issues = []
358
+
359
+ for (const library of schematicLibraries || []) {
360
+ const libraryFileName = library.fileName || ''
361
+ for (const [index, symbol] of (
362
+ library.schematicLibrary?.symbols || []
363
+ ).entries()) {
364
+ const name = String(symbol.name || '').trim()
365
+ const target = name || libraryFileName + '#' + index
366
+
367
+ if (!name) {
368
+ issues.push(
369
+ LibraryQaReportBuilder.#lintIssue({
370
+ code: 'library.symbol.empty-name',
371
+ target,
372
+ libraryFileName,
373
+ symbolName: name,
374
+ reason: 'symbol name was blank'
375
+ })
376
+ )
377
+ }
378
+
379
+ if (!Array.isArray(symbol.pins)) {
380
+ continue
381
+ }
382
+
383
+ if (symbol.pins.length === 0) {
384
+ issues.push(
385
+ LibraryQaReportBuilder.#lintIssue({
386
+ code: 'library.symbol.no-pins',
387
+ target,
388
+ libraryFileName,
389
+ symbolName: name,
390
+ reason: 'symbol declared an empty pin list'
391
+ })
392
+ )
393
+ }
394
+
395
+ const blankPinCount =
396
+ LibraryQaReportBuilder.#blankDesignatorCount(
397
+ symbol.pins,
398
+ 'designator'
399
+ )
400
+ if (blankPinCount) {
401
+ issues.push(
402
+ LibraryQaReportBuilder.#lintIssue({
403
+ code: 'library.symbol.blank-pin-designator',
404
+ target,
405
+ libraryFileName,
406
+ symbolName: name,
407
+ blankPinCount,
408
+ reason: 'one or more pins had a blank designator'
409
+ })
410
+ )
411
+ }
412
+
413
+ const unnamedPinCount =
414
+ LibraryQaReportBuilder.#blankDesignatorCount(
415
+ symbol.pins,
416
+ 'name'
417
+ )
418
+ if (unnamedPinCount) {
419
+ issues.push(
420
+ LibraryQaReportBuilder.#lintIssue({
421
+ code: 'library.symbol.unnamed-pin',
422
+ severity: 'info',
423
+ target,
424
+ libraryFileName,
425
+ symbolName: name,
426
+ unnamedPinCount,
427
+ reason: 'one or more pins had a blank name'
428
+ })
429
+ )
430
+ }
431
+
432
+ const duplicatePins =
433
+ LibraryQaReportBuilder.#duplicateDesignators(
434
+ symbol.pins,
435
+ 'designator'
436
+ )
437
+ if (duplicatePins.length) {
438
+ issues.push(
439
+ LibraryQaReportBuilder.#lintIssue({
440
+ code: 'library.symbol.duplicate-pin-designator',
441
+ target,
442
+ libraryFileName,
443
+ symbolName: name,
444
+ duplicateDesignators: duplicatePins,
445
+ reason: 'one or more pin designators were reused'
446
+ })
447
+ )
448
+ }
449
+ }
450
+ }
451
+
452
+ return issues
453
+ }
454
+
455
+ /**
456
+ * Lints PCB footprint libraries.
457
+ * @param {object[]} pcbLibraries PCB libraries.
458
+ * @returns {object[]}
459
+ */
460
+ static #footprintLintIssues(pcbLibraries) {
461
+ const issues = []
462
+
463
+ for (const library of pcbLibraries || []) {
464
+ const libraryFileName = library.fileName || ''
465
+ for (const [index, footprint] of (
466
+ library.pcbLibrary?.footprints || []
467
+ ).entries()) {
468
+ const name = String(footprint.name || '').trim()
469
+ const target = name || libraryFileName + '#' + index
470
+
471
+ if (!name) {
472
+ issues.push(
473
+ LibraryQaReportBuilder.#lintIssue({
474
+ code: 'library.footprint.empty-name',
475
+ target,
476
+ libraryFileName,
477
+ footprintName: name,
478
+ reason: 'footprint name was blank'
479
+ })
480
+ )
481
+ }
482
+
483
+ if (!Array.isArray(footprint.pads)) {
484
+ continue
485
+ }
486
+
487
+ if (footprint.pads.length === 0) {
488
+ issues.push(
489
+ LibraryQaReportBuilder.#lintIssue({
490
+ code: 'library.footprint.no-pads',
491
+ target,
492
+ libraryFileName,
493
+ footprintName: name,
494
+ reason: 'footprint declared an empty pad list'
495
+ })
496
+ )
497
+ }
498
+
499
+ const blankPadCount =
500
+ LibraryQaReportBuilder.#blankDesignatorCount(
501
+ footprint.pads,
502
+ 'designator'
503
+ )
504
+ if (blankPadCount) {
505
+ issues.push(
506
+ LibraryQaReportBuilder.#lintIssue({
507
+ code: 'library.footprint.blank-pad-designator',
508
+ target,
509
+ libraryFileName,
510
+ footprintName: name,
511
+ blankPadCount,
512
+ reason: 'one or more pads had a blank designator'
513
+ })
514
+ )
515
+ }
516
+
517
+ const duplicatePads =
518
+ LibraryQaReportBuilder.#duplicateDesignators(
519
+ footprint.pads,
520
+ 'designator'
521
+ )
522
+ if (duplicatePads.length) {
523
+ issues.push(
524
+ LibraryQaReportBuilder.#lintIssue({
525
+ code: 'library.footprint.duplicate-pad-designator',
526
+ target,
527
+ libraryFileName,
528
+ footprintName: name,
529
+ duplicateDesignators: duplicatePads,
530
+ reason: 'one or more pad designators were reused'
531
+ })
532
+ )
533
+ }
534
+ }
535
+ }
536
+
537
+ return issues
538
+ }
539
+
540
+ /**
541
+ * Finds linked symbol/footprint pin-pad count mismatches.
542
+ * @param {object[]} schematicLibraries Schematic libraries.
543
+ * @param {object[]} pcbLibraries PCB libraries.
544
+ * @returns {object[]}
545
+ */
546
+ static #symbolFootprintMismatchIssues(schematicLibraries, pcbLibraries) {
547
+ const footprintsByName =
548
+ LibraryQaReportBuilder.#footprintsByName(pcbLibraries)
549
+ const issues = []
550
+
551
+ for (const library of schematicLibraries || []) {
552
+ const libraryFileName = library.fileName || ''
553
+ for (const symbol of library.schematicLibrary?.symbols || []) {
554
+ if (!Array.isArray(symbol.pins)) {
555
+ continue
556
+ }
557
+
558
+ for (const implementation of symbol.implementations || []) {
559
+ const modelName = String(
560
+ implementation.modelName || ''
561
+ ).trim()
562
+ const footprint = footprintsByName.get(modelName)
563
+
564
+ if (!modelName || !Array.isArray(footprint?.pads)) {
565
+ continue
566
+ }
567
+
568
+ if (symbol.pins.length === footprint.pads.length) {
569
+ continue
570
+ }
571
+
572
+ issues.push(
573
+ LibraryQaReportBuilder.#lintIssue({
574
+ code: 'library.symbol-footprint.pin-pad-count-mismatch',
575
+ target: symbol.name || modelName,
576
+ libraryFileName,
577
+ symbolName: symbol.name || '',
578
+ footprintName: footprint.name || modelName,
579
+ pinCount: symbol.pins.length,
580
+ padCount: footprint.pads.length,
581
+ modelName,
582
+ reason: 'symbol pin count differs from the linked footprint pad count'
583
+ })
584
+ )
585
+ }
586
+ }
587
+ }
588
+
589
+ return issues
590
+ }
591
+
592
+ /**
593
+ * Builds a lookup of footprint names to footprint rows.
594
+ * @param {object[]} pcbLibraries PCB libraries.
595
+ * @returns {Map<string, object>}
596
+ */
597
+ static #footprintsByName(pcbLibraries) {
598
+ const footprintsByName = new Map()
599
+
600
+ for (const library of pcbLibraries || []) {
601
+ for (const footprint of library.pcbLibrary?.footprints || []) {
602
+ const name = String(footprint.name || '').trim()
603
+ if (name && !footprintsByName.has(name)) {
604
+ footprintsByName.set(name, footprint)
605
+ }
606
+ }
607
+ }
608
+
609
+ return footprintsByName
610
+ }
611
+
612
+ /**
613
+ * Counts explicitly blank designator fields.
614
+ * @param {object[]} rows Rows with optional designators.
615
+ * @param {string} key Designator key.
616
+ * @returns {number}
617
+ */
618
+ static #blankDesignatorCount(rows, key) {
619
+ return (rows || []).filter(
620
+ (row) =>
621
+ Object.prototype.hasOwnProperty.call(row, key) &&
622
+ String(row[key] || '').trim() === ''
623
+ ).length
624
+ }
625
+
626
+ /**
627
+ * Finds reused non-empty designators.
628
+ * @param {object[]} rows Rows with optional designators.
629
+ * @param {string} key Designator key.
630
+ * @returns {string[]}
631
+ */
632
+ static #duplicateDesignators(rows, key) {
633
+ const counts = new Map()
634
+
635
+ for (const row of rows || []) {
636
+ const value = String(row?.[key] || '').trim()
637
+ if (!value) continue
638
+ counts.set(value, (counts.get(value) || 0) + 1)
639
+ }
640
+
641
+ return [...counts.entries()]
642
+ .filter(([, count]) => count > 1)
643
+ .map(([value]) => value)
644
+ .sort((left, right) => left.localeCompare(right))
645
+ }
646
+
647
+ /**
648
+ * Builds one detailed lint issue.
649
+ * @param {object} issue Issue fields.
650
+ * @returns {object}
651
+ */
652
+ static #lintIssue(issue) {
653
+ return LibraryQaReportBuilder.#stripEmpty({
654
+ severity: 'warning',
655
+ ...issue
656
+ })
657
+ }
658
+
659
+ /**
660
+ * Counts issues by severity with stable keys.
661
+ * @param {object[]} issues Issue rows.
662
+ * @returns {{ error: number, warning: number, info: number }}
663
+ */
664
+ static #issueSeverityCounts(issues) {
665
+ const counts = {
666
+ error: 0,
667
+ warning: 0,
668
+ info: 0
669
+ }
670
+
671
+ for (const issue of issues || []) {
672
+ const severity = String(issue.severity || 'warning').toLowerCase()
673
+ if (Object.prototype.hasOwnProperty.call(counts, severity)) {
674
+ counts[severity] += 1
675
+ } else {
676
+ counts.warning += 1
677
+ }
678
+ }
679
+
680
+ return counts
681
+ }
682
+
310
683
  /**
311
684
  * Builds duplicate-symbol merge-plan rows.
312
685
  * @param {object[]} schematicLibraries Schematic libraries.