altium-toolkit 1.0.7 → 1.0.9

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 (93) hide show
  1. package/README.md +18 -6
  2. package/docs/api.md +78 -16
  3. package/docs/model-format.md +229 -8
  4. package/docs/schemas/altium_toolkit/netlist_a1.schema.json +47 -0
  5. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1661 -104
  6. package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +59 -0
  7. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +57 -0
  8. package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
  9. package/docs/testing.md +9 -3
  10. package/package.json +1 -1
  11. package/spec/library-scope.md +7 -1
  12. package/src/core/altium/AltiumLayoutParser.mjs +104 -8
  13. package/src/core/altium/AltiumParser.mjs +191 -45
  14. package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
  15. package/src/core/altium/IntLibModelParser.mjs +240 -0
  16. package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
  17. package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
  18. package/src/core/altium/LibrarySearchIndex.mjs +215 -0
  19. package/src/core/altium/NormalizedModelSchema.mjs +36 -0
  20. package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
  21. package/src/core/altium/PcbDefaultsParser.mjs +171 -0
  22. package/src/core/altium/PcbDimensionParser.mjs +229 -0
  23. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
  24. package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
  25. package/src/core/altium/PcbLibModelParser.mjs +235 -14
  26. package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
  27. package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
  28. package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
  29. package/src/core/altium/PcbModelParser.mjs +466 -28
  30. package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
  31. package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
  32. package/src/core/altium/PcbPadStackParser.mjs +58 -0
  33. package/src/core/altium/PcbPickPlacePositionResolver.mjs +217 -0
  34. package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
  35. package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
  36. package/src/core/altium/PcbRegionPrimitiveParser.mjs +5 -1
  37. package/src/core/altium/PcbRuleParser.mjs +354 -33
  38. package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
  39. package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
  40. package/src/core/altium/PcbStatisticsBuilder.mjs +532 -0
  41. package/src/core/altium/PcbStreamExtractor.mjs +111 -4
  42. package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
  43. package/src/core/altium/PcbUnionParser.mjs +307 -0
  44. package/src/core/altium/PcbViaStackParser.mjs +98 -10
  45. package/src/core/altium/PcbViaStructureParser.mjs +335 -0
  46. package/src/core/altium/PrintableTextDecoder.mjs +53 -3
  47. package/src/core/altium/PrjPcbModelParser.mjs +257 -5
  48. package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
  49. package/src/core/altium/ProjectDesignBundleBuilder.mjs +477 -0
  50. package/src/core/altium/ProjectNetlistExporter.mjs +499 -0
  51. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
  52. package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
  53. package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
  54. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
  55. package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
  56. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
  57. package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
  58. package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
  59. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
  60. package/src/core/altium/SchematicHarnessParser.mjs +302 -0
  61. package/src/core/altium/SchematicImageParser.mjs +474 -3
  62. package/src/core/altium/SchematicImplementationParser.mjs +518 -0
  63. package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
  64. package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
  65. package/src/core/altium/SchematicPinParser.mjs +84 -1
  66. package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
  67. package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
  68. package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
  69. package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
  70. package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
  71. package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
  72. package/src/core/altium/SchematicTemplateParser.mjs +256 -0
  73. package/src/core/altium/SchematicTextParser.mjs +123 -0
  74. package/src/core/ole/OleCompoundDocument.mjs +20 -0
  75. package/src/parser.mjs +29 -0
  76. package/src/renderers.mjs +3 -0
  77. package/src/styles/altium-renderers.css +25 -0
  78. package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
  79. package/src/ui/PcbInteractionGeometry.mjs +350 -0
  80. package/src/ui/PcbInteractionIndex.mjs +593 -0
  81. package/src/ui/PcbInteractionItemRegistry.mjs +66 -0
  82. package/src/ui/PcbInteractionLayerModel.mjs +99 -0
  83. package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +74 -9
  84. package/src/ui/PcbScene3dBuilder.mjs +169 -7
  85. package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
  86. package/src/ui/PcbSvgRenderer.mjs +1187 -34
  87. package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
  88. package/src/ui/SchematicNoteRenderer.mjs +9 -2
  89. package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
  90. package/src/ui/SchematicShapeRenderer.mjs +362 -0
  91. package/src/ui/SchematicSvgRenderer.mjs +1442 -92
  92. package/src/ui/SchematicTypography.mjs +48 -5
  93. package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
