altium-toolkit 1.1.23 → 1.1.25

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/docs/api.md CHANGED
@@ -76,8 +76,10 @@ Specialized parser helpers are exported for lower-level integrations, including
76
76
  `EmbeddedAssetReportBuilder`, `LibraryDiffReportBuilder`,
77
77
  `LibraryInspectionReportBuilder`,
78
78
  `PcbBoardRegionSemanticsParser`, `PcbComponentPrimitiveIndexer`,
79
- `PcbCustomPadShapeParser`, `PcbDimensionParser`, `PcbEmbeddedFontExtractor`,
80
- `PcbExtendedPrimitiveInformationParser`, `PcbFontMetricsParser`,
79
+ `PcbCustomPadShapeParser`, `PcbDimensionParser`,
80
+ `PcbDimensionReportBuilder`, `PcbEmbeddedFontExtractor`,
81
+ `PcbExtendedPrimitiveInformationParser`,
82
+ `PcbFabricationReadinessReportBuilder`, `PcbFontMetricsParser`,
81
83
  `LibraryRenderManifestBuilder`, `LibraryCatalogArtifactBuilder`,
82
84
  `LibrarySearchIndex`, `SchLibModelParser`, `SchLibStreamExtractor`,
83
85
  `PcbBomProfileBuilder`, `PcbClassReportBuilder`,
@@ -88,8 +90,10 @@ Specialized parser helpers are exported for lower-level integrations, including
88
90
  `ProjectDesignBundleBuilder`, `ProjectHierarchyReportBuilder`,
89
91
  `ProjectNetlistExporter`, `ProjectVariantViewBuilder`,
90
92
  `PcbMechanicalLayerPairParser`, `PcbSpecialStringResolver`, `PcbUnionParser`,
91
- `PcbViaStackParser`, `PcbRuleParser`, `PcbRawRecordRegistry`,
93
+ `PcbViaStackParser`, `PcbRuleImpactReportBuilder`, `PcbRuleParser`,
94
+ `PcbRawRecordRegistry`, `PcbReviewPolygonRealizationBuilder`,
92
95
  `PcbStatisticsBuilder`, `SchematicCodeSymbolParser`,
96
+ `SchematicConnectivityQaBuilder`, `SchematicImageDiagnosticsBuilder`,
93
97
  `SchematicOwnershipGraphParser`,
94
98
  `SchematicProjectParameterResolver`, `SchematicRecordStreamParser`, and
95
99
  `SchematicTextRunParser`.
@@ -100,6 +104,15 @@ the native component-index grouping used to populate
100
104
  rule helpers expose the same mask/cache, stack, and typed-constraint
101
105
  normalization used by `.PcbDoc` parsing. `PcbDimensionParser` exposes the
102
106
  parser-only Dimensions6 normalization used by `.PcbDoc` parsing.
107
+ `PcbDimensionReportBuilder` classifies recovered PCB dimensions as renderable
108
+ or unresolved from their reference geometry, and
109
+ `PcbRuleImpactReportBuilder` summarizes enabled/disabled design rules by
110
+ affected primitive family, scope predicate, manufacturing category, and
111
+ length-valued constraints. `PcbFabricationReadinessReportBuilder` summarizes
112
+ pad/via fabrication review items such as local pad stacks, offsets, slots,
113
+ mask overrides, thermal relief, via spans, and via protection metadata.
114
+ `PcbReviewPolygonRealizationBuilder` exposes the polygon-pour realization rows
115
+ used by `pcb.reviewMetadata`.
103
116
  `SchematicOwnershipGraphParser` and `PcbOwnershipGraphBuilder` expose the
104
117
  read-only ownership sidecars that parser roots attach under
105
118
  `schematic.ownership` and `pcb.ownership`.
@@ -190,10 +203,16 @@ implementation child rows where available.
190
203
  `LibrarySearchIndex` expose deterministic SchLib/PcbLib render/export
191
204
  manifests, static catalog artifacts, search metadata, plus exact, keyword, and
192
205
  fuzzy lookup helpers. `PcbStatisticsBuilder`, `PcbNetMembershipReportBuilder`,
