altium-toolkit 1.0.8 → 1.0.10

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 (102) 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/ci_artifact_bundle_a1.schema.json +76 -0
  5. package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +35 -0
  6. package/docs/schemas/altium_toolkit/netlist_a1.schema.json +53 -0
  7. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1826 -110
  8. package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
  9. package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +86 -0
  10. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +63 -0
  11. package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
  12. package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
  13. package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -0
  14. package/docs/testing.md +9 -3
  15. package/package.json +1 -1
  16. package/spec/library-scope.md +7 -1
  17. package/src/core/altium/AltiumLayoutParser.mjs +104 -8
  18. package/src/core/altium/AltiumParser.mjs +196 -45
  19. package/src/core/altium/CiArtifactBundleBuilder.mjs +202 -0
  20. package/src/core/altium/DraftsmanDigestParser.mjs +689 -0
  21. package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
  22. package/src/core/altium/IntLibModelParser.mjs +240 -0
  23. package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
  24. package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
  25. package/src/core/altium/LibrarySearchIndex.mjs +215 -0
  26. package/src/core/altium/NormalizedModelSchema.mjs +36 -0
  27. package/src/core/altium/ParserCompatibilityFuzzer.mjs +192 -0
  28. package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
  29. package/src/core/altium/PcbDefaultsParser.mjs +171 -0
  30. package/src/core/altium/PcbDimensionParser.mjs +229 -0
  31. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
  32. package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
  33. package/src/core/altium/PcbLibModelParser.mjs +235 -14
  34. package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
  35. package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
  36. package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
  37. package/src/core/altium/PcbModelParser.mjs +495 -32
  38. package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
  39. package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
  40. package/src/core/altium/PcbPadStackParser.mjs +229 -2
  41. package/src/core/altium/PcbPickPlacePositionResolver.mjs +224 -0
  42. package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
  43. package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
  44. package/src/core/altium/PcbRegionPrimitiveParser.mjs +76 -3
  45. package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
  46. package/src/core/altium/PcbRuleParser.mjs +354 -33
  47. package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
  48. package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
  49. package/src/core/altium/PcbStatisticsBuilder.mjs +541 -0
  50. package/src/core/altium/PcbStreamExtractor.mjs +111 -4
  51. package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
  52. package/src/core/altium/PcbUnionParser.mjs +307 -0
  53. package/src/core/altium/PcbViaStackParser.mjs +98 -10
  54. package/src/core/altium/PcbViaStructureParser.mjs +335 -0
  55. package/src/core/altium/PrintableTextDecoder.mjs +53 -3
  56. package/src/core/altium/PrjPcbModelParser.mjs +281 -7
  57. package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
  58. package/src/core/altium/ProjectDesignBundleBuilder.mjs +492 -0
  59. package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
  60. package/src/core/altium/ProjectNetlistExporter.mjs +503 -0
  61. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
  62. package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
  63. package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
  64. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
  65. package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
  66. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
  67. package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
  68. package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
  69. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
  70. package/src/core/altium/SchematicHarnessParser.mjs +302 -0
  71. package/src/core/altium/SchematicImageParser.mjs +474 -3
  72. package/src/core/altium/SchematicImplementationParser.mjs +518 -0
  73. package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
  74. package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
  75. package/src/core/altium/SchematicPinParser.mjs +84 -1
  76. package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
  77. package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
  78. package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
  79. package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
  80. package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
  81. package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
  82. package/src/core/altium/SchematicTemplateParser.mjs +256 -0
  83. package/src/core/altium/SchematicTextParser.mjs +123 -0
  84. package/src/core/altium/SvgModelCrossLinkValidator.mjs +402 -0
  85. package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +136 -96
  86. package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
  87. package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
  88. package/src/core/ole/OleCompoundDocument.mjs +20 -0
  89. package/src/parser.mjs +35 -0
  90. package/src/styles/altium-renderers.css +19 -0
  91. package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
  92. package/src/ui/PcbInteractionIndex.mjs +9 -4
  93. package/src/ui/PcbScene3dBuilder.mjs +137 -3
  94. package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
  95. package/src/ui/PcbSvgRenderer.mjs +1252 -34
  96. package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
  97. package/src/ui/SchematicNoteRenderer.mjs +9 -2
  98. package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
  99. package/src/ui/SchematicShapeRenderer.mjs +362 -0
  100. package/src/ui/SchematicSvgRenderer.mjs +1442 -92
  101. package/src/ui/SchematicTypography.mjs +48 -5
  102. package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
