altium-toolkit 1.1.3 → 1.1.23

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 (125) hide show
  1. package/README.md +34 -5
  2. package/docs/api.md +171 -23
  3. package/docs/model-format.md +192 -20
  4. package/docs/schemas/altium_toolkit/embedded_assets_a1.schema.json +56 -0
  5. package/docs/schemas/altium_toolkit/fixture_coverage_matrix_a1.schema.json +89 -0
  6. package/docs/schemas/altium_toolkit/geometry_bounds_a1.schema.json +86 -0
  7. package/docs/schemas/altium_toolkit/library_catalog_a1.schema.json +65 -0
  8. package/docs/schemas/altium_toolkit/library_diff_a1.schema.json +54 -0
  9. package/docs/schemas/altium_toolkit/library_inspection_a1.schema.json +94 -0
  10. package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +4 -0
  11. package/docs/schemas/altium_toolkit/native_stream_inventory_a1.schema.json +66 -0
  12. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +513 -3
  13. package/docs/schemas/altium_toolkit/parameter_record_inventory_a1.schema.json +84 -0
  14. package/docs/schemas/altium_toolkit/parser_diagnostics_a1.schema.json +63 -0
  15. package/docs/schemas/altium_toolkit/parser_value_verification_a1.schema.json +74 -0
  16. package/docs/schemas/altium_toolkit/pcb_class_report_a1.schema.json +79 -0
  17. package/docs/schemas/altium_toolkit/pcb_inspection_a1.schema.json +65 -0
  18. package/docs/schemas/altium_toolkit/pcb_net_membership_a1.schema.json +98 -0
  19. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +3 -0
  20. package/docs/schemas/altium_toolkit/project_hierarchy_a1.schema.json +79 -0
  21. package/docs/schemas/altium_toolkit/unsupported_features_a1.schema.json +212 -0
  22. package/docs/testing.md +7 -0
  23. package/examples/README.md +21 -0
  24. package/examples/cli-utils.mjs +148 -0
  25. package/examples/corpus-smoke.mjs +523 -0
  26. package/examples/extract-bom.mjs +47 -0
  27. package/examples/generate-pnp.mjs +59 -0
  28. package/examples/inspect-board.mjs +70 -0
  29. package/examples/inspect-schematic.mjs +406 -0
  30. package/examples/library-catalog.mjs +115 -0
  31. package/examples/net-report.mjs +61 -0
  32. package/examples/validate-library.mjs +59 -0
  33. package/package.json +1 -1
  34. package/spec/library-scope.md +5 -0
  35. package/src/core/BinaryReader.mjs +213 -2
  36. package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
  37. package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
  38. package/src/core/altium/AltiumParser.mjs +357 -16
  39. package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
  40. package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
  41. package/src/core/altium/AltiumUnits.mjs +205 -0
  42. package/src/core/altium/AsciiRecordParser.mjs +52 -11
  43. package/src/core/altium/EmbeddedAssetReportBuilder.mjs +383 -0
  44. package/src/core/altium/FixtureCoverageMatrixBuilder.mjs +304 -0
  45. package/src/core/altium/GeometryBoundsReportBuilder.mjs +935 -0
  46. package/src/core/altium/LibraryCatalogArtifactBuilder.mjs +296 -0
  47. package/src/core/altium/LibraryDiffReportBuilder.mjs +260 -0
  48. package/src/core/altium/LibraryInspectionReportBuilder.mjs +156 -0
  49. package/src/core/altium/LibraryQaReportBuilder.mjs +374 -1
  50. package/src/core/altium/NativeStreamInventoryBuilder.mjs +177 -0
  51. package/src/core/altium/NormalizedModelSchema.mjs +3 -31
  52. package/src/core/altium/ParameterCollection.mjs +431 -0
  53. package/src/core/altium/ParameterRecordInventoryBuilder.mjs +274 -0
  54. package/src/core/altium/ParserCompatibilityFuzzer.mjs +106 -2
  55. package/src/core/altium/ParserDiagnosticNormalizer.mjs +213 -0
  56. package/src/core/altium/ParserErrors.mjs +90 -0
  57. package/src/core/altium/ParserFieldCoverageReportBuilder.mjs +656 -0
  58. package/src/core/altium/ParserUtils.mjs +24 -0
  59. package/src/core/altium/ParserValueVerificationReportBuilder.mjs +323 -0
  60. package/src/core/altium/PcbClassReportBuilder.mjs +366 -0
  61. package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
  62. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
  63. package/src/core/altium/PcbInspectionReportBuilder.mjs +313 -0
  64. package/src/core/altium/PcbLayerGroups.mjs +308 -0
  65. package/src/core/altium/PcbLayerStackCustomDataParser.mjs +183 -0
  66. package/src/core/altium/PcbLayerStackInterchangeParser.mjs +473 -4
  67. package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +83 -15
  68. package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +74 -4
  69. package/src/core/altium/PcbLibModelParser.mjs +20 -4
  70. package/src/core/altium/PcbLibStreamExtractor.mjs +49 -6
  71. package/src/core/altium/PcbModelParser.mjs +223 -4
  72. package/src/core/altium/PcbNetMembershipReportBuilder.mjs +270 -0
  73. package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
  74. package/src/core/altium/PcbStreamExtractor.mjs +130 -6
  75. package/src/core/altium/PcbTrackPrimitiveParser.mjs +66 -2
  76. package/src/core/altium/ProjectDesignBundleBuilder.mjs +15 -0
  77. package/src/core/altium/ProjectHierarchyReportBuilder.mjs +660 -0
  78. package/src/core/altium/ProjectNetlistExporter.mjs +2 -0
  79. package/src/core/altium/RawDataPreservationReportBuilder.mjs +348 -0
  80. package/src/core/altium/SchLibModelParser.mjs +840 -0
  81. package/src/core/altium/SchLibStreamExtractor.mjs +586 -0
  82. package/src/core/altium/SchematicBusEntryParser.mjs +3 -2
  83. package/src/core/altium/SchematicCodeSymbolParser.mjs +663 -0
  84. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +177 -2
  85. package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
  86. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +10 -1
  87. package/src/core/altium/SchematicFieldCoverageReportBuilder.mjs +549 -0
  88. package/src/core/altium/SchematicHarnessParser.mjs +9 -3
  89. package/src/core/altium/SchematicHyperlinkParser.mjs +122 -0
  90. package/src/core/altium/SchematicNetlistBuilder.mjs +271 -8
  91. package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
  92. package/src/core/altium/SchematicOwnershipGraphParser.mjs +102 -3
  93. package/src/core/altium/SchematicPinParser.mjs +99 -65
  94. package/src/core/altium/SchematicPrimitiveParser.mjs +125 -22
  95. package/src/core/altium/SchematicQaReportBuilder.mjs +2 -0
  96. package/src/core/altium/SchematicRecordStreamParser.mjs +183 -0
  97. package/src/core/altium/SchematicRecordTypeRegistry.mjs +6 -1
  98. package/src/core/altium/SchematicSheetParser.mjs +8 -2
  99. package/src/core/altium/SchematicStreamExtractor.mjs +107 -21
  100. package/src/core/altium/SchematicTextOrientationResolver.mjs +76 -0
  101. package/src/core/altium/SchematicTextParser.mjs +28 -12
  102. package/src/core/altium/SchematicTextRunParser.mjs +81 -0
  103. package/src/core/altium/SchematicThumbnailParser.mjs +425 -0
  104. package/src/core/altium/SourceBundleExporter.mjs +156 -0
  105. package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
  106. package/src/core/altium/SourceComponentClient.mjs +239 -0
  107. package/src/core/altium/UnsupportedFeatureReportBuilder.mjs +380 -0
  108. package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
  109. package/src/parser.mjs +43 -1
  110. package/src/renderers.mjs +1 -0
  111. package/src/styles/altium-renderers.css +6 -6
  112. package/src/ui/PcbArcUtils.mjs +19 -2
  113. package/src/ui/PcbScene3dBuilder.mjs +202 -20
  114. package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
  115. package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
  116. package/src/ui/SchematicColorResolver.mjs +185 -0
  117. package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
  118. package/src/ui/SchematicLineColorResolver.mjs +88 -0
  119. package/src/ui/SchematicNoteRenderer.mjs +5 -1
  120. package/src/ui/SchematicOwnerPinLabelLayout.mjs +269 -8
  121. package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
  122. package/src/ui/SchematicPinSvgRenderer.mjs +229 -62
  123. package/src/ui/SchematicShapeRenderer.mjs +86 -17
  124. package/src/ui/SchematicSvgRenderer.mjs +980 -58
  125. package/src/ui/SchematicTypography.mjs +4 -3