193
- `PcbClassReportBuilder`, and `PcbInspectionReportBuilder` emit board QA,
194
- net-ownership, class-membership, and combined inspection artifacts for
195
- `.PcbDoc` models. `SchematicProjectParameterResolver` resolves dot-prefixed and
196
- equals-prefixed schematic special strings for parser and SVG integrations.
206
+ `PcbClassReportBuilder`, `PcbDimensionReportBuilder`,
207
+ `PcbRuleImpactReportBuilder`, `PcbFabricationReadinessReportBuilder`, and
208
+ `PcbInspectionReportBuilder` emit board QA, net-ownership, class-membership,
209
+ dimension QA, rule-impact, fabrication-readiness, and combined inspection
210
+ artifacts for `.PcbDoc` models. `SchematicImageDiagnosticsBuilder` and
211
+ `SchematicConnectivityQaBuilder` expose the same image payload and connectivity
212
+ QA sidecars attached to parsed schematic models.
213
+ `SchematicProjectParameterResolver`
214
+ resolves dot-prefixed and equals-prefixed schematic special strings for parser
215
+ and SVG integrations.
197
216
  `SchematicTextRunParser` parses schematic backslash suffix markers into display
198
217
  text plus overline run metadata reused by pin and text rendering.
199
218
 
@@ -303,13 +322,14 @@ available. The embedded `schematic-semantic-metadata` JSON sidecar uses schema
303
322
  components, and pins for downstream highlighting.
304
323
 
305
324
  PCB SVG output includes stable semantic `data-*` attributes on recovered board,
306
- copper, pad, via, component, and text elements. The embedded
325
+ copper, pad, via, component, text, and dimension elements. The embedded
307
326
  `pcb-semantic-metadata` JSON sidecar uses schema
308
327
  `altium-toolkit.pcb.svg.semantics.a1` and links SVG element keys to primitive
309
- kind, layer, net/class, component, pad number, hole ownership, and board-outline
310
- identity where that metadata is available. The same sidecar also records view
311
- context, including board centroid, included layer ids, layer roles, cutouts, and
312
- pad/via drill render state (`open`, `covered`, `filled`, or `capped`).
328
+ kind, layer, net/class, component, pad number, dimension kind/text, hole
329
+ ownership, and board-outline identity where that metadata is available. The
330
+ same sidecar also records view context, including board centroid, included layer
331
+ ids, layer roles, cutouts, and pad/via drill render state (`open`, `covered`,
332
+ `filled`, or `capped`).
313
333
  `PcbSvgRenderer.renderLayerSvgs()` uses the same semantic sidecar shape with
314
334
  `view.kind: 'layer'` and a layer-specific `layerSet`.
315
335
 
