altium-toolkit 1.1.3 → 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 (125) hide show
  1. package/README.md +34 -5
  2. package/docs/api.md +171 -23
  3. package/docs/model-format.md +192 -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 +513 -3
  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 +7 -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/spec/library-scope.md +5 -0
  35. package/src/core/BinaryReader.mjs +213 -2
  36. package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
  37. package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
  38. package/src/core/altium/AltiumParser.mjs +357 -16
  39. package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
  40. package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
  41. package/src/core/altium/AltiumUnits.mjs +205 -0
  42. package/src/core/altium/AsciiRecordParser.mjs +52 -11
  43. package/src/core/altium/EmbeddedAssetReportBuilder.mjs +383 -0
  44. package/src/core/altium/FixtureCoverageMatrixBuilder.mjs +304 -0
  45. package/src/core/altium/GeometryBoundsReportBuilder.mjs +935 -0
  46. package/src/core/altium/LibraryCatalogArtifactBuilder.mjs +296 -0
  47. package/src/core/altium/LibraryDiffReportBuilder.mjs +260 -0
  48. package/src/core/altium/LibraryInspectionReportBuilder.mjs +156 -0
  49. package/src/core/altium/LibraryQaReportBuilder.mjs +374 -1
  50. package/src/core/altium/NativeStreamInventoryBuilder.mjs +177 -0
  51. package/src/core/altium/NormalizedModelSchema.mjs +3 -31
  52. package/src/core/altium/ParameterCollection.mjs +431 -0
  53. package/src/core/altium/ParameterRecordInventoryBuilder.mjs +274 -0
  54. package/src/core/altium/ParserCompatibilityFuzzer.mjs +106 -2
  55. package/src/core/altium/ParserDiagnosticNormalizer.mjs +213 -0
  56. package/src/core/altium/ParserErrors.mjs +90 -0
  57. package/src/core/altium/ParserFieldCoverageReportBuilder.mjs +656 -0
  58. package/src/core/altium/ParserUtils.mjs +24 -0
  59. package/src/core/altium/ParserValueVerificationReportBuilder.mjs +323 -0
  60. package/src/core/altium/PcbClassReportBuilder.mjs +366 -0
  61. package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
  62. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
  63. package/src/core/altium/PcbInspectionReportBuilder.mjs +313 -0
  64. package/src/core/altium/PcbLayerGroups.mjs +308 -0
  65. package/src/core/altium/PcbLayerStackCustomDataParser.mjs +183 -0
  66. package/src/core/altium/PcbLayerStackInterchangeParser.mjs +473 -4
  67. package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +83 -15
  68. package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +74 -4
  69. package/src/core/altium/PcbLibModelParser.mjs +20 -4
  70. package/src/core/altium/PcbLibStreamExtractor.mjs +49 -6
  71. package/src/core/altium/PcbModelParser.mjs +223 -4
  72. package/src/core/altium/PcbNetMembershipReportBuilder.mjs +270 -0
  73. package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
  74. package/src/core/altium/PcbStreamExtractor.mjs +130 -6
  75. package/src/core/altium/PcbTrackPrimitiveParser.mjs +66 -2
  76. package/src/core/altium/ProjectDesignBundleBuilder.mjs +15 -0
  77. package/src/core/altium/ProjectHierarchyReportBuilder.mjs +660 -0
  78. package/src/core/altium/ProjectNetlistExporter.mjs +2 -0
  79. package/src/core/altium/RawDataPreservationReportBuilder.mjs +348 -0
  80. package/src/core/altium/SchLibModelParser.mjs +840 -0
  81. package/src/core/altium/SchLibStreamExtractor.mjs +586 -0
  82. package/src/core/altium/SchematicBusEntryParser.mjs +3 -2
  83. package/src/core/altium/SchematicCodeSymbolParser.mjs +663 -0
  84. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +177 -2
  85. package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
  86. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +10 -1
  87. package/src/core/altium/SchematicFieldCoverageReportBuilder.mjs +549 -0
  88. package/src/core/altium/SchematicHarnessParser.mjs +9 -3
  89. package/src/core/altium/SchematicHyperlinkParser.mjs +122 -0
  90. package/src/core/altium/SchematicNetlistBuilder.mjs +271 -8
  91. package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
  92. package/src/core/altium/SchematicOwnershipGraphParser.mjs +102 -3
  93. package/src/core/altium/SchematicPinParser.mjs +99 -65
  94. package/src/core/altium/SchematicPrimitiveParser.mjs +125 -22
  95. package/src/core/altium/SchematicQaReportBuilder.mjs +2 -0
  96. package/src/core/altium/SchematicRecordStreamParser.mjs +183 -0
  97. package/src/core/altium/SchematicRecordTypeRegistry.mjs +6 -1
  98. package/src/core/altium/SchematicSheetParser.mjs +8 -2
  99. package/src/core/altium/SchematicStreamExtractor.mjs +107 -21
  100. package/src/core/altium/SchematicTextOrientationResolver.mjs +76 -0
  101. package/src/core/altium/SchematicTextParser.mjs +28 -12
  102. package/src/core/altium/SchematicTextRunParser.mjs +81 -0
  103. package/src/core/altium/SchematicThumbnailParser.mjs +425 -0
  104. package/src/core/altium/SourceBundleExporter.mjs +156 -0
  105. package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
  106. package/src/core/altium/SourceComponentClient.mjs +239 -0
  107. package/src/core/altium/UnsupportedFeatureReportBuilder.mjs +380 -0
  108. package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
  109. package/src/parser.mjs +43 -1
  110. package/src/renderers.mjs +1 -0
  111. package/src/styles/altium-renderers.css +6 -6
  112. package/src/ui/PcbArcUtils.mjs +19 -2
  113. package/src/ui/PcbScene3dBuilder.mjs +202 -20
  114. package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
  115. package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
  116. package/src/ui/SchematicColorResolver.mjs +185 -0
  117. package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
  118. package/src/ui/SchematicLineColorResolver.mjs +88 -0
  119. package/src/ui/SchematicNoteRenderer.mjs +5 -1
  120. package/src/ui/SchematicOwnerPinLabelLayout.mjs +269 -8
  121. package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
  122. package/src/ui/SchematicPinSvgRenderer.mjs +229 -62
  123. package/src/ui/SchematicShapeRenderer.mjs +86 -17
  124. package/src/ui/SchematicSvgRenderer.mjs +980 -58
  125. package/src/ui/SchematicTypography.mjs +4 -3
