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.
- package/README.md +34 -5
- package/docs/api.md +134 -23
- package/docs/model-format.md +174 -20
- package/docs/schemas/altium_toolkit/embedded_assets_a1.schema.json +56 -0
- package/docs/schemas/altium_toolkit/fixture_coverage_matrix_a1.schema.json +89 -0
- package/docs/schemas/altium_toolkit/geometry_bounds_a1.schema.json +86 -0
- package/docs/schemas/altium_toolkit/library_catalog_a1.schema.json +65 -0
- package/docs/schemas/altium_toolkit/library_diff_a1.schema.json +54 -0
- package/docs/schemas/altium_toolkit/library_inspection_a1.schema.json +94 -0
- package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +4 -0
- package/docs/schemas/altium_toolkit/native_stream_inventory_a1.schema.json +66 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +511 -1
- package/docs/schemas/altium_toolkit/parameter_record_inventory_a1.schema.json +84 -0
- package/docs/schemas/altium_toolkit/parser_diagnostics_a1.schema.json +63 -0
- package/docs/schemas/altium_toolkit/parser_value_verification_a1.schema.json +74 -0
- package/docs/schemas/altium_toolkit/pcb_class_report_a1.schema.json +79 -0
- package/docs/schemas/altium_toolkit/pcb_inspection_a1.schema.json +65 -0
- package/docs/schemas/altium_toolkit/pcb_net_membership_a1.schema.json +98 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +3 -0
- package/docs/schemas/altium_toolkit/project_hierarchy_a1.schema.json +79 -0
- package/docs/schemas/altium_toolkit/unsupported_features_a1.schema.json +212 -0
- package/docs/testing.md +2 -0
- package/examples/README.md +21 -0
- package/examples/cli-utils.mjs +148 -0
- package/examples/corpus-smoke.mjs +523 -0
- package/examples/extract-bom.mjs +47 -0
- package/examples/generate-pnp.mjs +59 -0
- package/examples/inspect-board.mjs +70 -0
- package/examples/inspect-schematic.mjs +406 -0
- package/examples/library-catalog.mjs +115 -0
- package/examples/net-report.mjs +61 -0
- package/examples/validate-library.mjs +59 -0
- package/package.json +1 -1
- package/src/core/BinaryReader.mjs +213 -2
- package/src/core/altium/AltiumParser.mjs +352 -14
- package/src/core/altium/AltiumUnits.mjs +205 -0
- package/src/core/altium/AsciiRecordParser.mjs +9 -0
- package/src/core/altium/EmbeddedAssetReportBuilder.mjs +383 -0
- package/src/core/altium/FixtureCoverageMatrixBuilder.mjs +304 -0
- package/src/core/altium/GeometryBoundsReportBuilder.mjs +935 -0
- package/src/core/altium/LibraryCatalogArtifactBuilder.mjs +296 -0
- package/src/core/altium/LibraryDiffReportBuilder.mjs +260 -0
- package/src/core/altium/LibraryInspectionReportBuilder.mjs +156 -0
- package/src/core/altium/LibraryQaReportBuilder.mjs +374 -1
- package/src/core/altium/NativeStreamInventoryBuilder.mjs +177 -0
- package/src/core/altium/NormalizedModelSchema.mjs +3 -31
- package/src/core/altium/ParameterCollection.mjs +431 -0
- package/src/core/altium/ParameterRecordInventoryBuilder.mjs +274 -0
- package/src/core/altium/ParserCompatibilityFuzzer.mjs +106 -2
- package/src/core/altium/ParserDiagnosticNormalizer.mjs +213 -0
- package/src/core/altium/ParserErrors.mjs +90 -0
- package/src/core/altium/ParserFieldCoverageReportBuilder.mjs +656 -0
- package/src/core/altium/ParserUtils.mjs +24 -0
- package/src/core/altium/ParserValueVerificationReportBuilder.mjs +323 -0
- package/src/core/altium/PcbClassReportBuilder.mjs +366 -0
- package/src/core/altium/PcbInspectionReportBuilder.mjs +313 -0
- package/src/core/altium/PcbLayerGroups.mjs +308 -0
- package/src/core/altium/PcbLayerStackCustomDataParser.mjs +183 -0
- package/src/core/altium/PcbLayerStackInterchangeParser.mjs +473 -4
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +83 -15
- package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +74 -4
- package/src/core/altium/PcbLibModelParser.mjs +20 -4
- package/src/core/altium/PcbLibStreamExtractor.mjs +49 -6
- package/src/core/altium/PcbModelParser.mjs +223 -4
- package/src/core/altium/PcbNetMembershipReportBuilder.mjs +270 -0
- package/src/core/altium/PcbStreamExtractor.mjs +130 -6
- package/src/core/altium/PcbTrackPrimitiveParser.mjs +66 -2
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +15 -0
- package/src/core/altium/ProjectHierarchyReportBuilder.mjs +660 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +2 -0
- package/src/core/altium/RawDataPreservationReportBuilder.mjs +348 -0
- package/src/core/altium/SchLibModelParser.mjs +840 -0
- package/src/core/altium/SchLibStreamExtractor.mjs +586 -0
- package/src/core/altium/SchematicBusEntryParser.mjs +3 -2
- package/src/core/altium/SchematicCodeSymbolParser.mjs +663 -0
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +177 -2
- package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +10 -1
- package/src/core/altium/SchematicFieldCoverageReportBuilder.mjs +549 -0
- package/src/core/altium/SchematicHarnessParser.mjs +9 -3
- package/src/core/altium/SchematicHyperlinkParser.mjs +122 -0
- package/src/core/altium/SchematicNetlistBuilder.mjs +271 -8
- package/src/core/altium/SchematicOwnershipGraphParser.mjs +102 -3
- package/src/core/altium/SchematicPinParser.mjs +12 -45
- package/src/core/altium/SchematicPrimitiveParser.mjs +9 -14
- package/src/core/altium/SchematicQaReportBuilder.mjs +2 -0
- package/src/core/altium/SchematicRecordStreamParser.mjs +183 -0
- package/src/core/altium/SchematicRecordTypeRegistry.mjs +6 -1
- package/src/core/altium/SchematicSheetParser.mjs +8 -2
- package/src/core/altium/SchematicStreamExtractor.mjs +64 -25
- package/src/core/altium/SchematicTextOrientationResolver.mjs +76 -0
- package/src/core/altium/SchematicTextParser.mjs +28 -12
- package/src/core/altium/SchematicTextRunParser.mjs +81 -0
- package/src/core/altium/SchematicThumbnailParser.mjs +425 -0
- package/src/core/altium/UnsupportedFeatureReportBuilder.mjs +380 -0
- package/src/parser.mjs +35 -1
- package/src/renderers.mjs +1 -0
- package/src/ui/SchematicShapeRenderer.mjs +49 -6
- package/src/ui/SchematicSvgRenderer.mjs +37 -8
- 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.
|