altium-toolkit 1.0.9 → 1.1.0

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 (79) hide show
  1. package/docs/api.md +6 -2
  2. package/docs/model-format.md +29 -4
  3. package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +80 -0
  4. package/docs/schemas/altium_toolkit/contract_gate_a1.schema.json +34 -0
  5. package/docs/schemas/altium_toolkit/draftsman_board_view_cache_a1.schema.json +115 -0
  6. package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +166 -0
  7. package/docs/schemas/altium_toolkit/host_capabilities_a1.schema.json +39 -0
  8. package/docs/schemas/altium_toolkit/library_merge_plan_a1.schema.json +56 -0
  9. package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +70 -0
  10. package/docs/schemas/altium_toolkit/netlist_a1.schema.json +6 -0
  11. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +856 -7
  12. package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
  13. package/docs/schemas/altium_toolkit/pcb_bom_profile_a1.schema.json +48 -0
  14. package/docs/schemas/altium_toolkit/pcb_layer_stack_a1.schema.json +98 -0
  15. package/docs/schemas/altium_toolkit/pcb_layer_stack_fidelity_a1.schema.json +66 -0
  16. package/docs/schemas/altium_toolkit/pcb_placed_footprint_extraction_a1.schema.json +31 -0
  17. package/docs/schemas/altium_toolkit/pcb_review_metadata_a1.schema.json +62 -0
  18. package/docs/schemas/altium_toolkit/pcb_rigid_flex_topology_a1.schema.json +52 -0
  19. package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +27 -0
  20. package/docs/schemas/altium_toolkit/pcblib_parity_a1.schema.json +24 -0
  21. package/docs/schemas/altium_toolkit/project_bom_pnp_reconciliation_a1.schema.json +63 -0
  22. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +6 -0
  23. package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
  24. package/docs/schemas/altium_toolkit/project_outjob_digest_a1.schema.json +46 -0
  25. package/docs/schemas/altium_toolkit/project_script_a1.schema.json +50 -0
  26. package/docs/schemas/altium_toolkit/schematic_render_ops_a1.schema.json +55 -0
  27. package/docs/schemas/altium_toolkit/schematic_template_extraction_a1.schema.json +37 -0
  28. package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -0
  29. package/package.json +1 -1
  30. package/src/core/altium/AltiumParser.mjs +12 -2
  31. package/src/core/altium/CiArtifactBundleBuilder.mjs +213 -0
  32. package/src/core/altium/ContractGateReportBuilder.mjs +351 -0
  33. package/src/core/altium/DraftsmanBoardViewMetadataBuilder.mjs +653 -0
  34. package/src/core/altium/DraftsmanDigestParser.mjs +928 -0
  35. package/src/core/altium/DraftsmanImagePayloadManifestBuilder.mjs +178 -0
  36. package/src/core/altium/HostCapabilityDiagnosticsBuilder.mjs +271 -0
  37. package/src/core/altium/LibraryQaReportBuilder.mjs +504 -0
  38. package/src/core/altium/LibraryRenderManifestBuilder.mjs +172 -2
  39. package/src/core/altium/ParserCompatibilityFuzzer.mjs +192 -0
  40. package/src/core/altium/PcbBomProfileBuilder.mjs +263 -0
  41. package/src/core/altium/PcbComponentKindPolicy.mjs +146 -0
  42. package/src/core/altium/PcbLayerStackFidelityReportBuilder.mjs +141 -0
  43. package/src/core/altium/PcbLayerStackInterchangeParser.mjs +453 -0
  44. package/src/core/altium/PcbLayerStackQueryHelper.mjs +195 -0
  45. package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +906 -0
  46. package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +488 -0
  47. package/src/core/altium/PcbLibModelParser.mjs +2 -0
  48. package/src/core/altium/PcbLibParityReportBuilder.mjs +242 -0
  49. package/src/core/altium/PcbModelParser.mjs +211 -22
  50. package/src/core/altium/PcbPadStackParser.mjs +171 -2
  51. package/src/core/altium/PcbPickPlacePositionResolver.mjs +11 -1
  52. package/src/core/altium/PcbPlacedFootprintManifestBuilder.mjs +338 -0
  53. package/src/core/altium/PcbPolygonRecordParser.mjs +120 -0
  54. package/src/core/altium/PcbRegionPrimitiveParser.mjs +71 -2
  55. package/src/core/altium/PcbReviewDrillMetadataBuilder.mjs +301 -0
  56. package/src/core/altium/PcbReviewMetadataBuilder.mjs +373 -0
  57. package/src/core/altium/PcbReviewPolygonRealizationBuilder.mjs +269 -0
  58. package/src/core/altium/PcbReviewRouteHighlightProfileBuilder.mjs +298 -0
  59. package/src/core/altium/PcbRigidFlexTopologyBuilder.mjs +171 -0
  60. package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
  61. package/src/core/altium/PcbStatisticsBuilder.mjs +9 -0
  62. package/src/core/altium/PrintableTextDecoder.mjs +70 -6
  63. package/src/core/altium/PrjPcbModelParser.mjs +69 -2
  64. package/src/core/altium/PrjScrModelParser.mjs +386 -0
  65. package/src/core/altium/ProjectBomPnpReconciliationBuilder.mjs +237 -0
  66. package/src/core/altium/ProjectDesignBundleBuilder.mjs +76 -2
  67. package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
  68. package/src/core/altium/ProjectNetlistExporter.mjs +5 -1
  69. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +424 -13
  70. package/src/core/altium/SvgModelCrossLinkValidator.mjs +435 -0
  71. package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +300 -96
  72. package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
  73. package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
  74. package/src/parser.mjs +21 -0
  75. package/src/ui/PcbFootprintPrimitiveSelector.mjs +13 -1
  76. package/src/ui/PcbScene3dBuilder.mjs +26 -4
  77. package/src/ui/PcbSvgRenderer.mjs +65 -0
  78. package/src/ui/SchematicRenderOpsSidecarBuilder.mjs +554 -0
  79. package/src/ui/SchematicSvgRenderer.mjs +48 -2