@@ -12,49 +12,49 @@ const KIND_BY_VALUE = {
12
12
  displayName: 'Standard',
13
13
  includeInBom: true,
14
14
  includeInNetlist: true,
15
- includeInPnp: true
15
+ includeInPlacement: true
16
16
  },
17
17
  1: {
18
18
  name: 'mechanical',
19
19
  displayName: 'Mechanical',
20
20
  includeInBom: true,
21
21
  includeInNetlist: true,
22
- includeInPnp: true
22
+ includeInPlacement: true
23
23
  },
24
24
  2: {
25
25
  name: 'graphical',
26
26
  displayName: 'Graphical',
27
27
  includeInBom: false,
28
28
  includeInNetlist: false,
29
- includeInPnp: false
29
+ includeInPlacement: false
30
30
  },
31
31
  3: {
32
32
  name: 'net-tie-bom',
33
33
  displayName: 'Net Tie BOM',
34
34
  includeInBom: true,
35
35
  includeInNetlist: true,
36
- includeInPnp: true
36
+ includeInPlacement: true
37
37
  },
38
38
  4: {
39
39
  name: 'net-tie-no-bom',
40
40
  displayName: 'Net Tie No BOM',
41
41
  includeInBom: false,
42
42
  includeInNetlist: true,
43
- includeInPnp: true
43
+ includeInPlacement: true
44
44
  },