@@ -0,0 +1,492 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { NormalizedModelSchema } from './NormalizedModelSchema.mjs'
6
+ import { ProjectVariantViewBuilder } from './ProjectVariantViewBuilder.mjs'
7
+
8
+ /**
9
+ * Composes parsed project, schematic, and PCB models into one read-only design
10
+ * bundle for multi-document consumers.
11
+ */
12
+ export class ProjectDesignBundleBuilder {
13
+ static #UNITS = {
14
+ coordinate: 'mil',
15
+ length: 'mil',
16
+ board: 'mil',
17
+ pnp: 'mil',
18
+ angle: 'deg'
19
+ }
20
+
21
+ static #PNP_UNITS = {
22
+ coordinate: 'mil',
23
+ angle: 'deg'
24
+ }
25
+
26
+ /**
27
+ * Builds a normalized project/design bundle from already parsed models.
28
+ * @param {{ projectModel?: object, documentModels?: object[], annotationModels?: object[], variantName?: string }} options Bundle options.
29
+ * @returns {object}
30
+ */
31
+ static build(options = {}) {
32
+ const projectModel = options.projectModel || {}
33
+ const documentModels = Array.isArray(options.documentModels)
34
+ ? options.documentModels
35
+ : []
36
+ const project = projectModel.project || {}
37
+ const documents = project.documents || []
38
+ const schematicModels = documentModels.filter(
39
+ (model) => model?.kind === 'schematic'
40
+ )
41
+ const pcbModels = documentModels.filter(
42
+ (model) => model?.kind === 'pcb'
43
+ )
44
+ const sheets = ProjectDesignBundleBuilder.#buildSheets(
45
+ schematicModels,
46
+ documents
47
+ )
48
+ const components = ProjectDesignBundleBuilder.#buildComponents(
49
+ schematicModels,
50
+ pcbModels
51
+ )
52
+ const pnp = ProjectDesignBundleBuilder.#buildPnp(pcbModels)
53
+ const nets = ProjectDesignBundleBuilder.#buildNets(
54
+ schematicModels,
55
+ pcbModels
56
+ )
57
+ const bom = ProjectDesignBundleBuilder.#buildBom(documentModels)
58
+ const annotations = ProjectDesignBundleBuilder.#buildAnnotations(
59
+ options.annotationModels || []
60
+ )
61
+ const indexes = ProjectDesignBundleBuilder.#buildIndexes(
62
+ documents,
63
+ sheets,
64
+ components,
65
+ nets,
66
+ pnp
67
+ )
68
+ const bundle = NormalizedModelSchema.attach({
69
+ kind: 'design-bundle',
70
+ fileType: 'ProjectDesignBundle',
71
+ fileName: projectModel.fileName || 'design-bundle.json',
72
+ summary: {
73
+ title:
74
+ projectModel.summary?.title ||
75
+ project.name ||
76
+ 'Project design bundle',
77
+ sheetCount: sheets.length,
78
+ componentCount: components.length,
79
+ netCount: nets.length,
80
+ pnpCount: pnp.entries.length,
81
+ variantCount: (project.variants || []).length,
82
+ annotationMappingCount: annotations.mappings.length
83
+ },
84
+ diagnostics: [
85
+ {
86
+ severity: 'info',
87
+ message:
88
+ 'Composed ' +
89
+ documentModels.length +
90
+ ' parsed document models into a project design bundle.'
91
+ }
92
+ ],
93
+ project,
94
+ units: ProjectDesignBundleBuilder.#UNITS,
95
+ variants: project.variants || [],
96
+ sheets,
97
+ components,
98
+ schematic_hierarchy:
99
+ ProjectDesignBundleBuilder.#buildSchematicHierarchy(
100
+ project,
101
+ schematicModels,
102
+ documents
103
+ ),
104
+ pnp,
105
+ nets,
106
+ annotations,
107
+ indexes,
108
+ bom
109
+ })
110
+
111
+ if (options.variantName) {
112
+ bundle.effectiveVariant = ProjectVariantViewBuilder.build(bundle, {
113
+ variantName: options.variantName
114
+ })
115
+ }
116
+
117
+ return bundle
118
+ }
119
+
120
+ /**
121
+ * Builds sheet entries from schematic models and project document rows.
122
+ * @param {object[]} schematicModels Parsed schematic models.
123
+ * @param {object[]} documents Project document rows.
124
+ * @returns {object[]}
125
+ */
126
+ static #buildSheets(schematicModels, documents) {
127
+ return schematicModels.map((model, index) => {
128
+ const document = ProjectDesignBundleBuilder.#documentForModel(
129
+ model,
130
+ documents
131
+ )
132
+
133
+ return {
134
+ bundleIndex: index,
135
+ fileName: model.fileName,
136
+ title: model.summary?.title || model.fileName,
137
+ documentPath: document?.path || model.fileName,
138
+ uniqueId: document?.uniqueId || '',
139
+ sheet: model.schematic?.sheet || {},
140
+ componentCount: model.schematic?.components?.length || 0,
141
+ netCount: model.schematic?.nets?.length || 0
142
+ }
143
+ })
144
+ }
145
+
146
+ /**
147
+ * Builds component entries joined by designator across schematic and PCB
148
+ * documents.
149
+ * @param {object[]} schematicModels Parsed schematic models.
150
+ * @param {object[]} pcbModels Parsed PCB models.
151
+ * @returns {object[]}
152
+ */
153
+ static #buildComponents(schematicModels, pcbModels) {
154
+ const componentsByDesignator = new Map()
155
+
156
+ for (const model of schematicModels) {
157
+ for (const component of model.schematic?.components || []) {
158
+ const entry = ProjectDesignBundleBuilder.#componentEntry(
159
+ componentsByDesignator,
160
+ component.designator
161
+ )
162
+ entry.schematic = {
163
+ fileName: model.fileName,
164
+ uniqueId: component.uniqueId || '',
165
+ libReference: component.libReference || '',
166
+ value: component.value || ''
167
+ }
168
+ }
169
+ }
170
+
171
+ for (const model of pcbModels) {
172
+ for (const component of model.pcb?.components || []) {
173
+ const entry = ProjectDesignBundleBuilder.#componentEntry(
174
+ componentsByDesignator,
175
+ component.designator
176
+ )
177
+ entry.pcb = {
178
+ fileName: model.fileName,
179
+ componentIndex: component.componentIndex,
180
+ uniqueId: component.uniqueId || '',
181
+ pattern: component.pattern || ''
182
+ }
183
+ }
184
+ }
185
+
186
+ return [...componentsByDesignator.values()].map((component, index) => ({
187
+ bundleIndex: index,
188
+ ...component
189
+ }))
190
+ }
191
+
192
+ /**
193
+ * Gets or creates one component bundle entry.
194
+ * @param {Map<string, object>} componentsByDesignator Component map.
195
+ * @param {string} designator Component designator.
196
+ * @returns {object}
197
+ */
198
+ static #componentEntry(componentsByDesignator, designator) {
199
+ const key = String(designator || '').trim()
200
+ if (!componentsByDesignator.has(key)) {
201
+ componentsByDesignator.set(key, {
202
+ designator: key,
203
+ schematic: null,
204
+ pcb: null
205
+ })
206
+ }
207
+ return componentsByDesignator.get(key)
208
+ }
209
+
210
+ /**
211
+ * Builds a combined pick-place model.
212
+ * @param {object[]} pcbModels Parsed PCB models.
213
+ * @returns {object}
214
+ */
215
+ static #buildPnp(pcbModels) {
216
+ const entries = []
217
+ let positionMode = ''
218
+
219
+ for (const model of pcbModels) {
220
+ const pnp = model.pnp || model.pcb?.pickPlace || {}
221
+ positionMode ||= pnp.positionMode || ''
222
+ for (const entry of pnp.entries || []) {
223
+ entries.push({
224
+ bundleIndex: entries.length,
225
+ sourceFileName: model.fileName,
226
+ ...entry
227
+ })
228
+ }
229
+ }
230
+
231
+ return {
232
+ units: ProjectDesignBundleBuilder.#PNP_UNITS,
233
+ positionMode,
234
+ entries,
235
+ modes: {}
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Builds combined schematic and PCB net entries.
241
+ * @param {object[]} schematicModels Parsed schematic models.
242
+ * @param {object[]} pcbModels Parsed PCB models.
243
+ * @returns {object[]}
244
+ */
245
+ static #buildNets(schematicModels, pcbModels) {
246
+ const netsByName = new Map()
247
+
248
+ for (const model of schematicModels) {
249
+ for (const net of model.schematic?.nets || []) {
250
+ const entry = ProjectDesignBundleBuilder.#netEntry(
251
+ netsByName,
252
+ net.name
253
+ )
254
+ entry.schematic.push({
255
+ fileName: model.fileName,
256
+ pins: net.pins || [],
257
+ labels: net.labels || [],
258
+ segments: net.segments || [],
259
+ ...(model.schematic?.harnesses
260
+ ? { harnesses: model.schematic.harnesses.connectors }
261
+ : {})
262
+ })
263
+ entry.pins.push(...(net.pins || []))
264
+ }
265
+ }
266
+
267
+ for (const model of pcbModels) {
268
+ for (const net of model.pcb?.nets || []) {
269
+ const entry = ProjectDesignBundleBuilder.#netEntry(
270
+ netsByName,
271
+ net.name
272
+ )
273
+ entry.pcb.push({
274
+ fileName: model.fileName,
275
+ netIndex: net.netIndex,
276
+ uniqueId: net.uniqueId || ''
277
+ })
278
+ }
279
+ }
280
+
281
+ return [...netsByName.values()].map((net, index) => ({
282
+ bundleIndex: index,
283
+ ...net
284
+ }))
285
+ }
286
+
287
+ /**
288
+ * Gets or creates one net bundle entry.
289
+ * @param {Map<string, object>} netsByName Net map.
290
+ * @param {string} name Net name.
291
+ * @returns {object}
292
+ */
293
+ static #netEntry(netsByName, name) {
294
+ const key = String(name || '').trim()
295
+ if (!netsByName.has(key)) {
296
+ netsByName.set(key, {
297
+ name: key,
298
+ schematic: [],
299
+ pcb: [],
300
+ pins: []
301
+ })
302
+ }
303
+ return netsByName.get(key)
304
+ }
305
+
306
+ /**
307
+ * Selects a combined BOM, preferring PCB BOM rows when available.
308
+ * @param {object[]} documentModels Parsed document models.
309
+ * @returns {object[]}
310
+ */
311
+ static #buildBom(documentModels) {
312
+ const pcbBom = documentModels
313
+ .filter((model) => model?.kind === 'pcb')
314
+ .flatMap((model) => model.bom || [])
315
+
316
+ if (pcbBom.length) {
317
+ return pcbBom
318
+ }
319
+
320
+ return documentModels.flatMap((model) => model.bom || [])
321
+ }
322
+
323
+ /**
324
+ * Builds compiled-designator annotation mappings.
325
+ * @param {object[]} annotationModels Parsed annotation models.
326
+ * @returns {{ mappings: object[], bySourceDesignator: Record<string, object>, byCompiledDesignator: Record<string, object> }}
327
+ */
328
+ static #buildAnnotations(annotationModels) {
329
+ const mappings = []
330
+
331
+ for (const model of annotationModels || []) {
332
+ mappings.push(...(model?.annotations?.mappings || []))
333
+ }
334
+
335
+ return {
336
+ mappings,
337
+ bySourceDesignator: ProjectDesignBundleBuilder.#indexFullBy(
338
+ mappings,
339
+ 'sourceDesignator'
340
+ ),
341
+ byCompiledDesignator: ProjectDesignBundleBuilder.#indexFullBy(
342
+ mappings,
343
+ 'compiledDesignator'
344
+ )
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Builds schematic hierarchy metadata.
350
+ * @param {object} project Project model.
351
+ * @param {object[]} schematicModels Parsed schematic models.
352
+ * @param {object[]} documents Project document rows.
353
+ * @returns {object}
354
+ */
355
+ static #buildSchematicHierarchy(project, schematicModels, documents) {
356
+ const hierarchy = {
357
+ mode: project?.design?.HierarchyMode || '',
358
+ modeName: ProjectDesignBundleBuilder.#hierarchyModeName(
359
+ project?.design?.HierarchyMode
360
+ ),
361
+ sheets: schematicModels.map((model) => {
362
+ const document = ProjectDesignBundleBuilder.#documentForModel(
363
+ model,
364
+ documents
365
+ )
366
+ return {
367
+ fileName: model.fileName,
368
+ documentPath: document?.path || model.fileName,
369
+ uniqueId: document?.uniqueId || '',
370
+ title: model.summary?.title || model.fileName
371
+ }
372
+ }),
373
+ sheetSymbols: schematicModels.flatMap((model) =>
374
+ (model.schematic?.sheetSymbols || []).map((sheetSymbol) => ({
375
+ sheetFileName: model.fileName,
376
+ uniqueId: sheetSymbol.uniqueId || '',
377
+ entries: (model.schematic?.sheetEntries || []).map(
378
+ (entry) => entry.name
379
+ )
380
+ }))
381
+ )
382
+ }
383
+ const harnessBundleLinks = schematicModels.flatMap((model) =>
384
+ (model.schematic?.harnesses?.bundleLinks || []).map((link) => ({
385
+ sheetFileName: model.fileName,
386
+ ...link
387
+ }))
388
+ )
389
+
390
+ if (harnessBundleLinks.length) {
391
+ hierarchy.harness_bundle_links = harnessBundleLinks
392
+ }
393
+
394
+ return hierarchy
395
+ }
396
+
397
+ /**
398
+ * Builds bundle lookup indexes.
399
+ * @param {object[]} documents Project document rows.
400
+ * @param {object[]} sheets Bundle sheets.
401
+ * @param {object[]} components Bundle components.
402
+ * @param {object[]} nets Bundle nets.
403
+ * @param {object} pnp Bundle PnP model.
404
+ * @returns {object}
405
+ */
406
+ static #buildIndexes(documents, sheets, components, nets, pnp) {
407
+ return {
408
+ documentsByPath: ProjectDesignBundleBuilder.#indexBy(
409
+ documents,
410
+ 'normalizedPath'
411
+ ),
412
+ sheetsByFileName: ProjectDesignBundleBuilder.#indexBy(
413
+ sheets,
414
+ 'fileName'
415
+ ),
416
+ componentsByDesignator: ProjectDesignBundleBuilder.#indexBy(
417
+ components,
418
+ 'designator'
419
+ ),
420
+ netsByName: ProjectDesignBundleBuilder.#indexBy(nets, 'name'),
421
+ pnpByDesignator: ProjectDesignBundleBuilder.#indexBy(
422
+ pnp.entries,
423
+ 'designator'
424
+ )
425
+ }
426
+ }
427
+
428
+ /**
429
+ * Builds a compact object index by a field.
430
+ * @param {object[]} records Records to index.
431
+ * @param {string} key Field name.
432
+ * @returns {Record<string, object>}
433
+ */
434
+ static #indexBy(records, key) {
435
+ const index = {}
436
+ for (const record of records || []) {
437
+ const value = String(record?.[key] || '').trim()
438
+ if (!value) continue
439
+ index[value] = {
440
+ bundleIndex: record.bundleIndex ?? record.index ?? 0
441
+ }
442
+ }
443
+ return index
444
+ }
445
+
446
+ /**
447
+ * Builds a full object index by a field.
448
+ * @param {object[]} records Records to index.
449
+ * @param {string} key Field name.
450
+ * @returns {Record<string, object>}
451
+ */
452
+ static #indexFullBy(records, key) {
453
+ const index = {}
454
+ for (const record of records || []) {
455
+ const value = String(record?.[key] || '').trim()
456
+ if (value) index[value] = record
457
+ }
458
+ return index
459
+ }
460
+
461
+ /**
462
+ * Finds the project document row corresponding to a parsed model.
463
+ * @param {object} model Parsed document model.
464
+ * @param {object[]} documents Project document rows.
465
+ * @returns {object | null}
466
+ */
467
+ static #documentForModel(model, documents) {
468
+ return (
469
+ (documents || []).find(
470
+ (document) => document.fileName === model.fileName
471
+ ) || null
472
+ )
473
+ }
474
+
475
+ /**
476
+ * Resolves a display name for a project hierarchy mode.
477
+ * @param {string | number | undefined} mode Raw hierarchy mode.
478
+ * @returns {string}
479
+ */
480
+ static #hierarchyModeName(mode) {
481
+ switch (String(mode || '')) {
482
+ case '2':
483
+ return 'hierarchical'
484
+ case '1':
485
+ return 'flat'
486
+ case '3':
487
+ return 'global'
488
+ default:
489
+ return 'unspecified'
490
+ }
491
+ }
492
+ }