@@ -331,7 +351,8 @@ import {
331
351
  `drillCutouts` plus fill holes for drilled pads and vias. External model
332
352
  placements include `projection` diagnostics indicating whether bounds came
333
353
  from authored overrides, resolved model bounds, nearby pad spans, procedural
334
- component fallback, or only the model anchor.
354
+ component fallback, or only the model anchor. Shape-based 3D bodies with
355
+ complete native geometry are exposed as `staticBodyPlacements`.
335
356
  - `PcbScene3dModelRegistry` resolves embedded or session model candidates for
336
357
  component placements.
337
358
  - `PcbScene3dScenePreparator.prepare(documentModel, options)` prepares the same
@@ -170,10 +170,23 @@ possible unrouted pad-only nets are split into deterministic lists.
170
170
  `PcbClassReportBuilder.build()` emits
171
171
  `altium-toolkit.pcb.class-report.a1` reports that summarize PCB classes by
172
172
  kind, enabled state, member resolution, empty classes, and unresolved members.
173
+ `PcbDimensionReportBuilder.build()` emits
174
+ `altium-toolkit.pcb.dimensions.a1` reports that classify recovered PCB
175
+ dimensions as renderable or unresolved from reference geometry and summarize
176
+ dimension kinds. `PcbRuleImpactReportBuilder.build()` emits
177
+ `altium-toolkit.pcb.rule-impact.a1` reports that group design rules by enabled
178
+ state, affected primitive families, scope predicates, manufacturing category,
179
+ and length-valued constraints.
180
+ `PcbFabricationReadinessReportBuilder.build()` emits
181
+ `altium-toolkit.pcb.fabrication-readiness.a1` reports that summarize pad/via
182
+ fabrication review items such as non-simple pad stack modes, local pad
183
+ offsets, slotted and non-plated holes, mask overrides, thermal relief, via
184
+ spans, via protection, and microvia-like geometry.
173
185
  `PcbInspectionReportBuilder.build()` emits `altium-toolkit.pcb.inspection.a1`
174
186
  reports that compose board statistics, primitive counts, design-rule counts,
175
- diagnostics, net membership, class membership, and route-analysis summaries
176
- into one inspection artifact.
187
+ diagnostics, net membership, class membership, dimension QA, rule-impact,
188
+ review metadata, fabrication-readiness, and route-analysis summaries into one
189
+ inspection artifact.
177
190
 
178
191
  `PcbLayerGroups` provides stable layer-group names, deterministic display
179
192
  colors, and draw priorities for legacy PCB layer ids. `AltiumUnits` provides
@@ -254,7 +267,12 @@ multi-sheet design netlist.
254
267
  `schematic.connectivityQa` reports read-only connectivity findings, including
255
268
  implicit generated net names, dangling labels, orphan ports, unconnected pins,
256
269
  ambiguous junctions, and un-junctioned tee contacts where one wire endpoint
257
- touches another wire interior without an authored junction.
270
+ touches another wire interior without an authored junction. Harness-specific
271
+ findings flag sheet entries whose local harness type cannot be resolved,
272
+ harness entries without linked signal-harness geometry, and connector type-label
273
+ mismatches. Pin interpretation findings flag hidden name/number labels,
274
+ endpoint symbol markers, and power-like pin names whose recovered electrical
275
+ type is not a power pin.
258
276
 
259
277
  Embedded schematic images preserve the raw record geometry and expose
260
278
  browser-facing payload metadata. When an embedded stream contains a native
@@ -262,6 +280,10 @@ PNG/JPEG/GIF/SVG/WebP payload alongside a preview, `mimeType` and `dataBase64`
262
280
  refer to the native payload while `sourceMimeType` records the preview format.
263
281
  Alpha-bearing 32-bit BMP previews are converted to PNG and marked with
264
282
  `hasAlpha` so SVG renderers can display transparency deterministically.
283
+ `schematic.imageDiagnostics` reports embedded payload counts, external image
284
+ references, missing embedded payloads, unsupported MIME states, converted
285
+ preview/native payloads, and alpha-bearing images without attempting any file
286
+ system lookups.
265
287
  When a schematic OLE container exposes preview metadata, `schematic.thumbnails`
266
288
  contains PNG thumbnail sidecars with `kind`, dimensions, `sourceStream`,
267
289
  `pixelFormat`, `mimeType`, and `dataBase64` fields.
@@ -378,9 +400,11 @@ consumers.
378
400
 
379
401
  PCB dimensions from `Dimensions6/Data` are exposed through `pcb.dimensions`.
380
402
  Dimension entries preserve native kind codes and raw fields while adding a
381
- normalized `kind` (`linear`, `angular`, `radial`, `datum`, `baseline`, or
382
- `ordinate`), reference points, optional text location, prefix/suffix, precision,
383
- measured value, angle value, and unit.
403
+ normalized `kind` (`linear`, `angular`, `radial`, `diameter`, `datum`,
404
+ `baseline`, or `ordinate`), reference points, optional text location,
405
+ prefix/suffix, precision, measured value, angle value, and unit. PCB SVG output
406
+ renders dimensions with sufficient reference geometry as static mechanical
407
+ dimension primitives and includes them in the semantic sidecar.
384
408
 
385
409
  `pcb.extendedPrimitiveInformation` exposes
386
410
  `ExtendedPrimitiveInformation/Data` entries keyed by primitive index and, when
@@ -454,7 +478,9 @@ preserves raw mixed-format primitive records with the same registry metadata
454
478
  shape used by PcbDoc raw records. Library-level `embeddedFonts` uses the same
455
479
  payload and metric shape as PCB documents. Library-level `embeddedModels` and
456
480
  `componentBodies` preserve embedded 3D payloads and body references when
457
- present.
481
+ present. Shape-based component bodies may include `staticGeometry` for
482
+ extruded-polygon, cone, cylinder, and sphere bodies, with dimensions and
483
+ vertices expressed in mils when native evidence is available.
458
484
  `pcbLibrary.indexes.footprintsByName` provides read-only footprint lookup and
459
485
  search metadata, including source storage, primitive counts, pad/text counts,
460
486
  and keyword tokens from footprint and component parameters. Footprint entries
@@ -540,7 +566,10 @@ External model placements in the 3D scene description include a `projection`
540
566
  diagnostic object. The `source` explains whether bounds came from an authored
541
567
  projection override, resolved model bounds, nearby pad-span fallback,
542
568
  procedural component fallback, or model-anchor fallback. The diagnostic does
543
- not alter placement coordinates.
569
+ not alter placement coordinates. Shape-based component bodies with complete
570
+ static geometry are exposed separately as `staticBodyPlacements`, using the
571
+ same board-centered coordinate convention as component and external-model
572
+ placements.
544
573
 
545
574
  ## Integrated Library Fields
546
575
 
@@ -10,7 +10,8 @@
10
10
  "recordTypes",
11
11
  "rawRecords",
12
12
  "opaqueRecords",
13
- "diagnostics"
13
+ "diagnostics",
14
+ "edgeCases"
14
15
  ],