@@ -9,8 +9,8 @@
9
9
  export class SchematicNetlistBuilder {
10
10
  /**
11
11
  * Builds normalized nets and connectivity diagnostics.
12
- * @param {{ lines: { x1: number, y1: number, x2: number, y2: number, ownerIndex?: string, isBus?: boolean }[], texts: { x: number, y: number, text: string, recordType?: string }[], pins?: { x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom', name: string, designator: string }[], ports?: { x: number, y: number, width: number, direction?: 'left' | 'right' | 'up' | 'down', name: string }[], crossSheetConnectors?: { key: string, x: number, y: number, name: string, style?: string }[], junctions?: { x: number, y: number, color: string }[], busEntries?: { x1: number, y1: number, x2: number, y2: number }[], sheetEntries?: { x: number, y: number, name: string }[] }} schematic
13
- * @returns {{ nets: { name: string, segments: { x1: number, y1: number, x2: number, y2: number, ownerIndex?: string, isBus?: boolean }[], labels: { x: number, y: number, text: string, recordType?: string }[], powerPorts: { x: number, y: number, text: string, recordType?: string }[], crossSheetConnectors: { key: string, name: string, x: number, y: number, style?: string }[], pins: { x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom', name: string, designator: string }[], ports: { x: number, y: number, width: number, direction?: 'left' | 'right' | 'up' | 'down', name: string }[], junctions: { x: number, y: number, color: string }[], busEntries: { x1: number, y1: number, x2: number, y2: number }[], sheetEntries: { x: number, y: number, name: string }[] }[], diagnostics: { severity: 'warning', message: string }[] }}
12
+ * @param {{ lines: { x1: number, y1: number, x2: number, y2: number, ownerIndex?: string, isBus?: boolean }[], texts: { x: number, y: number, text: string, recordType?: string }[], pins?: { x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom', name: string, designator: string, ownerIndex?: string, componentDesignator?: string }[], ports?: { x: number, y: number, width: number, direction?: 'left' | 'right' | 'up' | 'down', name: string }[], crossSheetConnectors?: { key: string, x: number, y: number, name: string, style?: string }[], junctions?: { x: number, y: number, color: string }[], busEntries?: { x1: number, y1: number, x2: number, y2: number }[], sheetEntries?: { x: number, y: number, name: string }[], componentDesignatorsByOwnerIndex?: Map<string, string> | Record<string, string> }} schematic
13
+ * @returns {{ nets: { name: string, autoName?: string, autoNameSource?: string, aliasCandidates?: string[], segments: { x1: number, y1: number, x2: number, y2: number, ownerIndex?: string, isBus?: boolean }[], labels: { x: number, y: number, text: string, recordType?: string }[], powerPorts: { x: number, y: number, text: string, recordType?: string }[], crossSheetConnectors: { key: string, name: string, x: number, y: number, style?: string }[], pins: { x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom', name: string, designator: string, ownerIndex?: string, componentDesignator?: string }[], ports: { x: number, y: number, width: number, direction?: 'left' | 'right' | 'up' | 'down', name: string }[], junctions: { x: number, y: number, color: string }[], busEntries: { x1: number, y1: number, x2: number, y2: number }[], sheetEntries: { x: number, y: number, name: string }[] }[], diagnostics: { severity: 'warning', message: string }[] }}
14
14
  */
15
15
  static build(schematic) {
16
16
  const diagnostics = []
@@ -43,12 +43,19 @@ export class SchematicNetlistBuilder {
43
43
  ).filter((connector) =>
44
44
  SchematicNetlistBuilder.#groupContainsPoint(group, connector)
45
45
  )
46
- const pins = (schematic.pins || []).filter((pin) =>
47
- SchematicNetlistBuilder.#groupContainsPoint(
48
- group,
49
- SchematicNetlistBuilder.#resolvePinConnectionPoint(pin)
46
+ const pins = (schematic.pins || [])
47
+ .filter((pin) =>
48
+ SchematicNetlistBuilder.#groupContainsPoint(
49
+ group,
50
+ SchematicNetlistBuilder.#resolvePinConnectionPoint(pin)
51
+ )
52
+ )
53
+ .map((pin) =>
54
+ SchematicNetlistBuilder.#annotatePinComponentDesignator(
55
+ pin,
56
+ schematic.componentDesignatorsByOwnerIndex
57
+ )
50
58
  )
51
- )
52
59
  const ports = (schematic.ports || []).filter((port) =>
53
60
  SchematicNetlistBuilder.#groupContainsPoint(
54
61
  group,
@@ -89,6 +96,11 @@ export class SchematicNetlistBuilder {
89
96
  ]
90
97
  const name =
91
98
  explicitNames[0] || 'UnknownNet' + String(unknownNetIndex++)
99
+ const implicitNaming =
100
+ SchematicNetlistBuilder.#deriveImplicitNetNaming({
101
+ explicitNames,
102
+ pins
103
+ })
92
104
 
