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.
- package/README.md +34 -5
- package/docs/api.md +171 -23
- package/docs/model-format.md +192 -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 +513 -3
- 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 +7 -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/spec/library-scope.md +5 -0
- package/src/core/BinaryReader.mjs +213 -2
- package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
- package/src/core/altium/AltiumParser.mjs +357 -16
- package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
- package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
- package/src/core/altium/AltiumUnits.mjs +205 -0
- package/src/core/altium/AsciiRecordParser.mjs +52 -11
- 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/PcbComponentKindPolicy.mjs +9 -9
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
- 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/PcbOutlineRecovery.mjs +94 -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/SchematicDirectiveParser.mjs +5 -17
- 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/SchematicNoErcSymbolResolver.mjs +36 -0
- package/src/core/altium/SchematicOwnershipGraphParser.mjs +102 -3
- package/src/core/altium/SchematicPinParser.mjs +99 -65
- package/src/core/altium/SchematicPrimitiveParser.mjs +125 -22
- 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 +107 -21
- 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/SourceBundleExporter.mjs +156 -0
- package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
- package/src/core/altium/SourceComponentClient.mjs +239 -0
- package/src/core/altium/UnsupportedFeatureReportBuilder.mjs +380 -0
- package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
- package/src/parser.mjs +43 -1
- package/src/renderers.mjs +1 -0
- package/src/styles/altium-renderers.css +6 -6
- package/src/ui/PcbArcUtils.mjs +19 -2
- package/src/ui/PcbScene3dBuilder.mjs +202 -20
- package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
- package/src/ui/SchematicColorResolver.mjs +185 -0
- package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
- package/src/ui/SchematicLineColorResolver.mjs +88 -0
- package/src/ui/SchematicNoteRenderer.mjs +5 -1
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +269 -8
- package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
- package/src/ui/SchematicPinSvgRenderer.mjs +229 -62
- package/src/ui/SchematicShapeRenderer.mjs +86 -17
- package/src/ui/SchematicSvgRenderer.mjs +980 -58
- package/src/ui/SchematicTypography.mjs +4 -3
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds deterministic inventories of embedded assets across parser roots.
|
|
7
|
+
*/
|
|
8
|
+
export class EmbeddedAssetReportBuilder {
|
|
9
|
+
static SCHEMA = 'altium-toolkit.embedded-assets.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds an embedded-asset report.
|
|
13
|
+
* @param {{ models?: object[] } | object[]} input Parser-root models.
|
|
14
|
+
* @returns {object}
|
|
15
|
+
*/
|
|
16
|
+
static build(input = {}) {
|
|
17
|
+
const models = EmbeddedAssetReportBuilder.#models(input)
|
|
18
|
+
const assets = EmbeddedAssetReportBuilder.#dedupeAssets(
|
|
19
|
+
models.flatMap((model) =>
|
|
20
|
+
EmbeddedAssetReportBuilder.#assetsForModel(model)
|
|
21
|
+
)
|
|
22
|
+
).sort(EmbeddedAssetReportBuilder.#compareAssets)
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
schema: EmbeddedAssetReportBuilder.SCHEMA,
|
|
26
|
+
summary: {
|
|
27
|
+
modelCount: models.length,
|
|
28
|
+
assetCount: assets.length,
|
|
29
|
+
totalByteCount: assets.reduce(
|
|
30
|
+
(count, asset) => count + asset.byteLength,
|
|
31
|
+
0
|
|
32
|
+
),
|
|
33
|
+
byKind: EmbeddedAssetReportBuilder.#byKind(assets)
|
|
34
|
+
},
|
|
35
|
+
assets
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Normalizes builder input to a model array.
|
|
41
|
+
* @param {{ models?: object[] } | object[]} input Builder input.
|
|
42
|
+
* @returns {object[]}
|
|
43
|
+
*/
|
|
44
|
+
static #models(input) {
|
|
45
|
+
if (Array.isArray(input)) return input
|
|
46
|
+
return Array.isArray(input?.models) ? input.models : []
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Collects embedded asset rows from one parser-root model.
|
|
51
|
+
* @param {object} model Parser-root model.
|
|
52
|
+
* @returns {object[]}
|
|
53
|
+
*/
|
|
54
|
+
static #assetsForModel(model) {
|
|
55
|
+
const base = {
|
|
56
|
+
modelFileName: String(model?.fileName || ''),
|
|
57
|
+
modelKind: String(model?.kind || '')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return [
|
|
61
|
+
...EmbeddedAssetReportBuilder.#schematicAssets(base, model),
|
|
62
|
+
...EmbeddedAssetReportBuilder.#schematicLibraryAssets(base, model),
|
|
63
|
+
...EmbeddedAssetReportBuilder.#pcbAssets(base, model),
|
|
64
|
+
...EmbeddedAssetReportBuilder.#pcbLibraryAssets(base, model),
|
|
65
|
+
...EmbeddedAssetReportBuilder.#integratedLibraryAssets(base, model)
|
|
66
|
+
].filter(Boolean)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Collects schematic-document assets.
|
|
71
|
+
* @param {object} base Shared row fields.
|
|
72
|
+
* @param {object} model Parser-root model.
|
|
73
|
+
* @returns {object[]}
|
|
74
|
+
*/
|
|
75
|
+
static #schematicAssets(base, model) {
|
|
76
|
+
const schematic = model?.schematic || {}
|
|
77
|
+
|
|
78
|
+
return [
|
|
79
|
+
...EmbeddedAssetReportBuilder.#collectionAssets(
|
|
80
|
+
base,
|
|
81
|
+
'embedded-file',
|
|
82
|
+
schematic.embeddedFiles?.files || []
|
|
83
|
+
),
|
|
84
|
+
...EmbeddedAssetReportBuilder.#collectionAssets(
|
|
85
|
+
base,
|
|
86
|
+
'schematic-image',
|
|
87
|
+
schematic.images || [],
|
|
88
|
+
{ fallbackName: 'image' }
|
|
89
|
+
),
|
|
90
|
+
...EmbeddedAssetReportBuilder.#collectionAssets(
|
|
91
|
+
base,
|
|
92
|
+
'schematic-thumbnail',
|
|
93
|
+
schematic.thumbnails || [],
|
|
94
|
+
{ fallbackName: 'thumbnail' }
|
|
95
|
+
)
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Collects schematic-library assets.
|
|
101
|
+
* @param {object} base Shared row fields.
|
|
102
|
+
* @param {object} model Parser-root model.
|
|
103
|
+
* @returns {object[]}
|
|
104
|
+
*/
|
|
105
|
+
static #schematicLibraryAssets(base, model) {
|
|
106
|
+
const schematicLibrary = model?.schematicLibrary || {}
|
|
107
|
+
const embeddedFiles = schematicLibrary.embeddedFiles?.files || []
|
|
108
|
+
const symbolAssets = (schematicLibrary.symbols || []).flatMap(
|
|
109
|
+
(symbol) =>
|
|
110
|
+
(symbol.embeddedAssets || []).map((asset) => ({
|
|
111
|
+
...asset,
|
|
112
|
+
symbolName: symbol.name
|
|
113
|
+
}))
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return EmbeddedAssetReportBuilder.#collectionAssets(
|
|
117
|
+
base,
|
|
118
|
+
'embedded-file',
|
|
119
|
+
embeddedFiles.length ? embeddedFiles : symbolAssets
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Collects PCB-document assets.
|
|
125
|
+
* @param {object} base Shared row fields.
|
|
126
|
+
* @param {object} model Parser-root model.
|
|
127
|
+
* @returns {object[]}
|
|
128
|
+
*/
|
|
129
|
+
static #pcbAssets(base, model) {
|
|
130
|
+
const pcb = model?.pcb || {}
|
|
131
|
+
|
|
132
|
+
return [
|
|
133
|
+
...EmbeddedAssetReportBuilder.#collectionAssets(
|
|
134
|
+
base,
|
|
135
|
+
'pcb-font',
|
|
136
|
+
pcb.embeddedFonts || []
|
|
137
|
+
),
|
|
138
|
+
...EmbeddedAssetReportBuilder.#collectionAssets(
|
|
139
|
+
base,
|
|
140
|
+
'pcb-model',
|
|
141
|
+
pcb.embeddedModels || []
|
|
142
|
+
)
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Collects PCB-library assets.
|
|
148
|
+
* @param {object} base Shared row fields.
|
|
149
|
+
* @param {object} model Parser-root model.
|
|
150
|
+
* @returns {object[]}
|
|
151
|
+
*/
|
|
152
|
+
static #pcbLibraryAssets(base, model) {
|
|
153
|
+
const pcbLibrary = model?.pcbLibrary || {}
|
|
154
|
+
const footprintModels = (pcbLibrary.footprints || []).flatMap(
|
|
155
|
+
(footprint) =>
|
|
156
|
+
(footprint.embeddedModels || []).map((asset) => ({
|
|
157
|
+
...asset,
|
|
158
|
+
footprintName: footprint.name
|
|
159
|
+
}))
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return [
|
|
163
|
+
...EmbeddedAssetReportBuilder.#collectionAssets(
|
|
164
|
+
base,
|
|
165
|
+
'pcb-font',
|
|
166
|
+
pcbLibrary.embeddedFonts || []
|
|
167
|
+
),
|
|
168
|
+
...EmbeddedAssetReportBuilder.#collectionAssets(
|
|
169
|
+
base,
|
|
170
|
+
'pcb-model',
|
|
171
|
+
pcbLibrary.embeddedModels || []
|
|
172
|
+
),
|
|
173
|
+
...EmbeddedAssetReportBuilder.#collectionAssets(
|
|
174
|
+
base,
|
|
175
|
+
'pcb-model',
|
|
176
|
+
footprintModels
|
|
177
|
+
)
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Collects integrated-library source assets.
|
|
183
|
+
* @param {object} base Shared row fields.
|
|
184
|
+
* @param {object} model Parser-root model.
|
|
185
|
+
* @returns {object[]}
|
|
186
|
+
*/
|
|
187
|
+
static #integratedLibraryAssets(base, model) {
|
|
188
|
+
return EmbeddedAssetReportBuilder.#collectionAssets(
|
|
189
|
+
base,
|
|
190
|
+
'integrated-library-source',
|
|
191
|
+
model?.integratedLibrary?.sources || []
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Normalizes one asset collection.
|
|
197
|
+
* @param {object} base Shared row fields.
|
|
198
|
+
* @param {string} kind Asset kind.
|
|
199
|
+
* @param {object[]} assets Source assets.
|
|
200
|
+
* @param {{ fallbackName?: string }} options Collection options.
|
|
201
|
+
* @returns {object[]}
|
|
202
|
+
*/
|
|
203
|
+
static #collectionAssets(base, kind, assets, options = {}) {
|
|
204
|
+
return (Array.isArray(assets) ? assets : []).map((asset, index) =>
|
|
205
|
+
EmbeddedAssetReportBuilder.#assetRow(base, kind, asset, index, {
|
|
206
|
+
fallbackName: options.fallbackName || kind
|
|
207
|
+
})
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Normalizes one asset row.
|
|
213
|
+
* @param {object} base Shared row fields.
|
|
214
|
+
* @param {string} kind Asset kind.
|
|
215
|
+
* @param {object} asset Source asset.
|
|
216
|
+
* @param {number} index Collection index.
|
|
217
|
+
* @param {{ fallbackName: string }} options Row options.
|
|
218
|
+
* @returns {object}
|
|
219
|
+
*/
|
|
220
|
+
static #assetRow(base, kind, asset, index, options) {
|
|
221
|
+
return EmbeddedAssetReportBuilder.#stripUndefined({
|
|
222
|
+
...base,
|
|
223
|
+
kind,
|
|
224
|
+
name: EmbeddedAssetReportBuilder.#assetName(
|
|
225
|
+
asset,
|
|
226
|
+
index,
|
|
227
|
+
options.fallbackName
|
|
228
|
+
),
|
|
229
|
+
format: EmbeddedAssetReportBuilder.#format(asset),
|
|
230
|
+
sourceStream: EmbeddedAssetReportBuilder.#sourceStream(asset),
|
|
231
|
+
byteLength: EmbeddedAssetReportBuilder.#byteLength(asset),
|
|
232
|
+
symbolName: asset?.symbolName,
|
|
233
|
+
footprintName: asset?.footprintName
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Resolves a stable asset name.
|
|
239
|
+
* @param {object} asset Source asset.
|
|
240
|
+
* @param {number} index Collection index.
|
|
241
|
+
* @param {string} fallbackName Fallback prefix.
|
|
242
|
+
* @returns {string}
|
|
243
|
+
*/
|
|
244
|
+
static #assetName(asset, index, fallbackName) {
|
|
245
|
+
return String(
|
|
246
|
+
asset?.name ||
|
|
247
|
+
asset?.family ||
|
|
248
|
+
asset?.fileName ||
|
|
249
|
+
asset?.key ||
|
|
250
|
+
asset?.id ||
|
|
251
|
+
EmbeddedAssetReportBuilder.#basename(
|
|
252
|
+
EmbeddedAssetReportBuilder.#sourceStream(asset)
|
|
253
|
+
) ||
|
|
254
|
+
fallbackName + '-' + index
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Resolves a source stream or source path.
|
|
260
|
+
* @param {object} asset Source asset.
|
|
261
|
+
* @returns {string}
|
|
262
|
+
*/
|
|
263
|
+
static #sourceStream(asset) {
|
|
264
|
+
return String(asset?.sourceStream || asset?.path || asset?.stream || '')
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Resolves asset byte length.
|
|
269
|
+
* @param {object} asset Source asset.
|
|
270
|
+
* @returns {number}
|
|
271
|
+
*/
|
|
272
|
+
static #byteLength(asset) {
|
|
273
|
+
const value =
|
|
274
|
+
asset?.byteLength ??
|
|
275
|
+
asset?.byteCount ??
|
|
276
|
+
asset?.payloadByteLength ??
|
|
277
|
+
asset?.size ??
|
|
278
|
+
0
|
|
279
|
+
const numeric = Number(value)
|
|
280
|
+
return Number.isFinite(numeric) ? numeric : 0
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Resolves a compact asset format.
|
|
285
|
+
* @param {object} asset Source asset.
|
|
286
|
+
* @returns {string}
|
|
287
|
+
*/
|
|
288
|
+
static #format(asset) {
|
|
289
|
+
const explicit =
|
|
290
|
+
asset?.format || asset?.fileType || asset?.type || asset?.mimeType
|
|
291
|
+
const normalized = String(explicit || '').trim()
|
|
292
|
+
|
|
293
|
+
if (!normalized) return 'unknown'
|
|
294
|
+
if (normalized.includes('/')) {
|
|
295
|
+
return normalized.split('/').pop()
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return normalized
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Builds a sorted count map by asset kind.
|
|
303
|
+
* @param {object[]} assets Asset rows.
|
|
304
|
+
* @returns {Record<string, number>}
|
|
305
|
+
*/
|
|
306
|
+
static #byKind(assets) {
|
|
307
|
+
const counts = new Map()
|
|
308
|
+
|
|
309
|
+
for (const asset of assets) {
|
|
310
|
+
counts.set(asset.kind, (counts.get(asset.kind) || 0) + 1)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return Object.fromEntries(
|
|
314
|
+
[...counts.entries()].sort(([left], [right]) =>
|
|
315
|
+
left.localeCompare(right)
|
|
316
|
+
)
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Dedupe exact asset references.
|
|
322
|
+
* @param {object[]} assets Asset rows.
|
|
323
|
+
* @returns {object[]}
|
|
324
|
+
*/
|
|
325
|
+
static #dedupeAssets(assets) {
|
|
326
|
+
const byKey = new Map()
|
|
327
|
+
|
|
328
|
+
for (const asset of assets) {
|
|
329
|
+
const key = [
|
|
330
|
+
asset.modelFileName,
|
|
331
|
+
asset.modelKind,
|
|
332
|
+
asset.kind,
|
|
333
|
+
asset.sourceStream,
|
|
334
|
+
asset.name
|
|
335
|
+
].join('\0')
|
|
336
|
+
if (!byKey.has(key)) {
|
|
337
|
+
byKey.set(key, asset)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return [...byKey.values()]
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Sorts report rows deterministically.
|
|
346
|
+
* @param {object} left Left row.
|
|
347
|
+
* @param {object} right Right row.
|
|
348
|
+
* @returns {number}
|
|
349
|
+
*/
|
|
350
|
+
static #compareAssets(left, right) {
|
|
351
|
+
return (
|
|
352
|
+
left.modelFileName.localeCompare(right.modelFileName) ||
|
|
353
|
+
left.kind.localeCompare(right.kind) ||
|
|
354
|
+
left.name.localeCompare(right.name) ||
|
|
355
|
+
left.sourceStream.localeCompare(right.sourceStream)
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Resolves a path basename.
|
|
361
|
+
* @param {string} value Path-like value.
|
|
362
|
+
* @returns {string}
|
|
363
|
+
*/
|
|
364
|
+
static #basename(value) {
|
|
365
|
+
return (
|
|
366
|
+
String(value || '')
|
|
367
|
+
.split('/')
|
|
368
|
+
.filter(Boolean)
|
|
369
|
+
.pop() || ''
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Removes undefined values from a row.
|
|
375
|
+
* @param {object} row Row to normalize.
|
|
376
|
+
* @returns {object}
|
|
377
|
+
*/
|
|
378
|
+
static #stripUndefined(row) {
|
|
379
|
+
return Object.fromEntries(
|
|
380
|
+
Object.entries(row).filter(([, value]) => value !== undefined)
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds fixture coverage matrices from synthetic fixture manifests.
|
|
7
|
+
*/
|
|
8
|
+
export class FixtureCoverageMatrixBuilder {
|
|
9
|
+
static SCHEMA = 'altium-toolkit.fixture-coverage-matrix.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds a fixture coverage matrix report.
|
|
13
|
+
* @param {{ manifest?: object, requiredCoverage?: string[], requiredContracts?: Record<string, string[]> } | object} [input]
|
|
14
|
+
* @returns {object}
|
|
15
|
+
*/
|
|
16
|
+
static build(input = {}) {
|
|
17
|
+
const manifest = input.manifest || input
|
|
18
|
+
const fixtures = Array.isArray(manifest.fixtures)
|
|
19
|
+
? manifest.fixtures
|
|
20
|
+
: []
|
|
21
|
+
const requiredCoverage = FixtureCoverageMatrixBuilder.#stringList(
|
|
22
|
+
input.requiredCoverage
|
|
23
|
+
)
|
|
24
|
+
const requiredContracts =
|
|
25
|
+
FixtureCoverageMatrixBuilder.#requiredContracts(
|
|
26
|
+
input.requiredContracts
|
|
27
|
+
)
|
|
28
|
+
const coverage = FixtureCoverageMatrixBuilder.#coverageRows(
|
|
29
|
+
fixtures,
|
|
30
|
+
requiredCoverage
|
|
31
|
+
)
|
|
32
|
+
const contracts = FixtureCoverageMatrixBuilder.#contractRows(
|
|
33
|
+
fixtures,
|
|
34
|
+
requiredContracts
|
|
35
|
+
)
|
|
36
|
+
const missingCoverage = coverage
|
|
37
|
+
.filter((entry) => entry.required && !entry.covered)
|
|
38
|
+
.map((entry) => entry.tag)
|
|
39
|
+
const missingContracts = contracts
|
|
40
|
+
.filter((entry) => entry.required && !entry.covered)
|
|
41
|
+
.map((entry) => ({
|
|
42
|
+
group: entry.group,
|
|
43
|
+
contract: entry.contract
|
|
44
|
+
}))
|
|
45
|
+
const policy = FixtureCoverageMatrixBuilder.#policy(manifest, fixtures)
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
schema: FixtureCoverageMatrixBuilder.SCHEMA,
|
|
49
|
+
summary: {
|
|
50
|
+
fixtureCount: fixtures.length,
|
|
51
|
+
coverageTagCount: coverage.length,
|
|
52
|
+
contractCount: contracts.length,
|
|
53
|
+
requiredCoverageCount: requiredCoverage.length,
|
|
54
|
+
requiredContractCount: requiredContracts.length,
|
|
55
|
+
missingCoverageCount: missingCoverage.length,
|
|
56
|
+
missingContractCount: missingContracts.length,
|
|
57
|
+
nativeAssetCount: policy.nativeAssetCount,
|
|
58
|
+
status:
|
|
59
|
+
missingCoverage.length || missingContracts.length
|
|
60
|
+
? 'gap'
|
|
61
|
+
: 'pass'
|
|
62
|
+
},
|
|
63
|
+
policy,
|
|
64
|
+
coverage,
|
|
65
|
+
contracts,
|
|
66
|
+
missingCoverage,
|
|
67
|
+
missingContracts
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Builds coverage rows.
|
|
73
|
+
* @param {object[]} fixtures Fixture rows.
|
|
74
|
+
* @param {string[]} requiredCoverage Required coverage tags.
|
|
75
|
+
* @returns {object[]}
|
|
76
|
+
*/
|
|
77
|
+
static #coverageRows(fixtures, requiredCoverage) {
|
|
78
|
+
const required = new Set(requiredCoverage)
|
|
79
|
+
const fixtureKeysByTag = new Map()
|
|
80
|
+
|
|
81
|
+
for (const fixture of fixtures) {
|
|
82
|
+
for (const tag of FixtureCoverageMatrixBuilder.#stringList(
|
|
83
|
+
fixture.coverage
|
|
84
|
+
)) {
|
|
85
|
+
FixtureCoverageMatrixBuilder.#appendKey(
|
|
86
|
+
fixtureKeysByTag,
|
|
87
|
+
tag,
|
|
88
|
+
FixtureCoverageMatrixBuilder.#fixtureKey(fixture)
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return FixtureCoverageMatrixBuilder.#sortedUnion(
|
|
94
|
+
[...fixtureKeysByTag.keys()],
|
|
95
|
+
requiredCoverage
|
|
96
|
+
).map((tag) =>
|
|
97
|
+
FixtureCoverageMatrixBuilder.#coverageRow(
|
|
98
|
+
tag,
|
|
99
|
+
fixtureKeysByTag.get(tag) || new Set(),
|
|
100
|
+
required.has(tag)
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Builds one coverage row.
|
|
107
|
+
* @param {string} tag Coverage tag.
|
|
108
|
+
* @param {Set<string>} fixtureKeys Fixture keys.
|
|
109
|
+
* @param {boolean} required Required flag.
|
|
110
|
+
* @returns {object}
|
|
111
|
+
*/
|
|
112
|
+
static #coverageRow(tag, fixtureKeys, required) {
|
|
113
|
+
const keys = [...fixtureKeys].sort()
|
|
114
|
+
return {
|
|
115
|
+
tag,
|
|
116
|
+
fixtureKeys: keys,
|
|
117
|
+
count: keys.length,
|
|
118
|
+
required,
|
|
119
|
+
covered: keys.length > 0
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Builds contract rows.
|
|
125
|
+
* @param {object[]} fixtures Fixture rows.
|
|
126
|
+
* @param {{ group: string, contract: string }[]} requiredContracts Required contracts.
|
|
127
|
+
* @returns {object[]}
|
|
128
|
+
*/
|
|
129
|
+
static #contractRows(fixtures, requiredContracts) {
|
|
130
|
+
const required = new Set(
|
|
131
|
+
requiredContracts.map((entry) =>
|
|
132
|
+
FixtureCoverageMatrixBuilder.#contractKey(
|
|
133
|
+
entry.group,
|
|
134
|
+
entry.contract
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
const fixtureKeysByContract = new Map()
|
|
139
|
+
|
|
140
|
+
for (const fixture of fixtures) {
|
|
141
|
+
const contracts = fixture.contracts || {}
|
|
142
|
+
for (const [group, groupContracts] of Object.entries(contracts)) {
|
|
143
|
+
for (const contract of FixtureCoverageMatrixBuilder.#stringList(
|
|
144
|
+
groupContracts
|
|
145
|
+
)) {
|
|
146
|
+
FixtureCoverageMatrixBuilder.#appendKey(
|
|
147
|
+
fixtureKeysByContract,
|
|
148
|
+
FixtureCoverageMatrixBuilder.#contractKey(
|
|
149
|
+
group,
|
|
150
|
+
contract
|
|
151
|
+
),
|
|
152
|
+
FixtureCoverageMatrixBuilder.#fixtureKey(fixture)
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const allKeys = FixtureCoverageMatrixBuilder.#sortedUnion(
|
|
159
|
+
[...fixtureKeysByContract.keys()],
|
|
160
|
+
requiredContracts.map((entry) =>
|
|
161
|
+
FixtureCoverageMatrixBuilder.#contractKey(
|
|
162
|
+
entry.group,
|
|
163
|
+
entry.contract
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return allKeys.map((key) => {
|
|
169
|
+
const [group, contract] =
|
|
170
|
+
FixtureCoverageMatrixBuilder.#splitContractKey(key)
|
|
171
|
+
const fixtureKeys = fixtureKeysByContract.get(key) || new Set()
|
|
172
|
+
return FixtureCoverageMatrixBuilder.#contractRow(
|
|
173
|
+
group,
|
|
174
|
+
contract,
|
|
175
|
+
fixtureKeys,
|
|
176
|
+
required.has(key)
|
|
177
|
+
)
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Builds one contract row.
|
|
183
|
+
* @param {string} group Contract group.
|
|
184
|
+
* @param {string} contract Contract name.
|
|
185
|
+
* @param {Set<string>} fixtureKeys Fixture keys.
|
|
186
|
+
* @param {boolean} required Required flag.
|
|
187
|
+
* @returns {object}
|
|
188
|
+
*/
|
|
189
|
+
static #contractRow(group, contract, fixtureKeys, required) {
|
|
190
|
+
const keys = [...fixtureKeys].sort()
|
|
191
|
+
return {
|
|
192
|
+
group,
|
|
193
|
+
contract,
|
|
194
|
+
fixtureKeys: keys,
|
|
195
|
+
count: keys.length,
|
|
196
|
+
required,
|
|
197
|
+
covered: keys.length > 0
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Normalizes required contract input.
|
|
203
|
+
* @param {Record<string, string[]> | undefined} requiredContracts Required contract groups.
|
|
204
|
+
* @returns {{ group: string, contract: string }[]}
|
|
205
|
+
*/
|
|
206
|
+
static #requiredContracts(requiredContracts) {
|
|
207
|
+
return Object.entries(requiredContracts || {}).flatMap(
|
|
208
|
+
([group, contracts]) =>
|
|
209
|
+
FixtureCoverageMatrixBuilder.#stringList(contracts).map(
|
|
210
|
+
(contract) => ({ group, contract })
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Builds fixture policy metadata.
|
|
217
|
+
* @param {object} manifest Fixture manifest.
|
|
218
|
+
* @param {object[]} fixtures Fixture rows.
|
|
219
|
+
* @returns {{ assetPolicy: string, nativeAssetCount: number, compliant: boolean }}
|
|
220
|
+
*/
|
|
221
|
+
static #policy(manifest, fixtures) {
|
|
222
|
+
const assetPolicy = String(manifest.assetPolicy || '')
|
|
223
|
+
const nativeAssetCount = fixtures.filter(
|
|
224
|
+
(fixture) => fixture.nativeAsset
|
|
225
|
+
).length
|
|
226
|
+
const compliant =
|
|
227
|
+
nativeAssetCount === 0 &&
|
|
228
|
+
fixtures.every(
|
|
229
|
+
(fixture) =>
|
|
230
|
+
!fixture.nativeAsset &&
|
|
231
|
+
(!assetPolicy || fixture.assetPolicy === assetPolicy)
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
assetPolicy,
|
|
236
|
+
nativeAssetCount,
|
|
237
|
+
compliant
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Appends one fixture key to a map of sets.
|
|
243
|
+
* @param {Map<string, Set<string>>} map Destination map.
|
|
244
|
+
* @param {string} tag Map key.
|
|
245
|
+
* @param {string} fixtureKey Fixture key.
|
|
246
|
+
* @returns {void}
|
|
247
|
+
*/
|
|
248
|
+
static #appendKey(map, tag, fixtureKey) {
|
|
249
|
+
if (!tag) return
|
|
250
|
+
map.set(tag, new Set([...(map.get(tag) || []), fixtureKey]))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Returns a fixture key.
|
|
255
|
+
* @param {object} fixture Fixture row.
|
|
256
|
+
* @returns {string}
|
|
257
|
+
*/
|
|
258
|
+
static #fixtureKey(fixture) {
|
|
259
|
+
return String(fixture.key || fixture.id || fixture.name || '')
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Builds a stable contract key.
|
|
264
|
+
* @param {string} group Contract group.
|
|
265
|
+
* @param {string} contract Contract name.
|
|
266
|
+
* @returns {string}
|
|
267
|
+
*/
|
|
268
|
+
static #contractKey(group, contract) {
|
|
269
|
+
return String(group || '') + '\u0000' + String(contract || '')
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Splits one stable contract key.
|
|
274
|
+
* @param {string} key Contract key.
|
|
275
|
+
* @returns {[string, string]}
|
|
276
|
+
*/
|
|
277
|
+
static #splitContractKey(key) {
|
|
278
|
+
const [group, contract] = String(key).split('\u0000')
|
|
279
|
+
return [group || '', contract || '']
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Builds a sorted unique array from two lists.
|
|
284
|
+
* @param {string[]} first First values.
|
|
285
|
+
* @param {string[]} second Second values.
|
|
286
|
+
* @returns {string[]}
|
|
287
|
+
*/
|
|
288
|
+
static #sortedUnion(first, second) {
|
|
289
|
+
return [...new Set([...first, ...second])]
|
|
290
|
+
.filter(Boolean)
|
|
291
|
+
.sort((left, right) => left.localeCompare(right))
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Normalizes string-list input.
|
|
296
|
+
* @param {unknown} value Source value.
|
|
297
|
+
* @returns {string[]}
|
|
298
|
+
*/
|
|
299
|
+
static #stringList(value) {
|
|
300
|
+
return Array.isArray(value)
|
|
301
|
+
? value.map((entry) => String(entry)).filter(Boolean)
|
|
302
|
+
: []
|
|
303
|
+
}
|
|
304
|
+
}
|