15
16
  "properties": {
16
17
  "schema": {
@@ -25,6 +26,7 @@
25
26
  "rawRecordCount",
26
27
  "opaqueRecordCount",
27
28
  "diagnosticCount",
29
+ "edgeCaseCount",
28
30
  "itemCount",
29
31
  "status"
30
32
  ],
@@ -44,6 +46,9 @@
44
46
  "diagnosticCount": {
45
47
  "type": "number"
46
48
  },
49
+ "edgeCaseCount": {
50
+ "type": "number"
51
+ },
47
52
  "itemCount": {
48
53
  "type": "number"
49
54
  },
@@ -75,6 +80,12 @@
75
80
  "items": {
76
81
  "$ref": "#/$defs/diagnostic"
77
82
  }
83
+ },
84
+ "edgeCases": {
85
+ "type": "array",
86
+ "items": {
87
+ "$ref": "#/$defs/edgeCase"
88
+ }
78
89
  }
79
90
  },
80
91
  "$defs": {
@@ -207,6 +218,52 @@
207
218
  "type": "string"
208
219
  }
209
220
  }
221
+ },
222
+ "edgeCase": {
223
+ "type": "object",
224
+ "additionalProperties": true,
225
+ "required": ["code"],
226
+ "properties": {
227
+ "fileName": {
228
+ "type": "string"
229
+ },
230
+ "domain": {
231
+ "type": "string"
232
+ },
233
+ "code": {
234
+ "type": "string"
235
+ },
236
+ "feature": {
237
+ "type": "string"
238
+ },
239
+ "supportState": {
240
+ "type": "string"
241
+ },
242
+ "severity": {
243
+ "enum": ["info", "warning", "error"]
244
+ },
245
+ "message": {
246
+ "type": "string"
247
+ },
248
+ "source": {
249
+ "type": "string"
250
+ },
251
+ "sourceStream": {
252
+ "type": "string"
253
+ },
254
+ "sourceStorage": {
255
+ "type": "string"
256
+ },
257
+ "recordIndex": {
258
+ "type": "number"
259
+ },
260
+ "recordType": {
261
+ "type": "number"
262
+ },
263
+ "errorKind": {
264
+ "type": "string"
265
+ }
266
+ }
210
267
  }
211
268
  }
212
269
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "altium-toolkit",
3
- "version": "1.1.23",
3
+ "version": "1.1.25",
4
4
  "description": "Altium document parsing and non-interactive rendering utilities",
