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
@@ -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
+ }