93
105
  if (explicitNames.length > 1) {
94
106
  diagnostics.push({
@@ -102,6 +114,7 @@ export class SchematicNetlistBuilder {
102
114
 
103
115
  return {
104
116
  name,
117
+ ...implicitNaming,
105
118
  segments: group,
106
119
  labels,
107
120
  powerPorts,
@@ -120,7 +133,257 @@ export class SchematicNetlistBuilder {
120
133
  }
121
134
  })
122
135
 
123
- return { nets, diagnostics }
136
+ return {
137
+ nets: SchematicNetlistBuilder.#mergeMatchingPowerPortNets(nets),
138
+ diagnostics
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Adds a component designator to a net pin when owner metadata resolves it.
144
+ * @param {object} pin Net pin.
145
+ * @param {Map<string, string> | Record<string, string> | undefined} componentDesignatorsByOwnerIndex Component designators by native owner id.
146
+ * @returns {object}
147
+ */
148
+ static #annotatePinComponentDesignator(
149
+ pin,
150
+ componentDesignatorsByOwnerIndex
151
+ ) {
152
+ const componentDesignator =
153
+ pin?.componentDesignator ||
154
+ SchematicNetlistBuilder.#lookupComponentDesignator(
155
+ componentDesignatorsByOwnerIndex,
156
+ pin?.ownerIndex
157
+ )
158
+
159
+ if (!componentDesignator) {
160
+ return pin
161
+ }
162
+
163
+ return {
164
+ ...pin,
165
+ componentDesignator
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Derives non-breaking names for unnamed nets from attached pins.
171
+ * @param {{ explicitNames: string[], pins: object[] }} options Naming inputs.
172
+ * @returns {{ autoName?: string, autoNameSource?: string, aliasCandidates?: string[] }}
173
+ */
174
+ static #deriveImplicitNetNaming(options) {
175
+ if ((options.explicitNames || []).length) {
176
+ return {}
177
+ }
178
+
179
+ const candidates = SchematicNetlistBuilder.#pinAliasCandidates(
180
+ options.pins || []
181
+ )
182
+
183
+ if (!candidates.length) {
184
+ return {}
185
+ }
186
+
187
+ return {
188
+ autoName: candidates[0].name,
189
+ autoNameSource: candidates[0].source,
190
+ aliasCandidates: candidates.map((candidate) => candidate.name)
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Builds deterministic alias candidates from connected pins.
196
+ * @param {object[]} pins Connected pins.
197
+ * @returns {{ name: string, source: string }[]}
198
+ */
199
+ static #pinAliasCandidates(pins) {
200
+ const candidates = []
201
+
202
+ for (const pin of pins || []) {
203
+ const componentDesignator = String(
204
+ pin?.componentDesignator || ''
205
+ ).trim()
206
+ const ownerIndex = String(pin?.ownerIndex || '').trim()
207
+ const pinDesignator = String(pin?.designator || '').trim()
208
+ const pinName = String(pin?.name || '').trim()
209
+
210
+ if (componentDesignator) {
211
+ SchematicNetlistBuilder.#pushPinAliasCandidate(
212
+ candidates,
213
+ componentDesignator,
214
+ pinDesignator,
215
+ 'component-pin'
216
+ )
217
+ SchematicNetlistBuilder.#pushPinAliasCandidate(
218
+ candidates,
219
+ componentDesignator,
220
+ pinName,
221
+ 'component-pin-name'
222
+ )
223
+ continue
224
+ }
225
+
226
+ if (ownerIndex) {
227
+ SchematicNetlistBuilder.#pushPinAliasCandidate(
228
+ candidates,
229
+ 'owner-' + ownerIndex,
230
+ pinDesignator,
231
+ 'owner-pin'
232
+ )
233
+ SchematicNetlistBuilder.#pushPinAliasCandidate(
234
+ candidates,
235
+ 'owner-' + ownerIndex,
236
+ pinName,
237
+ 'owner-pin-name'
238
+ )
239
+ continue
240
+ }
241
+
242
+ if (pinName) {
243
+ candidates.push({
244
+ name: 'pin.' + pinName,
245
+ source: 'pin-name'
246
+ })
247
+ }
248
+ }
249
+
250
+ return SchematicNetlistBuilder.#dedupeAliasCandidates(candidates)
251
+ }
252
+
253
+ /**
254
+ * Adds one scoped pin alias candidate.
255
+ * @param {{ name: string, source: string }[]} candidates Alias candidates.
256
+ * @param {string} scope Alias scope.
257
+ * @param {string} pinLabel Pin designator or name.
258
+ * @param {string} source Alias source label.
259
+ * @returns {void}
260
+ */
261
+ static #pushPinAliasCandidate(candidates, scope, pinLabel, source) {
262
+ const trimmedScope = String(scope || '').trim()
263
+ const trimmedPinLabel = String(pinLabel || '').trim()
264
+
265
+ if (!trimmedScope || !trimmedPinLabel) {
266
+ return
267
+ }
268
+
269
+ candidates.push({
270
+ name: trimmedScope + '.' + trimmedPinLabel,
271
+ source
272
+ })
273
+ }
274
+
275
+ /**
276
+ * Deduplicates alias candidates by visible alias name.
277
+ * @param {{ name: string, source: string }[]} candidates Alias candidates.
278
+ * @returns {{ name: string, source: string }[]}
279
+ */
280
+ static #dedupeAliasCandidates(candidates) {
281
+ const seen = new Set()
282
+ const uniqueCandidates = []
283
+
284
+ for (const candidate of candidates || []) {
285
+ if (!candidate.name || seen.has(candidate.name)) {
286
+ continue
287
+ }
288
+
289
+ seen.add(candidate.name)
290
+ uniqueCandidates.push(candidate)
291
+ }
292
+
293
+ return uniqueCandidates
294
+ }
295
+
296
+ /**
297
+ * Looks up a component designator by native owner id.
298
+ * @param {Map<string, string> | Record<string, string> | undefined} componentDesignatorsByOwnerIndex Component designators by native owner id.
299
+ * @param {string | undefined} ownerIndex Native owner id.
300
+ * @returns {string}
301
+ */
302
+ static #lookupComponentDesignator(
303
+ componentDesignatorsByOwnerIndex,
304
+ ownerIndex
305
+ ) {
306
+ const key = String(ownerIndex || '').trim()
307
+ if (!key || !componentDesignatorsByOwnerIndex) {
308
+ return ''
309
+ }
310
+
311
+ if (componentDesignatorsByOwnerIndex instanceof Map) {
312
+ return String(componentDesignatorsByOwnerIndex.get(key) || '')
313
+ }
314
+
315
+ return String(componentDesignatorsByOwnerIndex[key] || '')
316
+ }
317
+
318
+ /**
319
+ * Merges physically separate net groups that share one power-port label.
320
+ * @param {{ name: string, powerPorts: { text?: string }[] }[]} nets Source nets.
321
+ * @returns {object[]}
322
+ */
323
+ static #mergeMatchingPowerPortNets(nets) {
324
+ const mergedNets = []
325
+ const netsByPowerName = new Map()
326
+
327
+ for (const net of nets) {
328
+ const powerName =
329
+ SchematicNetlistBuilder.#singlePowerPortNetName(net)
330
+
331
+ if (!powerName) {
332
+ mergedNets.push(net)
333
+ continue
334
+ }
335
+
336
+ const existingNet = netsByPowerName.get(powerName)
337
+ if (!existingNet) {
338
+ netsByPowerName.set(powerName, net)
339
+ mergedNets.push(net)
340
+ continue
341
+ }
342
+
343
+ SchematicNetlistBuilder.#appendNet(existingNet, net)
344
+ }
345
+
346
+ return mergedNets
347
+ }
348
+
349
+ /**
350
+ * Returns one normalized power-port name when every port agrees.
351
+ * @param {{ powerPorts?: { text?: string }[] }} net Source net.
352
+ * @returns {string}
353
+ */
354
+ static #singlePowerPortNetName(net) {
355
+ const names = [
356
+ ...new Set(
357
+ (net.powerPorts || [])
358
+ .map((powerPort) => String(powerPort.text || '').trim())
359
+ .filter(Boolean)
360
+ .map((name) => name.toUpperCase())
361
+ )
362
+ ]
363
+
364
+ return names.length === 1 ? names[0] : ''
365
+ }
366
+
367
+ /**
368
+ * Appends one net's observable members to an existing logical net.
369
+ * @param {object} target Existing logical net.
370
+ * @param {object} source Source net to append.
371
+ * @returns {void}
372
+ */
373
+ static #appendNet(target, source) {
374
+ for (const key of [
375
+ 'segments',
376
+ 'labels',
377
+ 'powerPorts',
378
+ 'crossSheetConnectors',
379
+ 'pins',
380
+ 'ports',
381
+ 'junctions',
382
+ 'busEntries',
383
+ 'sheetEntries'
384
+ ]) {
385
+ target[key].push(...(source[key] || []))
386
+ }
124
387
  }
