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,178 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Builds deterministic image-payload manifests for Draftsman digest images.
7
+ */
8
+ export class DraftsmanImagePayloadManifestBuilder {
9
+ static SCHEMA = 'altium-toolkit.draftsman.image-payloads.a1'
10
+
11
+ /**
12
+ * Builds a payload manifest from parsed Draftsman pages.
13
+ * @param {{ index: number, images?: object[] }[]} pages Parsed page rows.
14
+ * @returns {{ schema: string, summary: object, payloads: object[], diagnostics: object[] }}
15
+ */
16
+ static build(pages) {
17
+ const imageRows = DraftsmanImagePayloadManifestBuilder.#imageRows(pages)
18
+ const payloads = []
19
+ const diagnostics = []
20
+
21
+ for (const image of imageRows) {
22
+ const bytes =
23
+ DraftsmanImagePayloadManifestBuilder.#payloadBytes(image)
24
+ if (!bytes.length) {
25
+ diagnostics.push(
26
+ DraftsmanImagePayloadManifestBuilder.#missingPayloadDiagnostic(
27
+ image
28
+ )
29
+ )
30
+ continue
31
+ }
32
+
33
+ payloads.push(
34
+ DraftsmanImagePayloadManifestBuilder.#payloadRecord(
35
+ image,
36
+ bytes
37
+ )
38
+ )
39
+ }
40
+
41
+ return {
42
+ schema: DraftsmanImagePayloadManifestBuilder.SCHEMA,
43
+ summary: {
44
+ imageCount: imageRows.length,
45
+ payloadCount: payloads.length,
46
+ diagnosticCount: diagnostics.length
47
+ },
48
+ payloads,
49
+ diagnostics
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Flattens page/image rows while preserving page and image indexes.
55
+ * @param {{ index: number, images?: object[] }[]} pages Parsed pages.
56
+ * @returns {object[]}
57
+ */
58
+ static #imageRows(pages) {
59
+ return (pages || []).flatMap((page) =>
60
+ (page.images || []).map((image, index) => ({
61
+ ...image,
62
+ pageIndex: page.index,
63
+ imageIndex: index
64
+ }))
65
+ )
66
+ }
67
+
68
+ /**
69
+ * Builds one payload manifest record.
70
+ * @param {object} image Image descriptor.
71
+ * @param {Uint8Array} bytes Payload bytes.
72
+ * @returns {object}
73
+ */
74
+ static #payloadRecord(image, bytes) {
75
+ return DraftsmanImagePayloadManifestBuilder.#stripUndefined({
76
+ pageIndex: image.pageIndex,
77
+ imageId: image.id,
78
+ name: image.name,
79
+ nativeFormat: image.nativeFormat,
80
+ wrapperType:
81
+ image.wrapperType ||
82
+ image.fields?.WrapperType ||
83
+ image.fields?.Wrapper ||
84
+ undefined,
85
+ byteSize: bytes.byteLength,
86
+ checksum: {
87
+ algorithm: 'fnv1a32',
88
+ value: DraftsmanImagePayloadManifestBuilder.#fnv1a32(bytes)
89
+ }
90
+ })
91
+ }
92
+
93
+ /**
94
+ * Builds a structured missing-payload diagnostic.
95
+ * @param {object} image Image descriptor.
96
+ * @returns {object}
97
+ */
98
+ static #missingPayloadDiagnostic(image) {
99
+ return DraftsmanImagePayloadManifestBuilder.#stripUndefined({
100
+ code: 'draftsman.image-payload.missing-bytes',
101
+ severity: 'warning',
102
+ pageIndex: image.pageIndex,
103
+ imageId: image.id,
104
+ name: image.name,
105
+ message: 'Draftsman image item did not include payload bytes.'
106
+ })
107
+ }
108
+
109
+ /**
110
+ * Extracts base64 payload bytes from known image fields.
111
+ * @param {object} image Image descriptor.
112
+ * @returns {Uint8Array}
113
+ */
114
+ static #payloadBytes(image) {
115
+ const fields = image?.fields || {}
116
+ const value =
117
+ fields.PayloadBase64 ||
118
+ fields.DataBase64 ||
119
+ fields.BytesBase64 ||
120
+ fields.NativePayloadBase64 ||
121
+ fields.BitmapBase64 ||
122
+ ''
123
+
124
+ return DraftsmanImagePayloadManifestBuilder.#decodeBase64(value)
125
+ }
126
+
127
+ /**
128
+ * Decodes a base64 value without depending on Node-only globals.
129
+ * @param {string} value Base64 text.
130
+ * @returns {Uint8Array}
131
+ */
132
+ static #decodeBase64(value) {
133
+ const normalized = String(value || '').replace(/\s+/gu, '')
134
+ if (!normalized) {
135
+ return new Uint8Array()
136
+ }
137
+
138
+ try {
139
+ const binary = globalThis.atob(normalized)
140
+ const bytes = new Uint8Array(binary.length)
141
+ for (let index = 0; index < binary.length; index += 1) {
142
+ bytes[index] = binary.charCodeAt(index)
143
+ }
144
+ return bytes
145
+ } catch {
146
+ return new Uint8Array()
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Computes an FNV-1a 32-bit checksum.
152
+ * @param {Uint8Array} bytes Payload bytes.
153
+ * @returns {string}
154
+ */
155
+ static #fnv1a32(bytes) {
156
+ let hash = 0x811c9dc5
157
+
158
+ for (const value of bytes) {
159
+ hash ^= value
160
+ hash = Math.imul(hash, 0x01000193) >>> 0
161
+ }
162
+
163
+ return hash.toString(16).padStart(8, '0')
164
+ }
165
+
166
+ /**
167
+ * Removes undefined object fields.
168
+ * @param {Record<string, unknown>} value Candidate object.
169
+ * @returns {Record<string, unknown>}
170
+ */
171
+ static #stripUndefined(value) {
172
+ return Object.fromEntries(
173
+ Object.entries(value || {}).filter(
174
+ ([, entryValue]) => entryValue !== undefined
175
+ )
176
+ )
177
+ }
178
+ }
@@ -0,0 +1,271 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Builds deterministic host capability and fallback diagnostics.
7
+ */
8
+ export class HostCapabilityDiagnosticsBuilder {
9
+ static SCHEMA = 'altium-toolkit.host-capabilities.a1'
10
+
11
+ /**
12
+ * Builds a host capability diagnostics report.
13
+ * @param {{ host?: object, capabilities?: Record<string, boolean>, fallbacks?: object[] }} options Diagnostics options.
14
+ * @returns {object}
15
+ */
16
+ static build(options = {}) {
17
+ const capabilities = HostCapabilityDiagnosticsBuilder.#capabilityRows(
18
+ options.capabilities || {}
19
+ )
20
+ const diagnostics = [
21
+ ...HostCapabilityDiagnosticsBuilder.#capabilityDiagnostics(
22
+ capabilities
23
+ ),
24
+ ...HostCapabilityDiagnosticsBuilder.#fallbackDiagnostics(
25
+ options.fallbacks || []
26
+ )
27
+ ]
28
+ const readiness = HostCapabilityDiagnosticsBuilder.#readiness(
29
+ options.readinessCategories || [],
30
+ capabilities,
31
+ diagnostics
32
+ )
33
+
34
+ return HostCapabilityDiagnosticsBuilder.#stripUndefined({
35
+ schema: HostCapabilityDiagnosticsBuilder.SCHEMA,
36
+ host: options.host || {},
37
+ summary: HostCapabilityDiagnosticsBuilder.#stripUndefined({
38
+ capabilityCount: capabilities.length,
39
+ unsupportedCapabilityCount: capabilities.filter(
40
+ (capability) => !capability.supported
41
+ ).length,
42
+ fallbackCount: (options.fallbacks || []).length,
43
+ readinessStatus: readiness?.status,
44
+ readinessCategoryCount: readiness?.categories.length,
45
+ warningCount: diagnostics.filter(
46
+ (diagnostic) => diagnostic.severity === 'warning'
47
+ ).length
48
+ }),
49
+ capabilities,
50
+ readiness,
51
+ diagnostics
52
+ })
53
+ }
54
+
55
+ /**
56
+ * Builds sorted capability rows.
57
+ * @param {Record<string, boolean>} capabilities Capability map.
58
+ * @returns {object[]}
59
+ */
60
+ static #capabilityRows(capabilities) {
61
+ return Object.keys(capabilities || {})
62
+ .sort((left, right) =>
63
+ left.localeCompare(right, undefined, { numeric: true })
64
+ )
65
+ .map((key) => {
66
+ const supported = capabilities[key] === true
67
+ return HostCapabilityDiagnosticsBuilder.#stripUndefined({
68
+ key,
69
+ supported,
70
+ diagnosticCode: supported
71
+ ? undefined
72
+ : HostCapabilityDiagnosticsBuilder.#capabilityCode(key)
73
+ })
74
+ })
75
+ }
76
+
77
+ /**
78
+ * Builds diagnostics for unsupported capabilities.
79
+ * @param {object[]} capabilities Capability rows.
80
+ * @returns {object[]}
81
+ */
82
+ static #capabilityDiagnostics(capabilities) {
83
+ return capabilities
84
+ .filter((capability) => !capability.supported)
85
+ .map((capability) => ({
86
+ code: capability.diagnosticCode,
87
+ severity: 'warning',
88
+ capability: capability.key,
89
+ message:
90
+ 'Host capability ' + capability.key + ' is unavailable.'
91
+ }))
92
+ }
93
+
94
+ /**
95
+ * Builds diagnostics for caller-supplied fallback decisions.
96
+ * @param {object[]} fallbacks Fallback rows.
97
+ * @returns {object[]}
98
+ */
99
+ static #fallbackDiagnostics(fallbacks) {
100
+ return (fallbacks || []).map((fallback) =>
101
+ HostCapabilityDiagnosticsBuilder.#stripUndefined({
102
+ ...fallback,
103
+ code: fallback.code || 'host.fallback.used',
104
+ severity: fallback.severity || 'info',
105
+ message:
106
+ fallback.message ||
107
+ 'Host fallback ' +
108
+ (fallback.code || 'host.fallback.used') +
109
+ ' was used.'
110
+ })
111
+ )
112
+ }
113
+
114
+ /**
115
+ * Builds host support readiness groups.
116
+ * @param {object[]} categories Readiness category descriptors.
117
+ * @param {object[]} capabilities Capability rows.
118
+ * @param {object[]} diagnostics Diagnostic rows.
119
+ * @returns {object | undefined}
120
+ */
121
+ static #readiness(categories, capabilities, diagnostics) {
122
+ if (!categories.length) {
123
+ return undefined
124
+ }
125
+
126
+ const capabilityByKey = new Map(
127
+ capabilities.map((capability) => [capability.key, capability])
128
+ )
129
+ const categoryRows = categories.map((category) =>
130
+ HostCapabilityDiagnosticsBuilder.#readinessCategory(
131
+ category,
132
+ capabilityByKey,
133
+ diagnostics
134
+ )
135
+ )
136
+
137
+ return {
138
+ status: HostCapabilityDiagnosticsBuilder.#readinessStatus(
139
+ categoryRows
140
+ ),
141
+ categories: categoryRows
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Builds one readiness category.
147
+ * @param {object} category Category descriptor.
148
+ * @param {Map<string, object>} capabilityByKey Capability lookup.
149
+ * @param {object[]} diagnostics Diagnostic rows.
150
+ * @returns {object}
151
+ */
152
+ static #readinessCategory(category, capabilityByKey, diagnostics) {
153
+ const capabilityKeys = [...(category.capabilityKeys || [])]
154
+ const capabilityRows = capabilityKeys
155
+ .map((key) => capabilityByKey.get(key))
156
+ .filter(Boolean)
157
+ const unsupportedCapabilities = capabilityRows.filter(
158
+ (capability) => !capability.supported
159
+ )
160
+ const categoryDiagnostics =
161
+ HostCapabilityDiagnosticsBuilder.#categoryDiagnostics(
162
+ category.key,
163
+ unsupportedCapabilities,
164
+ diagnostics
165
+ )
166
+ const fallbackCount = categoryDiagnostics.filter(
167
+ (diagnostic) => diagnostic.category === category.key
168
+ ).length
169
+
170
+ return {
171
+ key: category.key,
172
+ displayName: category.displayName || category.key,
173
+ status: HostCapabilityDiagnosticsBuilder.#categoryStatus(
174
+ capabilityRows,
175
+ fallbackCount
176
+ ),
177
+ capabilityKeys,
178
+ supportedCapabilityCount: capabilityRows.filter(
179
+ (capability) => capability.supported
180
+ ).length,
181
+ unsupportedCapabilityCount: unsupportedCapabilities.length,
182
+ fallbackCount,
183
+ diagnosticCodes: categoryDiagnostics.map(
184
+ (diagnostic) => diagnostic.code
185
+ )
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Returns diagnostics related to one readiness category.
191
+ * @param {string} categoryKey Category key.
192
+ * @param {object[]} unsupportedCapabilities Unsupported capabilities.
193
+ * @param {object[]} diagnostics Diagnostic rows.
194
+ * @returns {object[]}
195
+ */
196
+ static #categoryDiagnostics(
197
+ categoryKey,
198
+ unsupportedCapabilities,
199
+ diagnostics
200
+ ) {
201
+ const unsupportedCodes = new Set(
202
+ unsupportedCapabilities.map(
203
+ (capability) => capability.diagnosticCode
204
+ )
205
+ )
206
+
207
+ return diagnostics.filter(
208
+ (diagnostic) =>
209
+ unsupportedCodes.has(diagnostic.code) ||
210
+ diagnostic.category === categoryKey
211
+ )
212
+ }
213
+
214
+ /**
215
+ * Resolves one readiness category status.
216
+ * @param {object[]} capabilityRows Capability rows.
217
+ * @param {number} fallbackCount Fallback count.
218
+ * @returns {'supported' | 'limited' | 'unsupported'}
219
+ */
220
+ static #categoryStatus(capabilityRows, fallbackCount) {
221
+ const supportedCount = capabilityRows.filter(
222
+ (capability) => capability.supported
223
+ ).length
224
+ const unsupportedCount = capabilityRows.length - supportedCount
225
+
226
+ if (capabilityRows.length > 0 && supportedCount === 0) {
227
+ return 'unsupported'
228
+ }
229
+ if (unsupportedCount > 0 || fallbackCount > 0) {
230
+ return 'limited'
231
+ }
232
+ return 'supported'
233
+ }
234
+
235
+ /**
236
+ * Resolves the aggregate readiness status.
237
+ * @param {object[]} categories Readiness category rows.
238
+ * @returns {'supported' | 'limited' | 'unsupported'}
239
+ */
240
+ static #readinessStatus(categories) {
241
+ if (categories.every((category) => category.status === 'supported')) {
242
+ return 'supported'
243
+ }
244
+ if (categories.every((category) => category.status === 'unsupported')) {
245
+ return 'unsupported'
246
+ }
247
+ return 'limited'
248
+ }
249
+
250
+ /**
251
+ * Builds the diagnostic code for an unsupported capability.
252
+ * @param {string} key Capability key.
253
+ * @returns {string}
254
+ */
255
+ static #capabilityCode(key) {
256
+ return 'host.capability.' + key + '.unsupported'
257
+ }
258
+
259
+ /**
260
+ * Removes undefined fields.
261
+ * @param {Record<string, unknown>} value Candidate object.
262
+ * @returns {Record<string, unknown>}
263
+ */
264
+ static #stripUndefined(value) {
265
+ return Object.fromEntries(
266
+ Object.entries(value || {}).filter(
267
+ ([, entryValue]) => entryValue !== undefined
268
+ )
269
+ )
270
+ }
271
+ }