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.
- package/docs/api.md +6 -2
- package/docs/model-format.md +29 -4
- package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +80 -0
- package/docs/schemas/altium_toolkit/contract_gate_a1.schema.json +34 -0
- package/docs/schemas/altium_toolkit/draftsman_board_view_cache_a1.schema.json +115 -0
- package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +166 -0
- package/docs/schemas/altium_toolkit/host_capabilities_a1.schema.json +39 -0
- package/docs/schemas/altium_toolkit/library_merge_plan_a1.schema.json +56 -0
- package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +70 -0
- package/docs/schemas/altium_toolkit/netlist_a1.schema.json +6 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +856 -7
- package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
- package/docs/schemas/altium_toolkit/pcb_bom_profile_a1.schema.json +48 -0
- package/docs/schemas/altium_toolkit/pcb_layer_stack_a1.schema.json +98 -0
- package/docs/schemas/altium_toolkit/pcb_layer_stack_fidelity_a1.schema.json +66 -0
- package/docs/schemas/altium_toolkit/pcb_placed_footprint_extraction_a1.schema.json +31 -0
- package/docs/schemas/altium_toolkit/pcb_review_metadata_a1.schema.json +62 -0
- package/docs/schemas/altium_toolkit/pcb_rigid_flex_topology_a1.schema.json +52 -0
- package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +27 -0
- package/docs/schemas/altium_toolkit/pcblib_parity_a1.schema.json +24 -0
- package/docs/schemas/altium_toolkit/project_bom_pnp_reconciliation_a1.schema.json +63 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +6 -0
- package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
- package/docs/schemas/altium_toolkit/project_outjob_digest_a1.schema.json +46 -0
- package/docs/schemas/altium_toolkit/project_script_a1.schema.json +50 -0
- package/docs/schemas/altium_toolkit/schematic_render_ops_a1.schema.json +55 -0
- package/docs/schemas/altium_toolkit/schematic_template_extraction_a1.schema.json +37 -0
- package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -0
- package/package.json +1 -1
- package/src/core/altium/AltiumParser.mjs +12 -2
- package/src/core/altium/CiArtifactBundleBuilder.mjs +213 -0
- package/src/core/altium/ContractGateReportBuilder.mjs +351 -0
- package/src/core/altium/DraftsmanBoardViewMetadataBuilder.mjs +653 -0
- package/src/core/altium/DraftsmanDigestParser.mjs +928 -0
- package/src/core/altium/DraftsmanImagePayloadManifestBuilder.mjs +178 -0
- package/src/core/altium/HostCapabilityDiagnosticsBuilder.mjs +271 -0
- package/src/core/altium/LibraryQaReportBuilder.mjs +504 -0
- package/src/core/altium/LibraryRenderManifestBuilder.mjs +172 -2
- package/src/core/altium/ParserCompatibilityFuzzer.mjs +192 -0
- package/src/core/altium/PcbBomProfileBuilder.mjs +263 -0
- package/src/core/altium/PcbComponentKindPolicy.mjs +146 -0
- package/src/core/altium/PcbLayerStackFidelityReportBuilder.mjs +141 -0
- package/src/core/altium/PcbLayerStackInterchangeParser.mjs +453 -0
- package/src/core/altium/PcbLayerStackQueryHelper.mjs +195 -0
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +906 -0
- package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +488 -0
- package/src/core/altium/PcbLibModelParser.mjs +2 -0
- package/src/core/altium/PcbLibParityReportBuilder.mjs +242 -0
- package/src/core/altium/PcbModelParser.mjs +211 -22
- package/src/core/altium/PcbPadStackParser.mjs +171 -2
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +11 -1
- package/src/core/altium/PcbPlacedFootprintManifestBuilder.mjs +338 -0
- package/src/core/altium/PcbPolygonRecordParser.mjs +120 -0
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +71 -2
- package/src/core/altium/PcbReviewDrillMetadataBuilder.mjs +301 -0
- package/src/core/altium/PcbReviewMetadataBuilder.mjs +373 -0
- package/src/core/altium/PcbReviewPolygonRealizationBuilder.mjs +269 -0
- package/src/core/altium/PcbReviewRouteHighlightProfileBuilder.mjs +298 -0
- package/src/core/altium/PcbRigidFlexTopologyBuilder.mjs +171 -0
- package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
- package/src/core/altium/PcbStatisticsBuilder.mjs +9 -0
- package/src/core/altium/PrintableTextDecoder.mjs +70 -6
- package/src/core/altium/PrjPcbModelParser.mjs +69 -2
- package/src/core/altium/PrjScrModelParser.mjs +386 -0
- package/src/core/altium/ProjectBomPnpReconciliationBuilder.mjs +237 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +76 -2
- package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +5 -1
- package/src/core/altium/ProjectOutJobDigestBuilder.mjs +424 -13
- package/src/core/altium/SvgModelCrossLinkValidator.mjs +435 -0
- package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +300 -96
- package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
- package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
- package/src/parser.mjs +21 -0
- package/src/ui/PcbFootprintPrimitiveSelector.mjs +13 -1
- package/src/ui/PcbScene3dBuilder.mjs +26 -4
- package/src/ui/PcbSvgRenderer.mjs +65 -0
- package/src/ui/SchematicRenderOpsSidecarBuilder.mjs +554 -0
- package/src/ui/SchematicSvgRenderer.mjs +48 -2
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { SvgModelCrossLinkValidator } from './SvgModelCrossLinkValidator.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Builds deterministic contract-gate reports for CI artifact bundles.
|
|
9
|
+
*/
|
|
10
|
+
export class ContractGateReportBuilder {
|
|
11
|
+
static SCHEMA = 'altium-toolkit.contract-gate.a1'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Builds a contract-gate report over normalized and rendered artifacts.
|
|
15
|
+
* @param {{ documentModels?: object[], netlist?: { json?: object, wirelist?: string }, schematicSvgs?: object[], pcbLayerSvgs?: object[], diagnostics?: object[] }} options Gate input artifacts.
|
|
16
|
+
* @returns {object}
|
|
17
|
+
*/
|
|
18
|
+
static build(options = {}) {
|
|
19
|
+
const documentModels = options.documentModels || []
|
|
20
|
+
const svgLinkReports = ContractGateReportBuilder.#svgLinkReports(
|
|
21
|
+
documentModels,
|
|
22
|
+
options.schematicSvgs || [],
|
|
23
|
+
options.pcbLayerSvgs || []
|
|
24
|
+
)
|
|
25
|
+
const gates = [
|
|
26
|
+
ContractGateReportBuilder.#normalizedModelGate(documentModels),
|
|
27
|
+
ContractGateReportBuilder.#netlistJsonGate(options.netlist?.json),
|
|
28
|
+
ContractGateReportBuilder.#wirelistGate(options.netlist?.wirelist),
|
|
29
|
+
ContractGateReportBuilder.#svgLinkageGate(svgLinkReports),
|
|
30
|
+
ContractGateReportBuilder.#diagnosticsGate(
|
|
31
|
+
options.diagnostics || []
|
|
32
|
+
)
|
|
33
|
+
]
|
|
34
|
+
const failingGateCount = gates.filter(
|
|
35
|
+
(gate) => gate.status === 'fail'
|
|
36
|
+
).length
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
schema: ContractGateReportBuilder.SCHEMA,
|
|
40
|
+
status: failingGateCount > 0 ? 'fail' : 'pass',
|
|
41
|
+
summary: {
|
|
42
|
+
gateCount: gates.length,
|
|
43
|
+
failingGateCount,
|
|
44
|
+
documentCount: documentModels.length,
|
|
45
|
+
svgLinkReportCount: svgLinkReports.length,
|
|
46
|
+
diagnosticCount: (options.diagnostics || []).length
|
|
47
|
+
},
|
|
48
|
+
gates,
|
|
49
|
+
svgLinkReports
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Builds SVG link-validation reports for all rendered document outputs.
|
|
55
|
+
* @param {object[]} documentModels Normalized document models.
|
|
56
|
+
* @param {object[]} schematicSvgs Schematic SVG entries.
|
|
57
|
+
* @param {object[]} pcbLayerSvgs PCB layer SVG entries.
|
|
58
|
+
* @returns {object[]}
|
|
59
|
+
*/
|
|
60
|
+
static #svgLinkReports(documentModels, schematicSvgs, pcbLayerSvgs) {
|
|
61
|
+
return [
|
|
62
|
+
...ContractGateReportBuilder.#schematicSvgReports(
|
|
63
|
+
documentModels,
|
|
64
|
+
schematicSvgs
|
|
65
|
+
),
|
|
66
|
+
...ContractGateReportBuilder.#pcbLayerSvgReports(
|
|
67
|
+
documentModels,
|
|
68
|
+
pcbLayerSvgs
|
|
69
|
+
)
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Builds schematic SVG link reports.
|
|
75
|
+
* @param {object[]} documentModels Normalized document models.
|
|
76
|
+
* @param {object[]} schematicSvgs Schematic SVG entries.
|
|
77
|
+
* @returns {object[]}
|
|
78
|
+
*/
|
|
79
|
+
static #schematicSvgReports(documentModels, schematicSvgs) {
|
|
80
|
+
return (schematicSvgs || []).map((entry) => {
|
|
81
|
+
const model = ContractGateReportBuilder.#modelForFileName(
|
|
82
|
+
documentModels,
|
|
83
|
+
entry.fileName
|
|
84
|
+
)
|
|
85
|
+
return ContractGateReportBuilder.#linkReport(
|
|
86
|
+
entry.fileName,
|
|
87
|
+
model,
|
|
88
|
+
[entry.svg || '']
|
|
89
|
+
)
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Builds PCB layer SVG link reports as aggregate layer-view sets.
|
|
95
|
+
* @param {object[]} documentModels Normalized document models.
|
|
96
|
+
* @param {object[]} pcbLayerSvgs PCB layer SVG entries.
|
|
97
|
+
* @returns {object[]}
|
|
98
|
+
*/
|
|
99
|
+
static #pcbLayerSvgReports(documentModels, pcbLayerSvgs) {
|
|
100
|
+
return (pcbLayerSvgs || []).map((entry) => {
|
|
101
|
+
const model = ContractGateReportBuilder.#modelForFileName(
|
|
102
|
+
documentModels,
|
|
103
|
+
entry.fileName
|
|
104
|
+
)
|
|
105
|
+
return ContractGateReportBuilder.#linkReport(
|
|
106
|
+
entry.fileName,
|
|
107
|
+
model,
|
|
108
|
+
(entry.layers || []).map((layer) => layer.svg || '')
|
|
109
|
+
)
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Builds one SVG link report.
|
|
115
|
+
* @param {string} fileName Source file name.
|
|
116
|
+
* @param {object | undefined} model Normalized model.
|
|
117
|
+
* @param {string[]} svgMarkups SVG markup strings.
|
|
118
|
+
* @returns {object}
|
|
119
|
+
*/
|
|
120
|
+
static #linkReport(fileName, model, svgMarkups) {
|
|
121
|
+
if (!model) {
|
|
122
|
+
return {
|
|
123
|
+
fileName,
|
|
124
|
+
documentKind: 'unknown',
|
|
125
|
+
status: 'fail',
|
|
126
|
+
summary: {
|
|
127
|
+
missingElementCount: 0,
|
|
128
|
+
orphanElementCount: 0,
|
|
129
|
+
unresolvedReferenceCount: 1
|
|
130
|
+
},
|
|
131
|
+
missingElements: [],
|
|
132
|
+
orphanElements: [],
|
|
133
|
+
unresolvedReferences: [
|
|
134
|
+
{
|
|
135
|
+
referenceKind: 'document',
|
|
136
|
+
value: fileName
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const report = ContractGateReportBuilder.#normalizeLayerSetReport(
|
|
143
|
+
SvgModelCrossLinkValidator.validateSet(model, svgMarkups),
|
|
144
|
+
model,
|
|
145
|
+
svgMarkups
|
|
146
|
+
)
|
|
147
|
+
const status =
|
|
148
|
+
report.summary.missingElementCount > 0 ||
|
|
149
|
+
report.summary.orphanElementCount > 0 ||
|
|
150
|
+
report.summary.unresolvedReferenceCount > 0
|
|
151
|
+
? 'fail'
|
|
152
|
+
: 'pass'
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
fileName,
|
|
156
|
+
documentKind: report.documentKind,
|
|
157
|
+
status,
|
|
158
|
+
summary: report.summary,
|
|
159
|
+
missingElements: report.missingElements,
|
|
160
|
+
orphanElements: report.orphanElements,
|
|
161
|
+
unresolvedReferences: report.unresolvedReferences
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Removes composite-only component omissions from PCB layer-set reports.
|
|
167
|
+
* @param {object} report Raw SVG link report.
|
|
168
|
+
* @param {object} model Normalized model.
|
|
169
|
+
* @param {string[]} svgMarkups SVG markup strings.
|
|
170
|
+
* @returns {object}
|
|
171
|
+
*/
|
|
172
|
+
static #normalizeLayerSetReport(report, model, svgMarkups) {
|
|
173
|
+
if (
|
|
174
|
+
model?.kind !== 'pcb' ||
|
|
175
|
+
!ContractGateReportBuilder.#isLayerSvgSet(svgMarkups)
|
|
176
|
+
) {
|
|
177
|
+
return report
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const missingElements = (report.missingElements || []).filter(
|
|
181
|
+
(element) => element.collectionKey !== 'components'
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
...report,
|
|
186
|
+
summary: {
|
|
187
|
+
...report.summary,
|
|
188
|
+
linkedElementCount:
|
|
189
|
+
Number(report.summary.expectedElementCount || 0) -
|
|
190
|
+
missingElements.length,
|
|
191
|
+
missingElementCount: missingElements.length
|
|
192
|
+
},
|
|
193
|
+
missingElements
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Returns true when all supplied SVGs are layer-view exports.
|
|
199
|
+
* @param {string[]} svgMarkups SVG markup strings.
|
|
200
|
+
* @returns {boolean}
|
|
201
|
+
*/
|
|
202
|
+
static #isLayerSvgSet(svgMarkups) {
|
|
203
|
+
return (
|
|
204
|
+
(svgMarkups || []).length > 0 &&
|
|
205
|
+
(svgMarkups || []).every((svgMarkup) =>
|
|
206
|
+
String(svgMarkup || '').includes('data-view-kind="layer"')
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Builds the normalized-model gate.
|
|
213
|
+
* @param {object[]} documentModels Normalized document models.
|
|
214
|
+
* @returns {object}
|
|
215
|
+
*/
|
|
216
|
+
static #normalizedModelGate(documentModels) {
|
|
217
|
+
const failures = (documentModels || []).filter(
|
|
218
|
+
(model) => !model?.schema || !model?.kind
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
return ContractGateReportBuilder.#gate({
|
|
222
|
+
key: 'normalized-models',
|
|
223
|
+
status: failures.length ? 'fail' : 'pass',
|
|
224
|
+
checkedCount: documentModels.length,
|
|
225
|
+
failureCount: failures.length
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Builds the netlist JSON gate.
|
|
231
|
+
* @param {object | undefined} netlistJson Netlist JSON payload.
|
|
232
|
+
* @returns {object}
|
|
233
|
+
*/
|
|
234
|
+
static #netlistJsonGate(netlistJson) {
|
|
235
|
+
const pass =
|
|
236
|
+
Boolean(netlistJson?.schema) && Array.isArray(netlistJson?.nets)
|
|
237
|
+
|
|
238
|
+
return ContractGateReportBuilder.#gate({
|
|
239
|
+
key: 'netlist-json',
|
|
240
|
+
status: pass ? 'pass' : 'fail',
|
|
241
|
+
checkedCount: pass ? 1 : 0,
|
|
242
|
+
failureCount: pass ? 0 : 1
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Builds the wirelist gate.
|
|
248
|
+
* @param {string | undefined} wirelist Wirelist text.
|
|
249
|
+
* @returns {object}
|
|
250
|
+
*/
|
|
251
|
+
static #wirelistGate(wirelist) {
|
|
252
|
+
const pass = typeof wirelist === 'string'
|
|
253
|
+
|
|
254
|
+
return ContractGateReportBuilder.#gate({
|
|
255
|
+
key: 'wirelist',
|
|
256
|
+
status: pass ? 'pass' : 'fail',
|
|
257
|
+
checkedCount: pass ? 1 : 0,
|
|
258
|
+
failureCount: pass ? 0 : 1
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Builds the SVG linkage gate.
|
|
264
|
+
* @param {object[]} svgLinkReports SVG link reports.
|
|
265
|
+
* @returns {object}
|
|
266
|
+
*/
|
|
267
|
+
static #svgLinkageGate(svgLinkReports) {
|
|
268
|
+
const failingReports = (svgLinkReports || []).filter(
|
|
269
|
+
(report) => report.status === 'fail'
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
return ContractGateReportBuilder.#gate({
|
|
273
|
+
key: 'svg-linkage',
|
|
274
|
+
status: failingReports.length ? 'fail' : 'pass',
|
|
275
|
+
checkedCount: svgLinkReports.length,
|
|
276
|
+
failureCount: failingReports.length,
|
|
277
|
+
missingElementCount: ContractGateReportBuilder.#sumSummary(
|
|
278
|
+
svgLinkReports,
|
|
279
|
+
'missingElementCount'
|
|
280
|
+
),
|
|
281
|
+
orphanElementCount: ContractGateReportBuilder.#sumSummary(
|
|
282
|
+
svgLinkReports,
|
|
283
|
+
'orphanElementCount'
|
|
284
|
+
),
|
|
285
|
+
unresolvedReferenceCount: ContractGateReportBuilder.#sumSummary(
|
|
286
|
+
svgLinkReports,
|
|
287
|
+
'unresolvedReferenceCount'
|
|
288
|
+
)
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Builds the diagnostics gate.
|
|
294
|
+
* @param {object[]} diagnostics Diagnostic rows.
|
|
295
|
+
* @returns {object}
|
|
296
|
+
*/
|
|
297
|
+
static #diagnosticsGate(diagnostics) {
|
|
298
|
+
const errorCount = (diagnostics || []).filter(
|
|
299
|
+
(diagnostic) => diagnostic.severity === 'error'
|
|
300
|
+
).length
|
|
301
|
+
|
|
302
|
+
return ContractGateReportBuilder.#gate({
|
|
303
|
+
key: 'diagnostics',
|
|
304
|
+
status: errorCount ? 'fail' : 'pass',
|
|
305
|
+
checkedCount: diagnostics.length,
|
|
306
|
+
failureCount: errorCount,
|
|
307
|
+
warningCount: diagnostics.filter(
|
|
308
|
+
(diagnostic) => diagnostic.severity === 'warning'
|
|
309
|
+
).length,
|
|
310
|
+
errorCount
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Finds a normalized model by file name.
|
|
316
|
+
* @param {object[]} documentModels Normalized document models.
|
|
317
|
+
* @param {string} fileName Source file name.
|
|
318
|
+
* @returns {object | undefined}
|
|
319
|
+
*/
|
|
320
|
+
static #modelForFileName(documentModels, fileName) {
|
|
321
|
+
return (documentModels || []).find(
|
|
322
|
+
(model) => model?.fileName === fileName
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Sums one SVG link report summary field.
|
|
328
|
+
* @param {object[]} reports SVG link reports.
|
|
329
|
+
* @param {string} field Summary field.
|
|
330
|
+
* @returns {number}
|
|
331
|
+
*/
|
|
332
|
+
static #sumSummary(reports, field) {
|
|
333
|
+
return (reports || []).reduce(
|
|
334
|
+
(total, report) => total + Number(report.summary?.[field] || 0),
|
|
335
|
+
0
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Removes undefined gate fields.
|
|
341
|
+
* @param {object} gate Gate row.
|
|
342
|
+
* @returns {object}
|
|
343
|
+
*/
|
|
344
|
+
static #gate(gate) {
|
|
345
|
+
return Object.fromEntries(
|
|
346
|
+
Object.entries(gate || {}).filter(
|
|
347
|
+
([, value]) => value !== undefined
|
|
348
|
+
)
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
}
|