5
5
  "keywords": [
6
6
  "altium",
@@ -32,6 +32,7 @@ import { SchematicSheetParser } from './SchematicSheetParser.mjs'
32
32
  import { SchematicJunctionParser } from './SchematicJunctionParser.mjs'
33
33
  import { SchematicBusEntryParser } from './SchematicBusEntryParser.mjs'
34
34
  import { SchematicImageParser } from './SchematicImageParser.mjs'
35
+ import { SchematicImageDiagnosticsBuilder } from './SchematicImageDiagnosticsBuilder.mjs'
35
36
  import { SchematicNetlistBuilder } from './SchematicNetlistBuilder.mjs'
36
37
  import { SchematicRecordTypeRegistry } from './SchematicRecordTypeRegistry.mjs'
37
38
  import { SchematicComponentTextResolver } from './SchematicComponentTextResolver.mjs'
@@ -617,6 +618,9 @@ export class AltiumParser {
617
618
  recordIndexAwareRecords,
618
619
  arrayBuffer
619
620
  )
621
+ const imageDiagnosticsReport = SchematicImageDiagnosticsBuilder.build({
622
+ images
623
+ })
620
624
  const { thumbnails, diagnostics: thumbnailDiagnostics } =
621
625
  SchematicThumbnailParser.parse(arrayBuffer)
622
626
  const template = SchematicTemplateParser.parse(
@@ -832,7 +836,9 @@ export class AltiumParser {
832
836
  texts: anchoredTexts,
833
837
  pins,
834
838
  ports,
835
- junctions
839
+ junctions,
840
+ sheetEntries,
841
+ harnesses
836
842
  })
837
843
  const embeddedFiles = schematicExtraction?.embeddedFiles || null
838
844
  const nativeStreams = schematicExtraction?.nativeStreams || null
@@ -905,6 +911,7 @@ export class AltiumParser {
905
911
  junctions,
906
912
  busEntries,
907
913
  images,
914
+ imageDiagnostics: imageDiagnosticsReport,
908
915
  ...(thumbnails.length ? { thumbnails } : {}),
909
916
  nets,
910
917
  ownership,
@@ -173,6 +173,7 @@ export class PcbDimensionParser {
173
173
  static #normalizeKind(kindCode) {
174
174
  const normalized = String(kindCode || '').toLowerCase()
175
175
  if (/ang/u.test(normalized)) return 'angular'
176
+ if (/diam/u.test(normalized)) return 'diameter'
176
177
  if (/radial|radius/u.test(normalized)) return 'radial'
177
178
  if (/datum/u.test(normalized)) return 'datum'
178
179
  if (/baseline/u.test(normalized)) return 'baseline'
@@ -0,0 +1,190 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Builds deterministic QA summaries for recovered PCB dimension records.
7
+ */
8
+ export class PcbDimensionReportBuilder {
9
+ static SCHEMA = 'altium-toolkit.pcb.dimensions.a1'
10
+
11
+ /**
12
+ * Builds a dimension QA report from a parser root or PCB model.
13
+ * @param {object} input Parser root, PCB model, or options object.
14
+ * @returns {object}
15
+ */
16
+ static build(input = {}) {
17
+ const pcb = PcbDimensionReportBuilder.#pcb(input)
18
+ const dimensions = PcbDimensionReportBuilder.#dimensions(pcb)
19
+ const rows = dimensions.map((dimension) =>
20
+ PcbDimensionReportBuilder.#dimensionRow(dimension)
21
+ )
22
+ const findings = rows.flatMap((row) =>
23
+ PcbDimensionReportBuilder.#findingsForRow(row)
24
+ )
25
+
26
+ return {
27
+ schema: PcbDimensionReportBuilder.SCHEMA,
28
+ summary: PcbDimensionReportBuilder.#summary(rows),
29
+ dimensions: rows,
30
+ findings
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Resolves the PCB payload from a parser root or direct PCB model.
36
+ * @param {object} input Parser root or PCB model.
37
+ * @returns {object}
38
+ */
39
+ static #pcb(input) {
40
+ return input?.pcb || input || {}
41
+ }
42
+
43
+ /**
44
+ * Resolves normalized dimension rows from a PCB payload.
45
+ * @param {object} pcb PCB model.
46
+ * @returns {object[]}
47
+ */
48
+ static #dimensions(pcb) {
49
+ return Array.isArray(pcb?.dimensions) ? pcb.dimensions : []
50
+ }
51
+
52
+ /**
53
+ * Builds one public dimension report row.
54
+ * @param {object} dimension Normalized dimension record.
55
+ * @returns {object}
56
+ */
57
+ static #dimensionRow(dimension) {
58
+ const kind = PcbDimensionReportBuilder.#kind(dimension)
59
+ const references = Array.isArray(dimension?.references)
60
+ ? dimension.references
61
+ : []
62
+ const requiredReferenceCount =
63
+ PcbDimensionReportBuilder.#requiredReferenceCount(kind)
64
+ const status =
65
+ references.length >= requiredReferenceCount
66
+ ? 'renderable'
67
+ : 'unresolved'
68
+
69
+ return PcbDimensionReportBuilder.#stripEmpty({
70
+ dimensionIndex: PcbDimensionReportBuilder.#integerOrFallback(
71
+ dimension?.dimensionIndex,
72
+ 0
73
+ ),
74
+ kind,
75
+ name: dimension?.name || '',
76
+ layer: dimension?.layer || '',
77
+ text: dimension?.text || '',
78
+ status,
79
+ referenceCount: references.length,
80
+ requiredReferenceCount,
81
+ hasTextLocation: Boolean(dimension?.textLocation),
82
+ measuredValue: dimension?.measuredValue ?? undefined,
83
+ angleValue: dimension?.angleValue ?? undefined,
84
+ unit: dimension?.unit || ''
85
+ })
86
+ }
87
+
88
+ /**
89
+ * Builds report findings for one dimension row.
90
+ * @param {object} row Dimension report row.
91
+ * @returns {object[]}
92
+ */
93
+ static #findingsForRow(row) {
94
+ if (row.status !== 'unresolved') {
95
+ return []
96
+ }
97
+
98
+ return [
99
+ PcbDimensionReportBuilder.#stripEmpty({
100
+ code: 'pcb.dimension.missing-reference',
101
+ severity: 'warning',
102
+ dimensionIndex: row.dimensionIndex,
103
+ kind: row.kind,
104
+ name: row.name,
105
+ referenceCount: row.referenceCount,
106
+ requiredReferenceCount: row.requiredReferenceCount
107
+ })
108
+ ]
109
+ }
110
+
111
+ /**
112
+ * Builds top-level dimension counters.
113
+ * @param {object[]} rows Dimension report rows.
114
+ * @returns {object}
115
+ */
116
+ static #summary(rows) {
117
+ const byKind = new Map()
118
+
119
+ for (const row of rows) {
120
+ byKind.set(row.kind, Number(byKind.get(row.kind) || 0) + 1)
121
+ }
122
+
123
+ return {
124
+ dimensionCount: rows.length,
125
+ renderableCount: rows.filter((row) => row.status === 'renderable')
126
+ .length,
127
+ unresolvedCount: rows.filter((row) => row.status === 'unresolved')
128
+ .length,
129
+ missingTextLocationCount: rows.filter(
130
+ (row) => row.hasTextLocation !== true
131
+ ).length,
132
+ byKind: [...byKind.entries()]
133
+ .map(([kind, count]) => ({ kind, count }))
134
+ .sort((left, right) =>
135
+ left.kind.localeCompare(right.kind, undefined, {
136
+ numeric: true
137
+ })
138
+ )
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Resolves the canonical dimension kind.
144
+ * @param {object} dimension Dimension record.
145
+ * @returns {string}
146
+ */
147
+ static #kind(dimension) {
148
+ const kind = String(dimension?.kind || '')
149
+ .trim()
150
+ .toLowerCase()
151
+ return kind || 'linear'
152
+ }
153
+
154
+ /**
155
+ * Returns the minimum reference count needed for deterministic rendering.
156
+ * @param {string} kind Dimension kind.
157
+ * @returns {number}
158
+ */
159
+ static #requiredReferenceCount(kind) {
160
+ if (kind === 'angular') {
161
+ return 3
162
+ }
163
+
164
+ return 2
165
+ }
166
+
167
+ /**
168
+ * Parses an integer with a fallback.
169
+ * @param {unknown} value Numeric candidate.
170
+ * @param {number} fallback Fallback value.
171
+ * @returns {number}
172
+ */
173
+ static #integerOrFallback(value, fallback) {
174
+ const number = Number(value)
175
+ return Number.isInteger(number) ? number : fallback
176
+ }
177
+
178
+ /**
179
+ * Removes undefined and blank-string values from a shallow object.
180
+ * @param {Record<string, unknown>} row Input row.
181
+ * @returns {Record<string, unknown>}
182
+ */
183
+ static #stripEmpty(row) {
184
+ return Object.fromEntries(
185
+ Object.entries(row).filter(
186
+ ([, value]) => value !== undefined && value !== ''
187
+ )
188
+ )
189
+ }
190
+ }