45
45
  5: {
46
46
  name: 'standard-no-bom',
47
47
  displayName: 'Standard No BOM',
48
48
  includeInBom: false,
49
49
  includeInNetlist: true,
50
- includeInPnp: true
50
+ includeInPlacement: true
51
51
  },
52
52
  6: {
53
53
  name: 'jumper',
54
54
  displayName: 'Jumper',
55
55
  includeInBom: true,
56
56
  includeInNetlist: true,
57
- includeInPnp: true
57
+ includeInPlacement: true
58
58
  }
59
59
  }
60
60
 
@@ -65,7 +65,7 @@ export class PcbComponentKindPolicy {
65
65
  /**
66
66
  * Parses native versioned component-kind fields.
67
67
  * @param {Record<string, string | string[]>} fields Native component row.
68
- * @returns {{ value: number, name: string, displayName: string, includeInBom: boolean, includeInNetlist: boolean, includeInPnp: boolean } | undefined}
68
+ * @returns {{ value: number, name: string, displayName: string, includeInBom: boolean, includeInNetlist: boolean, includeInPlacement: boolean } | undefined}
69
69
  */
70
70
  static parse(fields) {
71
71
  if (!PcbComponentKindPolicy.#hasKindField(fields)) return undefined
@@ -76,7 +76,7 @@ export class PcbComponentKindPolicy {
76
76
  displayName: 'Unknown',
77
77
  includeInBom: true,
78
78
  includeInNetlist: true,
79
- includeInPnp: true
79
+ includeInPlacement: true
80
80
  }
81
81
 
82
82
  return {
@@ -20,14 +20,16 @@ export class PcbEmbeddedModelExtractor {
20
20
  * @returns {{ models: { id: string, checksum: number, name: string, format: string, payloadText: string, sourceStream: string, transform: { rotationDeg: { x: number, y: number, z: number }, dzMil: number } }[], componentBodies: { sourceStream: string, layer: string, identifier: string, modelId: string, checksum: number | null, embedded: boolean, name: string, positionMil: { x: number, y: number }, rotationDeg: number, modelRotationDeg: { x: number, y: number, z: number }, dzMil: number, overallHeightMil: number | null, standoffHeightMil: number | null }[] }}
21
21
  */
22
22
  static extractFromStreams(streams) {
23
+ const modelStreamPrefix =
24
+ PcbEmbeddedModelExtractor.#resolveModelStreamPrefix(streams)
23
25
  const modelMetadataRecords =
24
26
  PcbEmbeddedModelExtractor.#parseModelMetadataStream(
25
- streams.get('Models/Data')
27
+ streams.get(modelStreamPrefix + '/Data')
26
28
  )
27
29
  const modelMetadataRows = modelMetadataRecords.map((fields, index) => ({
28
30
  fields,
29
31
  index,
30
- sourceStream: 'Models/' + index,
32
+ sourceStream: modelStreamPrefix + '/' + index,
31
33
  id: PcbEmbeddedModelExtractor.#getField(fields, 'ID'),
32
34
  name: PcbEmbeddedModelExtractor.#getField(fields, 'NAME'),
33
35
  checksum: PcbEmbeddedModelExtractor.#normalizeChecksum(
@@ -70,7 +72,24 @@ export class PcbEmbeddedModelExtractor {
70
72
  }
71
73
 
72
74
  /**
73
- * Parses the length-prefixed `Models/Data` metadata stream.
75
+ * Resolves the embedded-model stream folder used by the compound document.
76
+ * @param {Map<string, Uint8Array>} streams
77
+ * @returns {string}
78
+ */
79
+ static #resolveModelStreamPrefix(streams) {
80
+ if (streams.has('Models/Data')) {
81
+ return 'Models'
82
+ }
83
+
84
+ if (streams.has('Library/Models/Data')) {
85
+ return 'Library/Models'
86
+ }
87
+
88
+ return 'Models'
89
+ }
90
+
91
+ /**
92
+ * Parses the length-prefixed model metadata stream.
74
93
  * @param {Uint8Array | undefined} bytes
75
94
  * @returns {Record<string, string | string[]>[]}
76
95
  */
@@ -0,0 +1,313 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { PcbClassReportBuilder } from './PcbClassReportBuilder.mjs'
6
+ import { PcbNetMembershipReportBuilder } from './PcbNetMembershipReportBuilder.mjs'
7
+ import { PcbRouteAnalysisBuilder } from './PcbRouteAnalysisBuilder.mjs'
8
+ import { PcbStatisticsBuilder } from './PcbStatisticsBuilder.mjs'
9
+
10
+ /**
11
+ * Builds one deterministic inspection artifact for normalized PCB models.
12
+ */
13
+ export class PcbInspectionReportBuilder {
14
+ static SCHEMA = 'altium-toolkit.pcb.inspection.a1'
15
+
16
+ /**
17
+ * Builds a PCB inspection report from a parser root or normalized PCB model.
18
+ * @param {object} input Parser root, PCB model, or options object.
19
+ * @returns {object}
20
+ */
21
+ static build(input = {}) {
22
+ const pcb = PcbInspectionReportBuilder.#pcb(input)
23
+ const diagnostics = PcbInspectionReportBuilder.#diagnostics(input, pcb)
24
+ const statistics =
25
+ input.statistics ||
26
+ pcb.statistics ||
27
+ PcbStatisticsBuilder.build(pcb)
28
+ const routeAnalysis =
29
+ input.routeAnalysis ||
30
+ pcb.routeAnalysis ||
31
+ PcbRouteAnalysisBuilder.build(pcb)
32
+ const netMembership =
33
+ input.netMembership ||
34
+ pcb.netMembership ||
35
+ PcbNetMembershipReportBuilder.build(pcb)
36
+ const classes =
37
+ input.classReport ||
38
+ pcb.classReport ||
39
+ PcbClassReportBuilder.build(pcb)
40
+ const rules = PcbInspectionReportBuilder.#rules(pcb?.rules || [])
41
+ const primitives = PcbInspectionReportBuilder.#primitives(pcb)
42
+ const diagnosticSummary =
43
+ PcbInspectionReportBuilder.#diagnosticSummary(diagnostics)
44
+ const summary = PcbInspectionReportBuilder.#summary({
45
+ input,
46
+ statistics,
47
+ pcb,
48
+ primitives,
49
+ rules,
50
+ diagnosticSummary,
51
+ netMembership,
52
+ classes
53
+ })
54
+
55
+ return {
56
+ schema: PcbInspectionReportBuilder.SCHEMA,
57
+ units: statistics.units || {
58
+ coordinate: 'mil',
59
+ length: 'mil',
60
+ board: 'mil'
61
+ },
62
+ summary,
63
+ board: statistics.board || {},
64
+ statistics,
65
+ primitives,
66
+ rules,
67
+ diagnostics: {
68
+ summary: diagnosticSummary,
69
+ items: diagnostics.map((diagnostic) =>
70
+ PcbInspectionReportBuilder.#diagnosticRow(diagnostic)
71
+ )
72
+ },
73
+ netMembership,
74
+ classes,
75
+ routeAnalysis: {
76
+ schema: routeAnalysis.schema,
77
+ summary: routeAnalysis.summary || {}
78
+ }
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Resolves the PCB payload from a parser root or direct PCB model.
84
+ * @param {object} input Parser root or PCB model.
85
+ * @returns {object}
86
+ */
87
+ static #pcb(input) {
88
+ return input?.pcb || input || {}
89
+ }
90
+
91
+ /**
92
+ * Resolves diagnostics from the root or PCB object.
93
+ * @param {object} input Parser root or options object.
94
+ * @param {object} pcb Normalized PCB model.
95
+ * @returns {object[]}
96
+ */
97
+ static #diagnostics(input, pcb) {
98
+ if (Array.isArray(input?.diagnostics)) {
99
+ return input.diagnostics
100
+ }
101
+ if (Array.isArray(pcb?.diagnostics)) {
102
+ return pcb.diagnostics
103
+ }
104
+
105
+ return []
106
+ }
107
+
108
+ /**
109
+ * Builds primitive-family counters.
110
+ * @param {object} pcb Normalized PCB model.
111
+ * @returns {object}
112
+ */
113
+ static #primitives(pcb) {
114
+ const families = [
115
+ 'pads',
116
+ 'tracks',
117
+ 'arcs',
118
+ 'vias',
119
+ 'fills',
120
+ 'regions',
121
+ 'shapeBasedRegions',
122
+ 'polygons',
123
+ 'texts',
124
+ 'boardRegions'
125
+ ]
126
+ const counts = Object.fromEntries(
127
+ families.map((family) => [
128
+ family + 'Count',
129
+ Array.isArray(pcb?.[family]) ? pcb[family].length : 0
130
+ ])
131
+ )
132
+ const primitiveCount = [
133
+ 'padsCount',
134
+ 'tracksCount',
135
+ 'arcsCount',
136
+ 'viasCount',
137
+ 'fillsCount',
138
+ 'regionsCount',
139
+ 'shapeBasedRegionsCount',
140
+ 'polygonsCount'
141
+ ].reduce((total, key) => total + Number(counts[key] || 0), 0)
142
+
143
+ return {
144
+ ...counts,
145
+ componentCount: Array.isArray(pcb?.components)
146
+ ? pcb.components.length
147
+ : 0,
148
+ primitiveCount
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Builds design-rule counters by kind.
154
+ * @param {object[]} rules Design-rule rows.
155
+ * @returns {object}
156
+ */
157
+ static #rules(rules) {
158
+ const counts = new Map()
159
+
160
+ for (const rule of Array.isArray(rules) ? rules : []) {
161
+ const kind = PcbInspectionReportBuilder.#ruleKind(rule)
162
+ counts.set(kind, Number(counts.get(kind) || 0) + 1)
163
+ }
164
+
165
+ return {
166
+ count: Array.isArray(rules) ? rules.length : 0,
167
+ byKind: [...counts.entries()]
168
+ .map(([kind, count]) => ({ kind, count }))
169
+ .sort((left, right) =>
170
+ PcbInspectionReportBuilder.#naturalCompare(
171
+ left.kind,
172
+ right.kind
173
+ )
174
+ )
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Resolves one design-rule kind label.
180
+ * @param {object} rule Design-rule row.
181
+ * @returns {string}
182
+ */
183
+ static #ruleKind(rule) {
184
+ const kind = String(
185
+ rule?.kind ||
186
+ rule?.ruleKind ||
187
+ rule?.kindName ||
188
+ rule?.type ||
189
+ 'unknown'
190
+ ).trim()
191
+
192
+ return kind || 'unknown'
193
+ }
194
+
195
+ /**
196
+ * Builds diagnostic severity counters.
197
+ * @param {object[]} diagnostics Diagnostic rows.
198
+ * @returns {object}
199
+ */
200
+ static #diagnosticSummary(diagnostics) {
201
+ const rows = Array.isArray(diagnostics) ? diagnostics : []
202
+
203
+ return {
204
+ diagnosticCount: rows.length,
205
+ errorCount: rows.filter(
206
+ (diagnostic) =>
207
+ PcbInspectionReportBuilder.#severity(diagnostic) === 'error'
208
+ ).length,
209
+ warningCount: rows.filter(
210
+ (diagnostic) =>
211
+ PcbInspectionReportBuilder.#severity(diagnostic) ===
212
+ 'warning'
213
+ ).length,
214
+ infoCount: rows.filter(
215
+ (diagnostic) =>
216
+ PcbInspectionReportBuilder.#severity(diagnostic) === 'info'
217
+ ).length
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Builds one normalized diagnostic row.
223
+ * @param {object} diagnostic Diagnostic row.
224
+ * @returns {object}
225
+ */
226
+ static #diagnosticRow(diagnostic) {
227
+ return PcbInspectionReportBuilder.#stripEmpty({
228
+ severity: PcbInspectionReportBuilder.#severity(diagnostic),
229
+ code: diagnostic?.code,
230
+ message: diagnostic?.message,
231
+ source: diagnostic?.source,
232
+ errorKind: diagnostic?.errorKind
233
+ })
234
+ }
235
+
236
+ /**
237
+ * Resolves diagnostic severity.
238
+ * @param {object} diagnostic Diagnostic row.
239
+ * @returns {string}
240
+ */
241
+ static #severity(diagnostic) {
242
+ const severity = String(diagnostic?.severity || '')
243
+ .trim()
244
+ .toLowerCase()
245
+ if (['error', 'warning', 'info'].includes(severity)) {
246
+ return severity
247
+ }
248
+
249
+ return 'info'
250
+ }
251
+
252
+ /**
253
+ * Builds the top-level inspection summary.
254
+ * @param {object} parts Report parts.
255
+ * @returns {object}
256
+ */
257
+ static #summary(parts) {
258
+ const board = parts.statistics.board || {}
259
+ const reviewItemCount =
260
+ parts.diagnosticSummary.errorCount +
261
+ parts.diagnosticSummary.warningCount +
262
+ Number(parts.netMembership.summary.undeclaredNetCount || 0) +
263
+ Number(parts.netMembership.summary.unownedPrimitiveCount || 0) +
264
+ Number(parts.classes.summary.unresolvedMemberCount || 0)
265
+
266
+ return {
267
+ fileName: parts.input.fileName || '',
268
+ status: reviewItemCount > 0 ? 'needs-review' : 'clean',
269
+ boardWidthMil: board.widthMil || 0,
270
+ boardHeightMil: board.heightMil || 0,
271
+ layerCount: Array.isArray(parts.pcb?.layers)
272
+ ? parts.pcb.layers.length
273
+ : Number(parts.statistics.layers?.count || 0),
274
+ netCount: Array.isArray(parts.pcb?.nets)
275
+ ? parts.pcb.nets.length
276
+ : Number(parts.netMembership.summary.declaredNetCount || 0),
277
+ componentCount: parts.primitives.componentCount,
278
+ primitiveCount: parts.primitives.primitiveCount,
279
+ ruleCount: parts.rules.count,
280
+ diagnosticCount: parts.diagnosticSummary.diagnosticCount,
281
+ errorCount: parts.diagnosticSummary.errorCount,
282
+ warningCount: parts.diagnosticSummary.warningCount,
283
+ possibleUnroutedNetCount:
284
+ parts.netMembership.summary.possibleUnroutedNetCount || 0,
285
+ classIssueCount: parts.classes.summary.issueCount || 0
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Sorts strings with numeric chunks in human order.
291
+ * @param {string} left Left value.
292
+ * @param {string} right Right value.
293
+ * @returns {number}
294
+ */
295
+ static #naturalCompare(left, right) {
296
+ return String(left).localeCompare(String(right), undefined, {
297
+ numeric: true
298
+ })
299
+ }
300
+
301
+ /**
302
+ * Removes undefined and blank-string values from a shallow object.
303
+ * @param {Record<string, unknown>} row Input row.
304
+ * @returns {Record<string, unknown>}
305
+ */
306
+ static #stripEmpty(row) {
307
+ return Object.fromEntries(
308
+ Object.entries(row).filter(
309
+ ([, value]) => value !== undefined && value !== ''
310
+ )
311
+ )
312
+ }
313
+ }