@@ -0,0 +1,435 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Validates that semantic SVG links point back to normalized model entries.
7
+ */
8
+ export class SvgModelCrossLinkValidator {
9
+ static SCHEMA = 'altium-toolkit.svg-model-cross-link.a1'
10
+
11
+ /**
12
+ * Validates semantic SVG data attributes against a normalized model.
13
+ * @param {object} documentModel Normalized schematic or PCB model.
14
+ * @param {string} svgMarkup SVG markup.
15
+ * @returns {object}
16
+ */
17
+ static validate(documentModel, svgMarkup) {
18
+ return SvgModelCrossLinkValidator.validateSet(documentModel, [
19
+ svgMarkup
20
+ ])
21
+ }
22
+
23
+ /**
24
+ * Validates a set of semantic SVG fragments against one normalized model.
25
+ * @param {object} documentModel Normalized schematic or PCB model.
26
+ * @param {string[]} svgMarkups SVG markup strings.
27
+ * @returns {object}
28
+ */
29
+ static validateSet(documentModel, svgMarkups) {
30
+ const documentKind =
31
+ SvgModelCrossLinkValidator.#documentKind(documentModel)
32
+ const expectedElements =
33
+ SvgModelCrossLinkValidator.#expectedElements(documentModel)
34
+ const expectedByKey = new Map(
35
+ expectedElements.map((element) => [element.elementKey, element])
36
+ )
37
+ const svgElements = (svgMarkups || []).flatMap((svgMarkup) =>
38
+ SvgModelCrossLinkValidator.#svgElements(svgMarkup)
39
+ )
40
+ const renderedKeys = new Set(
41
+ svgElements.map((element) => element.elementKey).filter(Boolean)
42
+ )
43
+ const orphanElements = SvgModelCrossLinkValidator.#orphanElements(
44
+ svgElements,
45
+ expectedByKey,
46
+ documentKind
47
+ )
48
+ const missingElements = SvgModelCrossLinkValidator.#missingElements(
49
+ expectedElements,
50
+ renderedKeys,
51
+ documentModel.diagnostics || []
52
+ )
53
+ const unresolvedReferences =
54
+ SvgModelCrossLinkValidator.#unresolvedReferences(
55
+ documentModel,
56
+ svgElements
57
+ )
58
+ const metadata = SvgModelCrossLinkValidator.#metadataSet(svgMarkups)
59
+
60
+ return {
61
+ schema: SvgModelCrossLinkValidator.SCHEMA,
62
+ documentKind,
63
+ summary: {
64
+ svgCount: (svgMarkups || []).length,
65
+ expectedElementCount: expectedElements.length,
66
+ renderedElementCount: renderedKeys.size,
67
+ linkedElementCount:
68
+ expectedElements.length - missingElements.length,
69
+ missingElementCount: missingElements.length,
70
+ orphanElementCount: orphanElements.length,
71
+ unresolvedReferenceCount: unresolvedReferences.length,
72
+ metadataElementCount: metadata.elements.length
73
+ },
74
+ missingElements,
75
+ orphanElements,
76
+ unresolvedReferences,
77
+ metadata
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Determines the model kind for report output.
83
+ * @param {object} documentModel Normalized document model.
84
+ * @returns {'schematic' | 'pcb' | 'unknown'}
85
+ */
86
+ static #documentKind(documentModel) {
87
+ if (documentModel?.schematic) return 'schematic'
88
+ if (documentModel?.pcb) return 'pcb'
89
+ return 'unknown'
90
+ }
91
+
92
+ /**
93
+ * Builds expected semantic element keys from a normalized model.
94
+ * @param {object} documentModel Normalized document model.
95
+ * @returns {object[]}
96
+ */
97
+ static #expectedElements(documentModel) {
98
+ if (documentModel?.schematic) {
99
+ return SvgModelCrossLinkValidator.#schematicExpectedElements(
100
+ documentModel.schematic
101
+ )
102
+ }
103
+ if (documentModel?.pcb) {
104
+ return SvgModelCrossLinkValidator.#pcbExpectedElements(
105
+ documentModel.pcb
106
+ )
107
+ }
108
+ return []
109
+ }
110
+
111
+ /**
112
+ * Builds expected schematic element descriptors.
113
+ * @param {object} schematic Normalized schematic payload.
114
+ * @returns {object[]}
115
+ */
116
+ static #schematicExpectedElements(schematic) {
117
+ return SvgModelCrossLinkValidator.#collectionElements('schematic', [
118
+ ['lines', 'line', schematic?.lines || []],
119
+ ['polygons', 'polygon', schematic?.polygons || []],
120
+ ['rectangles', 'rectangle', schematic?.rectangles || []],
121
+ [
122
+ 'roundedRectangles',
123
+ 'rounded-rectangle',
124
+ schematic?.roundedRectangles || []
125
+ ],
126
+ ['ellipses', 'ellipse', schematic?.ellipses || []],
127
+ ['arcs', 'arc', schematic?.arcs || []],
128
+ ['beziers', 'bezier', schematic?.beziers || []],
129
+ ['pies', 'pie', schematic?.pies || []],
130
+ ['ieeeSymbols', 'ieee-symbol', schematic?.ieeeSymbols || []],
131
+ ['texts', 'text', schematic?.texts || []],
132
+ ['pins', 'pin', schematic?.pins || []],
133
+ ['ports', 'port', schematic?.ports || []],
134
+ ['directives', 'directive', schematic?.directives || []]
135
+ ])
136
+ }
137
+
138
+ /**
139
+ * Builds expected PCB element descriptors.
140
+ * @param {object} pcb Normalized PCB payload.
141
+ * @returns {object[]}
142
+ */
143
+ static #pcbExpectedElements(pcb) {
144
+ return SvgModelCrossLinkValidator.#collectionElements('pcb', [
145
+ ['polygons', 'polygon', pcb?.polygons || []],
146
+ ['fills', 'fill', pcb?.fills || []],
147
+ ['tracks', 'track', pcb?.tracks || []],
148
+ ['arcs', 'arc', pcb?.arcs || []],
149
+ ['vias', 'via', pcb?.vias || []],
150
+ ['pads', 'pad', pcb?.pads || []],
151
+ ['texts', 'text', pcb?.texts || []],
152
+ ['components', 'component', pcb?.components || []]
153
+ ])
154
+ }
155
+
156
+ /**
157
+ * Builds descriptors for primitive collections.
158
+ * @param {'schematic' | 'pcb'} prefix SVG element prefix.
159
+ * @param {[string, string, object[]][]} collections Collections to inspect.
160
+ * @returns {object[]}
161
+ */
162
+ static #collectionElements(prefix, collections) {
163
+ const elements = []
164
+ for (const [collectionKey, primitiveKind, records] of collections) {
165
+ for (const [index, record] of (records || []).entries()) {
166
+ elements.push({
167
+ elementKey: prefix + '-' + primitiveKind + '-' + index,
168
+ collectionKey,
169
+ primitiveKind,
170
+ recordId:
171
+ record?.recordId ??
172
+ record?.sourceRecordId ??
173
+ record?.sourceRecordIndex ??
174
+ ''
175
+ })
176
+ }
177
+ }
178
+ return elements
179
+ }
180
+
181
+ /**
182
+ * Extracts SVG elements that carry semantic data attributes.
183
+ * @param {string} svgMarkup SVG markup.
184
+ * @returns {object[]}
185
+ */
186
+ static #svgElements(svgMarkup) {
187
+ const elements = []
188
+ const tagPattern = /<[^>]+data-element-key="[^"]+"[^>]*>/gu
189
+ let match = tagPattern.exec(String(svgMarkup || ''))
190
+ while (match) {
191
+ const attrs = SvgModelCrossLinkValidator.#dataAttributes(match[0])
192
+ elements.push({
193
+ elementKey: attrs.elementKey || '',
194
+ primitive: attrs.primitive || '',
195
+ component: attrs.component || '',
196
+ net: attrs.net || '',
197
+ pin: attrs.pin || '',
198
+ attrs
199
+ })
200
+ match = tagPattern.exec(String(svgMarkup || ''))
201
+ }
202
+ return elements
203
+ }
204
+
205
+ /**
206
+ * Extracts data attributes from one SVG tag.
207
+ * @param {string} tag SVG tag markup.
208
+ * @returns {Record<string, string>}
209
+ */
210
+ static #dataAttributes(tag) {
211
+ const attrs = {}
212
+ const attrPattern = /data-([a-z0-9-]+)="([^"]*)"/giu
213
+ let match = attrPattern.exec(tag || '')
214
+ while (match) {
215
+ attrs[SvgModelCrossLinkValidator.#camelCase(match[1])] =
216
+ SvgModelCrossLinkValidator.#decodeEntities(match[2])
217
+ match = attrPattern.exec(tag || '')
218
+ }
219
+ return attrs
220
+ }
221
+
222
+ /**
223
+ * Finds rendered elements not represented by the normalized model.
224
+ * @param {object[]} svgElements Rendered semantic SVG rows.
225
+ * @param {Map<string, object>} expectedByKey Expected element map.
226
+ * @param {'schematic' | 'pcb' | 'unknown'} documentKind Document kind.
227
+ * @returns {object[]}
228
+ */
229
+ static #orphanElements(svgElements, expectedByKey, documentKind) {
230
+ return svgElements
231
+ .filter(
232
+ (element) =>
233
+ !expectedByKey.has(element.elementKey) &&
234
+ !SvgModelCrossLinkValidator.#isRendererOwnedElement(
235
+ element.elementKey,
236
+ documentKind
237
+ )
238
+ )
239
+ .map((element) => ({
240
+ elementKey: element.elementKey,
241
+ primitive: element.primitive || ''
242
+ }))
243
+ }
244
+
245
+ /**
246
+ * Finds normalized elements that are not represented in SVG output.
247
+ * @param {object[]} expectedElements Expected model element rows.
248
+ * @param {Set<string>} renderedKeys Rendered SVG element keys.
249
+ * @param {object[]} diagnostics Model diagnostics.
250
+ * @returns {object[]}
251
+ */
252
+ static #missingElements(expectedElements, renderedKeys, diagnostics) {
253
+ return expectedElements
254
+ .filter(
255
+ (element) =>
256
+ !renderedKeys.has(element.elementKey) &&
257
+ !SvgModelCrossLinkValidator.#hasDiagnostic(
258
+ element,
259
+ diagnostics
260
+ )
261
+ )
262
+ .map((element) => ({
263
+ elementKey: element.elementKey,
264
+ collectionKey: element.collectionKey,
265
+ primitiveKind: element.primitiveKind,
266
+ recordId: element.recordId || undefined
267
+ }))
268
+ }
269
+
270
+ /**
271
+ * Checks whether a missing element has an explicit diagnostic.
272
+ * @param {object} element Expected element row.
273
+ * @param {object[]} diagnostics Model diagnostics.
274
+ * @returns {boolean}
275
+ */
276
+ static #hasDiagnostic(element, diagnostics) {
277
+ return (diagnostics || []).some((diagnostic) => {
278
+ const message = String(diagnostic?.message || '')
279
+ return (
280
+ diagnostic?.elementKey === element.elementKey ||
281
+ (element.recordId &&
282
+ diagnostic?.recordId === element.recordId) ||
283
+ message.includes(element.elementKey)
284
+ )
285
+ })
286
+ }
287
+
288
+ /**
289
+ * Finds component/net references that cannot be resolved in the model.
290
+ * @param {object} documentModel Normalized document model.
291
+ * @param {object[]} svgElements Rendered semantic SVG rows.
292
+ * @returns {object[]}
293
+ */
294
+ static #unresolvedReferences(documentModel, svgElements) {
295
+ const components =
296
+ SvgModelCrossLinkValidator.#componentNames(documentModel)
297
+ const nets = SvgModelCrossLinkValidator.#netNames(documentModel)
298
+ const unresolved = []
299
+
300
+ for (const element of svgElements) {
301
+ if (element.component && !components.has(element.component)) {
302
+ unresolved.push({
303
+ elementKey: element.elementKey,
304
+ referenceKind: 'component',
305
+ value: element.component
306
+ })
307
+ }
308
+ if (element.net && !nets.has(element.net)) {
309
+ unresolved.push({
310
+ elementKey: element.elementKey,
311
+ referenceKind: 'net',
312
+ value: element.net
313
+ })
314
+ }
315
+ }
316
+
317
+ return unresolved
318
+ }
319
+
320
+ /**
321
+ * Collects normalized component designators.
322
+ * @param {object} documentModel Normalized document model.
323
+ * @returns {Set<string>}
324
+ */
325
+ static #componentNames(documentModel) {
326
+ const components =
327
+ documentModel?.schematic?.components ||
328
+ documentModel?.pcb?.components ||
329
+ []
330
+ return new Set(
331
+ components
332
+ .map((component) => String(component?.designator || ''))
333
+ .filter(Boolean)
334
+ )
335
+ }
336
+
337
+ /**
338
+ * Collects normalized net names.
339
+ * @param {object} documentModel Normalized document model.
340
+ * @returns {Set<string>}
341
+ */
342
+ static #netNames(documentModel) {
343
+ const nets =
344
+ documentModel?.schematic?.nets || documentModel?.pcb?.nets || []
345
+ return new Set(
346
+ nets.map((net) => String(net?.name || '')).filter(Boolean)
347
+ )
348
+ }
349
+
350
+ /**
351
+ * Returns true for renderer-owned semantic helpers.
352
+ * @param {string} elementKey SVG element key.
353
+ * @param {'schematic' | 'pcb' | 'unknown'} documentKind Document kind.
354
+ * @returns {boolean}
355
+ */
356
+ static #isRendererOwnedElement(elementKey, documentKind) {
357
+ if (documentKind !== 'pcb') return false
358
+ return (
359
+ elementKey === 'pcb-board-outline' ||
360
+ elementKey === 'pcb-board-outline-stroke' ||
361
+ /^pcb-board-cutout-\d+$/u.test(elementKey) ||
362
+ /^pcb-(via|pad)-hole-\d+$/u.test(elementKey)
363
+ )
364
+ }
365
+
366
+ /**
367
+ * Extracts the semantic metadata JSON sidecar when present.
368
+ * @param {string} svgMarkup SVG markup.
369
+ * @returns {{ schema: string, elements: object[] }}
370
+ */
371
+ static #metadata(svgMarkup) {
372
+ const match = String(svgMarkup || '').match(
373
+ /<metadata id="(?:schematic|pcb)-semantic-metadata"[^>]*>([^<]*)<\/metadata>/u
374
+ )
375
+ if (!match) {
376
+ return { schema: '', elements: [] }
377
+ }
378
+ try {
379
+ const metadata = JSON.parse(
380
+ SvgModelCrossLinkValidator.#decodeEntities(match[1])
381
+ )
382
+ return {
383
+ schema: metadata.schema || '',
384
+ elements: Array.isArray(metadata.elements)
385
+ ? metadata.elements
386
+ : []
387
+ }
388
+ } catch {
389
+ return { schema: '', elements: [] }
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Extracts semantic metadata from a set of SVG fragments.
395
+ * @param {string[]} svgMarkups SVG markup strings.
396
+ * @returns {{ schema: string, elements: object[] }}
397
+ */
398
+ static #metadataSet(svgMarkups) {
399
+ const metadataRows = (svgMarkups || []).map((svgMarkup) =>
400
+ SvgModelCrossLinkValidator.#metadata(svgMarkup)
401
+ )
402
+ const schema =
403
+ metadataRows.find((metadata) => metadata.schema)?.schema || ''
404
+
405
+ return {
406
+ schema,
407
+ elements: metadataRows.flatMap((metadata) => metadata.elements)
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Converts a data attribute token to a camelCase object key.
413
+ * @param {string} value Attribute token.
414
+ * @returns {string}
415
+ */
416
+ static #camelCase(value) {
417
+ return String(value || '').replace(/-([a-z0-9])/giu, (_match, char) =>
418
+ String(char).toUpperCase()
419
+ )
420
+ }
421
+
422
+ /**
423
+ * Decodes basic XML entities.
424
+ * @param {string} value Encoded value.
425
+ * @returns {string}
426
+ */
427
+ static #decodeEntities(value) {
428
+ return String(value || '')
429
+ .replace(/&quot;/gu, '"')
430
+ .replace(/&apos;/gu, "'")
431
+ .replace(/&lt;/gu, '<')
432
+ .replace(/&gt;/gu, '>')
433
+ .replace(/&amp;/gu, '&')
434
+ }
435
+ }