125
388
 
126
389
  /**
@@ -0,0 +1,36 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Resolves Altium No ERC marker symbol identifiers into stable names.
7
+ */
8
+ export class SchematicNoErcSymbolResolver {
9
+ /**
10
+ * Converts common No ERC symbol ids and names into public labels.
11
+ * @param {number | string | null | undefined} symbol Symbol id or source name.
12
+ * @returns {string}
13
+ */
14
+ static resolveSymbolName(symbol) {
15
+ const text = String(symbol ?? '')
16
+ .trim()
17
+ .toLowerCase()
18
+
19
+ if (text) {
20
+ if (/check\s*box/.test(text)) return 'checkbox'
21
+ if (/cross/.test(text)) return 'cross'
22
+ if (/triangle/.test(text)) return 'triangle'
23
+ if (/box/.test(text)) return 'box'
24
+ if (/generic/.test(text)) return 'generic'
25
+ }
26
+
27
+ return (
28
+ {
29
+ 0: 'generic',
30
+ 1: 'box',
31
+ 2: 'cross',
32
+ 3: 'triangle'
33
+ }[Number(symbol)] || 'unknown'
34
+ )
35
+ }
36
+ }
@@ -15,7 +15,7 @@ export class SchematicOwnershipGraphParser {
15
15
  /**
16
16
  * Parses record parent/child links from raw schematic records.
17
17
  * @param {{ fields: Record<string, string | string[]>, recordIndex?: number }[]} records Schematic records.
18
- * @returns {{ schema: string, records: object[], recordsByIndexInSheet: Record<string, object>, childrenByParentKey: Record<string, object[]>, parentsByChildKey: Record<string, object> }}
18
+ * @returns {{ schema: string, records: object[], hierarchy: object[], recordsByRecordIndex: Record<string, object>, recordsByIndexInSheet: Record<string, object>, childrenByParentKey: Record<string, object[]>, parentsByChildKey: Record<string, object> }}
19
19
  */
20
20
  static parse(records) {
21
21
  const entries = (records || []).map((record, fallbackIndex) =>
@@ -54,6 +54,12 @@ export class SchematicOwnershipGraphParser {
54
54
  records: entries.map((entry) =>
55
55
  SchematicOwnershipGraphParser.#publicRecord(entry)
56
56
  ),
57
+ hierarchy: SchematicOwnershipGraphParser.#hierarchy(
58
+ entries,
59
+ parentsByChildKey
60
+ ),
61
+ recordsByRecordIndex:
62
+ SchematicOwnershipGraphParser.#recordsByRecordIndex(entries),
57
63
  recordsByIndexInSheet:
58
64
  SchematicOwnershipGraphParser.#recordsByIndexInSheet(entries),
59
65
  childrenByParentKey,
@@ -61,6 +67,63 @@ export class SchematicOwnershipGraphParser {
61
67
  }
62
68
  }
63
69
 
70
+ /**
71
+ * Builds a nested public hierarchy from resolved parent links.
72
+ * @param {object[]} entries Record sidecar rows.
73
+ * @param {Record<string, { parentKey: string }>} parentsByChildKey Resolved parent links.
74
+ * @returns {object[]}
75
+ */
76
+ static #hierarchy(entries, parentsByChildKey) {
77
+ const entriesByKey = new Map(entries.map((entry) => [entry.key, entry]))
78
+ const childrenByParentKey = new Map()
79
+ const childKeys = new Set()
80
+
81
+ for (const [childKey, parentLink] of Object.entries(
82
+ parentsByChildKey
83
+ )) {
84
+ const child = entriesByKey.get(childKey)
85
+ const parent = entriesByKey.get(parentLink.parentKey)
86
+
87
+ if (!child || !parent) {
88
+ continue
89
+ }
90
+
91
+ childKeys.add(childKey)
92
+ if (!childrenByParentKey.has(parent.key)) {
93
+ childrenByParentKey.set(parent.key, [])
94
+ }
95
+
96
+ childrenByParentKey.get(parent.key).push(child)
97
+ }
98
+
99
+ return entries
100
+ .filter((entry) => !childKeys.has(entry.key))
101
+ .map((entry) =>
102
+ SchematicOwnershipGraphParser.#hierarchyNode(
103
+ entry,
104
+ childrenByParentKey
105
+ )
106
+ )
107
+ }
108
+
109
+ /**
110
+ * Builds one nested hierarchy node.
111
+ * @param {object} entry Record sidecar row.
112
+ * @param {Map<string, object[]>} childrenByParentKey Child rows by parent key.
113
+ * @returns {object}
114
+ */
115
+ static #hierarchyNode(entry, childrenByParentKey) {
116
+ return {
117
+ ...SchematicOwnershipGraphParser.#publicRecord(entry),
118
+ children: (childrenByParentKey.get(entry.key) || []).map((child) =>
119
+ SchematicOwnershipGraphParser.#hierarchyNode(
120
+ child,
121
+ childrenByParentKey
122
+ )
123
+ )
124
+ }
125
+ }
126
+
64
127
  /**
65
128
  * Normalizes one raw schematic record into a sidecar row.
66
129
  * @param {{ fields: Record<string, string | string[]>, recordIndex?: number }} record Source record.
@@ -83,7 +146,8 @@ export class SchematicOwnershipGraphParser {
83
146
  getField(record?.fields, 'UniqueID') ||
84
147
  getField(record?.fields, 'UniqueId'),
85
148
  name: getField(record?.fields, 'Name'),
86
- text: getField(record?.fields, 'Text')
149
+ text: getField(record?.fields, 'Text'),
150
+ fields: SchematicOwnershipGraphParser.#publicFields(record?.fields)
87
151
  }
88
152
  }
89
153
 
@@ -139,7 +203,8 @@ export class SchematicOwnershipGraphParser {
139
203
  ownerIndex: entry.ownerIndex,
140
204
  uniqueId: entry.uniqueId,
141
205
  name: entry.name,
142
- text: entry.text
206
+ text: entry.text,
207
+ fields: entry.fields
143
208
  })
144
209
  }
145
210
 
@@ -159,6 +224,22 @@ export class SchematicOwnershipGraphParser {
159
224
  })
160
225
  }
161
226
 
227
+ /**
228
+ * Indexes records by parser record index.
229
+ * @param {object[]} entries Record sidecar rows.
230
+ * @returns {Record<string, object>}
231
+ */
232
+ static #recordsByRecordIndex(entries) {
233
+ const index = {}
234
+
235
+ for (const entry of entries) {
236
+ index[String(entry.recordIndex)] =
237
+ SchematicOwnershipGraphParser.#publicRecord(entry)
238
+ }
239
+
240
+ return index
241
+ }
242
+
162
243
  /**
163
244
  * Indexes records by native IndexInSheet values.
164
245
  * @param {object[]} entries Record sidecar rows.
@@ -177,6 +258,24 @@ export class SchematicOwnershipGraphParser {
177
258
  return index
178
259
  }
179
260
 
261
+ /**
262
+ * Copies source fields into a stable plain object for read-only consumers.
263
+ * @param {Record<string, string | string[]> | undefined} fields Source fields.
264
+ * @returns {Record<string, string | string[]>}
265
+ */
266
+ static #publicFields(fields) {
267
+ if (!fields || typeof fields !== 'object') {
268
+ return {}
269
+ }
270
+
271
+ return Object.fromEntries(
272
+ Object.entries(fields).map(([key, value]) => [
273
+ key,
274
+ Array.isArray(value) ? [...value] : value
275
+ ])
276
+ )
277
+ }
278
+
180
279
  /**
181
280
  * Drops empty object fields while preserving numeric zero.
182
281
  * @param {Record<string, unknown>} value Candidate object.