@@ -0,0 +1,334 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Builds consumer-facing effective views for project variants.
7
+ */
8
+ export class ProjectVariantViewBuilder {
9
+ /**
10
+ * Applies one project variant to bundle-level BOM, PnP, component, and net
11
+ * collections.
12
+ * @param {object} bundle Project design bundle.
13
+ * @param {{ variantName?: string }} options Variant selection options.
14
+ * @returns {object}
15
+ */
16
+ static build(bundle, options = {}) {
17
+ const variant = ProjectVariantViewBuilder.#findVariant(
18
+ bundle,
19
+ options.variantName
20
+ )
21
+ const dnp = new Set(variant?.dnp || [])
22
+ const parameterOverrides = variant?.parameterOverrides || {}
23
+ const alternateFitted = variant?.alternateFitted || {}
24
+ const annotations = bundle?.annotations?.bySourceDesignator || {}
25
+ const includeVariantDetails =
26
+ ProjectVariantViewBuilder.#hasKeys(alternateFitted) ||
27
+ ProjectVariantViewBuilder.#hasKeys(annotations)
28
+
29
+ return {
30
+ name: variant?.description || options.variantName || '',
31
+ uniqueId: variant?.uniqueId || '',
32
+ dnp: [...dnp],
33
+ parameterOverrides,
34
+ ...(includeVariantDetails ? { alternateFitted } : {}),
35
+ bom: ProjectVariantViewBuilder.#applyBomVariant(
36
+ bundle?.bom || [],
37
+ dnp,
38
+ parameterOverrides,
39
+ alternateFitted,
40
+ annotations,
41
+ includeVariantDetails
42
+ ),
43
+ pnp: {
44
+ ...(bundle?.pnp || {}),
45
+ entries: (bundle?.pnp?.entries || [])
46
+ .filter((entry) => !dnp.has(entry.designator))
47
+ .map((entry) =>
48
+ ProjectVariantViewBuilder.#applyDesignatorAnnotation(
49
+ entry,
50
+ annotations
51
+ )
52
+ )
53
+ },
54
+ nets: ProjectVariantViewBuilder.#applyNetVariant(
55
+ bundle?.nets || [],
56
+ dnp,
57
+ annotations
58
+ ),
59
+ components: (bundle?.components || []).map((component) =>
60
+ ProjectVariantViewBuilder.#applyComponentVariant(
61
+ component,
62
+ dnp,
63
+ parameterOverrides,
64
+ alternateFitted,
65
+ annotations,
66
+ includeVariantDetails
67
+ )
68
+ )
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Finds the requested variant or the current project variant.
74
+ * @param {object} bundle Project design bundle.
75
+ * @param {string | undefined} variantName Requested variant name.
76
+ * @returns {object | null}
77
+ */
78
+ static #findVariant(bundle, variantName) {
79
+ const variants = bundle?.variants || bundle?.project?.variants || []
80
+ const requested =
81
+ variantName || bundle?.project?.design?.CurrentVariant || ''
82
+ if (!requested) {
83
+ return variants.find((variant) => variant.isCurrent) || null
84
+ }
85
+
86
+ const lookup = requested.toLowerCase()
87
+ return (
88
+ variants.find(
89
+ (variant) =>
90
+ String(variant.description || '').toLowerCase() ===
91
+ lookup ||
92
+ String(variant.uniqueId || '').toLowerCase() === lookup
93
+ ) ||
94
+ variants.find((variant) => variant.isCurrent) ||
95
+ null
96
+ )
97
+ }
98
+
99
+ /**
100
+ * Applies DNP and parameter overrides to BOM rows.
101
+ * @param {object[]} bom BOM rows.
102
+ * @param {Set<string>} dnp DNP designators.
103
+ * @param {Record<string, Record<string, string>>} parameterOverrides Overrides by designator.
104
+ * @param {Record<string, object>} alternateFitted Alternate fitted rows by designator.
105
+ * @param {Record<string, object>} annotations Annotation rows by source designator.
106
+ * @param {boolean} includeVariantDetails Whether to expose alternate metadata.
107
+ * @returns {object[]}
108
+ */
109
+ static #applyBomVariant(
110
+ bom,
111
+ dnp,
112
+ parameterOverrides,
113
+ alternateFitted,
114
+ annotations,
115
+ includeVariantDetails
116
+ ) {
117
+ return (bom || [])
118
+ .flatMap((row) =>
119
+ (row.designators || []).map((designator) =>
120
+ ProjectVariantViewBuilder.#applyBomDesignatorVariant(
121
+ row,
122
+ designator,
123
+ parameterOverrides,
124
+ alternateFitted,
125
+ annotations,
126
+ includeVariantDetails
127
+ )
128
+ )
129
+ )
130
+ .filter(
131
+ (row) => !dnp.has(row.sourceDesignator || row.designators[0])
132
+ )
133
+ }
134
+
135
+ /**
136
+ * Applies one designator's parameter overrides to a BOM row.
137
+ * @param {object} row Source BOM row.
138
+ * @param {string} designator Designator.
139
+ * @param {Record<string, Record<string, string>>} parameterOverrides Overrides by designator.
140
+ * @param {Record<string, object>} alternateFitted Alternate fitted rows by designator.
141
+ * @param {Record<string, object>} annotations Annotation rows by source designator.
142
+ * @param {boolean} includeVariantDetails Whether to expose alternate metadata.
143
+ * @returns {object}
144
+ */
145
+ static #applyBomDesignatorVariant(
146
+ row,
147
+ designator,
148
+ parameterOverrides,
149
+ alternateFitted,
150
+ annotations,
151
+ includeVariantDetails
152
+ ) {
153
+ const parameters = parameterOverrides[designator] || {}
154
+ const alternate = alternateFitted[designator] || null
155
+ const value =
156
+ parameters.Comment ||
157
+ parameters.Value ||
158
+ alternate?.comment ||
159
+ alternate?.description ||
160
+ row.value
161
+
162
+ return {
163
+ ...row,
164
+ designators: [
165
+ ProjectVariantViewBuilder.#compiledDesignator(
166
+ designator,
167
+ annotations
168
+ )
169
+ ],
170
+ ...(includeVariantDetails ? { sourceDesignator: designator } : {}),
171
+ quantity: 1,
172
+ value,
173
+ source: alternate?.libReference || row.source,
174
+ pattern: alternate?.footprint || row.pattern,
175
+ parameters,
176
+ ...(includeVariantDetails
177
+ ? { alternateFitted: alternate || null }
178
+ : {})
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Applies DNP filtering metadata to normalized nets.
184
+ * @param {object[]} nets Bundle nets.
185
+ * @param {Set<string>} dnp DNP designators.
186
+ * @param {Record<string, object>} annotations Annotation rows by source designator.
187
+ * @returns {object[]}
188
+ */
189
+ static #applyNetVariant(nets, dnp, annotations) {
190
+ return (nets || []).map((net) => {
191
+ const excludedDesignators = []
192
+ const pins = (net.pins || [])
193
+ .filter((pin) => {
194
+ const designator =
195
+ pin.componentDesignator || pin.ownerIndex || ''
196
+ if (dnp.has(designator)) {
197
+ excludedDesignators.push(designator)
198
+ return false
199
+ }
200
+ return true
201
+ })
202
+ .map((pin) =>
203
+ ProjectVariantViewBuilder.#applyPinAnnotation(
204
+ pin,
205
+ annotations
206
+ )
207
+ )
208
+
209
+ return {
210
+ ...net,
211
+ pins,
212
+ excludedDesignators:
213
+ ProjectVariantViewBuilder.#dedupe(excludedDesignators)
214
+ }
215
+ })
216
+ }
217
+
218
+ /**
219
+ * Applies DNP and parameter metadata to one component entry.
220
+ * @param {object} component Bundle component.
221
+ * @param {Set<string>} dnp DNP designators.
222
+ * @param {Record<string, Record<string, string>>} parameterOverrides Overrides by designator.
223
+ * @param {Record<string, object>} alternateFitted Alternate fitted rows by designator.
224
+ * @param {Record<string, object>} annotations Annotation rows by source designator.
225
+ * @param {boolean} includeVariantDetails Whether to expose alternate metadata.
226
+ * @returns {object}
227
+ */
228
+ static #applyComponentVariant(
229
+ component,
230
+ dnp,
231
+ parameterOverrides,
232
+ alternateFitted,
233
+ annotations,
234
+ includeVariantDetails
235
+ ) {
236
+ const alternate = alternateFitted[component.designator] || null
237
+ return {
238
+ designator: ProjectVariantViewBuilder.#compiledDesignator(
239
+ component.designator,
240
+ annotations
241
+ ),
242
+ ...(includeVariantDetails
243
+ ? { sourceDesignator: component.designator }
244
+ : {}),
245
+ schematic: component.schematic,
246
+ pcb: component.pcb,
247
+ dnp: dnp.has(component.designator),
248
+ parameters: parameterOverrides[component.designator] || {},
249
+ ...(includeVariantDetails
250
+ ? { alternateFitted: alternate || null }
251
+ : {})
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Applies an annotation row to one designator-bearing object.
257
+ * @param {object} entry Source entry.
258
+ * @param {Record<string, object>} annotations Annotation rows by source designator.
259
+ * @returns {object}
260
+ */
261
+ static #applyDesignatorAnnotation(entry, annotations) {
262
+ const compiledDesignator =
263
+ ProjectVariantViewBuilder.#compiledDesignator(
264
+ entry.designator,
265
+ annotations
266
+ )
267
+ return compiledDesignator === entry.designator
268
+ ? entry
269
+ : {
270
+ ...entry,
271
+ sourceDesignator: entry.designator,
272
+ designator: compiledDesignator
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Applies annotation mapping to one net pin.
278
+ * @param {object} pin Net pin.
279
+ * @param {Record<string, object>} annotations Annotation rows by source designator.
280
+ * @returns {object}
281
+ */
282
+ static #applyPinAnnotation(pin, annotations) {
283
+ const ownerIndex = String(pin?.ownerIndex || '')
284
+ const componentDesignator = String(pin?.componentDesignator || '')
285
+ const sourceDesignator = componentDesignator || ownerIndex
286
+ const compiledDesignator =
287
+ ProjectVariantViewBuilder.#compiledDesignator(
288
+ sourceDesignator,
289
+ annotations
290
+ )
291
+
292
+ if (compiledDesignator === sourceDesignator) {
293
+ return pin
294
+ }
295
+
296
+ return {
297
+ ...pin,
298
+ sourceDesignator,
299
+ ...(ownerIndex ? { ownerIndex: compiledDesignator } : {}),
300
+ ...(componentDesignator
301
+ ? { componentDesignator: compiledDesignator }
302
+ : {})
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Resolves a compiled designator for a source designator.
308
+ * @param {string} designator Source designator.
309
+ * @param {Record<string, object>} annotations Annotation rows by source designator.
310
+ * @returns {string}
311
+ */
312
+ static #compiledDesignator(designator, annotations) {
313
+ const key = String(designator || '').trim()
314
+ return String(annotations?.[key]?.compiledDesignator || key)
315
+ }
316
+
317
+ /**
318
+ * Checks whether an object has enumerable keys.
319
+ * @param {object | undefined} value Candidate object.
320
+ * @returns {boolean}
321
+ */
322
+ static #hasKeys(value) {
323
+ return Object.keys(value || {}).length > 0
324
+ }
325
+
326
+ /**
327
+ * Deduplicates values while preserving order.
328
+ * @param {unknown[]} values Candidate values.
329
+ * @returns {unknown[]}
330
+ */
331
+ static #dedupe(values) {
332
+ return [...new Set((values || []).filter(Boolean))]
333
+ }
334
+ }
@@ -0,0 +1,223 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { ParserUtils } from './ParserUtils.mjs'
6
+
7
+ const { getField, parseNumericField } = ParserUtils
8
+
9
+ /**
10
+ * Builds a read-only schematic component library/model binding layer.
11
+ */
12
+ export class SchematicBindingProvenanceParser {
13
+ static SCHEMA_ID = 'altium-toolkit.schematic.bindings.a1'
14
+
15
+ /**
16
+ * Parses component binding status from component and implementation rows.
17
+ * @param {object[]} records Schematic records.
18
+ * @param {object | null} implementations Implementation read model.
19
+ * @returns {object | null}
20
+ */
21
+ static parse(records, implementations) {
22
+ const componentRows =
23
+ SchematicBindingProvenanceParser.#componentRows(records)
24
+
25
+ if (!componentRows.length) {
26
+ return null
27
+ }
28
+
29
+ const implementationsByComponent =
30
+ SchematicBindingProvenanceParser.#implementationsByComponent(
31
+ implementations
32
+ )
33
+ const components = componentRows.map((component) =>
34
+ SchematicBindingProvenanceParser.#componentBinding(
35
+ component,
36
+ implementationsByComponent.get(component.componentKey) || []
37
+ )
38
+ )
39
+
40
+ return {
41
+ schema: SchematicBindingProvenanceParser.SCHEMA_ID,
42
+ summary: SchematicBindingProvenanceParser.#summary(components),
43
+ components
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Builds normalized component identity rows.
49
+ * @param {object[]} records Schematic records.
50
+ * @returns {object[]}
51
+ */
52
+ static #componentRows(records) {
53
+ return (records || [])
54
+ .filter((record) => getField(record.fields, 'RECORD') === '1')
55
+ .map((record) => {
56
+ const indexInSheet = parseNumericField(
57
+ record.fields,
58
+ 'IndexInSheet'
59
+ )
60
+ return {
61
+ componentKey:
62
+ 'schematic-component-' +
63
+ String(indexInSheet ?? record.recordIndex ?? 0),
64
+ recordKey:
65
+ 'schematic-record-' + String(record.recordIndex ?? 0),
66
+ uniqueId: getField(record.fields, 'UniqueID'),
67
+ libReference:
68
+ getField(record.fields, 'LibReference') ||
69
+ getField(record.fields, 'DesignItemId')
70
+ }
71
+ })
72
+ }
73
+
74
+ /**
75
+ * Groups implementation rows by owner component key.
76
+ * @param {object | null} implementations Implementation read model.
77
+ * @returns {Map<string, object[]>}
78
+ */
79
+ static #implementationsByComponent(implementations) {
80
+ const grouped = new Map()
81
+
82
+ for (const implementation of implementations?.implementations || []) {
83
+ const key = implementation.ownerComponentKey || ''
84
+ if (!key) continue
85
+ if (!grouped.has(key)) grouped.set(key, [])
86
+ grouped.get(key).push(implementation)
87
+ }
88
+
89
+ return grouped
90
+ }
91
+
92
+ /**
93
+ * Builds one component binding row.
94
+ * @param {object} component Component row.
95
+ * @param {object[]} linkedImplementations Linked implementation rows.
96
+ * @returns {object}
97
+ */
98
+ static #componentBinding(component, linkedImplementations) {
99
+ const status = SchematicBindingProvenanceParser.#status(
100
+ component,
101
+ linkedImplementations
102
+ )
103
+ const reasons = SchematicBindingProvenanceParser.#reasons(
104
+ status,
105
+ component,
106
+ linkedImplementations
107
+ )
108
+
109
+ return {
110
+ componentKey: component.componentKey,
111
+ recordKey: component.recordKey,
112
+ uniqueId: component.uniqueId,
113
+ libReference: component.libReference,
114
+ status,
115
+ implementationKeys: linkedImplementations.map(
116
+ (implementation) => implementation.key
117
+ ),
118
+ targetLibraries: SchematicBindingProvenanceParser.#targetLibraries(
119
+ linkedImplementations
120
+ ),
121
+ reasons
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Classifies component binding status.
127
+ * @param {object} component Component row.
128
+ * @param {object[]} linkedImplementations Linked implementation rows.
129
+ * @returns {'resolved' | 'unresolved' | 'stale' | 'external'}
130
+ */
131
+ static #status(component, linkedImplementations) {
132
+ if (linkedImplementations.length) {
133
+ const current =
134
+ linkedImplementations.find(
135
+ (implementation) => implementation.isCurrent
136
+ ) || linkedImplementations[0]
137
+ return current.modelName &&
138
+ (current.targetLibraries || []).length > 0
139
+ ? 'resolved'
140
+ : 'stale'
141
+ }
142
+
143
+ return component.libReference ? 'external' : 'unresolved'
144
+ }
145
+
146
+ /**
147
+ * Explains non-resolved status values.
148
+ * @param {string} status Binding status.
149
+ * @param {object} component Component row.
150
+ * @param {object[]} linkedImplementations Linked implementation rows.
151
+ * @returns {string[]}
152
+ */
153
+ static #reasons(status, component, linkedImplementations) {
154
+ if (status === 'resolved') return []
155
+ if (status === 'external') {
156
+ return ['component has a library reference but no local model link']
157
+ }
158
+ if (status === 'unresolved') {
159
+ return ['component has no local library reference or model link']
160
+ }
161
+
162
+ const reasons = []
163
+ const current =
164
+ linkedImplementations.find(
165
+ (implementation) => implementation.isCurrent
166
+ ) || linkedImplementations[0]
167
+ if (!current?.modelName) reasons.push('current model name is missing')
168
+ if (!(current?.targetLibraries || []).length) {
169
+ reasons.push('current model target library is missing')
170
+ }
171
+ if (!component.libReference) {
172
+ reasons.push('component library reference is missing')
173
+ }
174
+ return reasons
175
+ }
176
+
177
+ /**
178
+ * Flattens target-library rows without implementation-local indexes.
179
+ * @param {object[]} linkedImplementations Linked implementation rows.
180
+ * @returns {object[]}
181
+ */
182
+ static #targetLibraries(linkedImplementations) {
183
+ const libraries = []
184
+ const seen = new Set()
185
+
186
+ for (const implementation of linkedImplementations) {
187
+ for (const library of implementation.targetLibraries || []) {
188
+ const publicLibrary = {
189
+ entity: library.entity || '',
190
+ kind: library.kind || '',
191
+ fileName: library.fileName || ''
192
+ }
193
+ const key = JSON.stringify(publicLibrary)
194
+ if (seen.has(key)) continue
195
+ seen.add(key)
196
+ libraries.push(publicLibrary)
197
+ }
198
+ }
199
+
200
+ return libraries
201
+ }
202
+
203
+ /**
204
+ * Builds status counters.
205
+ * @param {object[]} components Binding rows.
206
+ * @returns {object}
207
+ */
208
+ static #summary(components) {
209
+ const summary = {
210
+ componentCount: components.length,
211
+ resolvedCount: 0,
212
+ unresolvedCount: 0,
213
+ staleCount: 0,
214
+ externalCount: 0
215
+ }
216
+
217
+ for (const component of components) {
218
+ summary[component.status + 'Count'] += 1
219
+ }
220
+
221
+ return summary
222
+ }
223
+ }