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