altium-toolkit 1.0.8 → 1.0.9

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 (88) 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/netlist_a1.schema.json +47 -0
  5. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1661 -104
  6. package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +59 -0
  7. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +57 -0
  8. package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
  9. package/docs/testing.md +9 -3
  10. package/package.json +1 -1
  11. package/spec/library-scope.md +7 -1
  12. package/src/core/altium/AltiumLayoutParser.mjs +104 -8
  13. package/src/core/altium/AltiumParser.mjs +191 -45
  14. package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
  15. package/src/core/altium/IntLibModelParser.mjs +240 -0
  16. package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
  17. package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
  18. package/src/core/altium/LibrarySearchIndex.mjs +215 -0
  19. package/src/core/altium/NormalizedModelSchema.mjs +36 -0
  20. package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
  21. package/src/core/altium/PcbDefaultsParser.mjs +171 -0
  22. package/src/core/altium/PcbDimensionParser.mjs +229 -0
  23. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
  24. package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
  25. package/src/core/altium/PcbLibModelParser.mjs +235 -14
  26. package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
  27. package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
  28. package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
  29. package/src/core/altium/PcbModelParser.mjs +466 -28
  30. package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
  31. package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
  32. package/src/core/altium/PcbPadStackParser.mjs +58 -0
  33. package/src/core/altium/PcbPickPlacePositionResolver.mjs +217 -0
  34. package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
  35. package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
  36. package/src/core/altium/PcbRegionPrimitiveParser.mjs +5 -1
  37. package/src/core/altium/PcbRuleParser.mjs +354 -33
  38. package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
  39. package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
  40. package/src/core/altium/PcbStatisticsBuilder.mjs +532 -0
  41. package/src/core/altium/PcbStreamExtractor.mjs +111 -4
  42. package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
  43. package/src/core/altium/PcbUnionParser.mjs +307 -0
  44. package/src/core/altium/PcbViaStackParser.mjs +98 -10
  45. package/src/core/altium/PcbViaStructureParser.mjs +335 -0
  46. package/src/core/altium/PrintableTextDecoder.mjs +53 -3
  47. package/src/core/altium/PrjPcbModelParser.mjs +257 -5
  48. package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
  49. package/src/core/altium/ProjectDesignBundleBuilder.mjs +477 -0
  50. package/src/core/altium/ProjectNetlistExporter.mjs +499 -0
  51. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
  52. package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
  53. package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
  54. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
  55. package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
  56. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
  57. package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
  58. package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
  59. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
  60. package/src/core/altium/SchematicHarnessParser.mjs +302 -0
  61. package/src/core/altium/SchematicImageParser.mjs +474 -3
  62. package/src/core/altium/SchematicImplementationParser.mjs +518 -0
  63. package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
  64. package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
  65. package/src/core/altium/SchematicPinParser.mjs +84 -1
  66. package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
  67. package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
  68. package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
  69. package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
  70. package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
  71. package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
  72. package/src/core/altium/SchematicTemplateParser.mjs +256 -0
  73. package/src/core/altium/SchematicTextParser.mjs +123 -0
  74. package/src/core/ole/OleCompoundDocument.mjs +20 -0
  75. package/src/parser.mjs +29 -0
  76. package/src/styles/altium-renderers.css +19 -0
  77. package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
  78. package/src/ui/PcbInteractionIndex.mjs +9 -4
  79. package/src/ui/PcbScene3dBuilder.mjs +137 -3
  80. package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
  81. package/src/ui/PcbSvgRenderer.mjs +1187 -34
  82. package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
  83. package/src/ui/SchematicNoteRenderer.mjs +9 -2
  84. package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
  85. package/src/ui/SchematicShapeRenderer.mjs +362 -0
  86. package/src/ui/SchematicSvgRenderer.mjs +1442 -92
  87. package/src/ui/SchematicTypography.mjs +48 -5
  88. package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
@@ -0,0 +1,417 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Builds deterministic render/export manifests for read-only library models.
7
+ */
8
+ export class LibraryRenderManifestBuilder {
9
+ static #SCHEMA = 'altium-toolkit.library.render-manifest.a1'
10
+
11
+ /**
12
+ * Builds a schematic-symbol library render manifest.
13
+ * @param {{ symbols?: object[] } | { schematicLibrary?: { symbols?: object[] } }} library Parsed schematic library.
14
+ * @returns {{ schema: string, libraryKind: string, outputs: object[], embeddedAssets: object[] }}
15
+ */
16
+ static buildSchematicLibraryManifest(library) {
17
+ const schematicLibrary = library?.schematicLibrary || library || {}
18
+ const symbols = Array.isArray(schematicLibrary.symbols)
19
+ ? schematicLibrary.symbols
20
+ : []
21
+ const outputs = symbols.flatMap((symbol, symbolIndex) =>
22
+ LibraryRenderManifestBuilder.#schematicSymbolOutputs(
23
+ symbol,
24
+ symbolIndex
25
+ )
26
+ )
27
+
28
+ return {
29
+ schema: LibraryRenderManifestBuilder.#SCHEMA,
30
+ libraryKind: 'schematic-symbols',
31
+ outputs,
32
+ embeddedAssets: LibraryRenderManifestBuilder.#dedupeEmbeddedAssets(
33
+ outputs.flatMap((output) => output.embeddedAssets || [])
34
+ )
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Builds a PCB footprint-library render manifest.
40
+ * @param {{ footprints?: object[], embeddedAssets?: object[] } | { pcbLibrary?: { footprints?: object[], embeddedAssets?: object[] } }} library Parsed PCB library.
41
+ * @returns {{ schema: string, libraryKind: string, outputs: object[], embeddedAssets: object[] }}
42
+ */
43
+ static buildPcbLibraryManifest(library) {
44
+ const pcbLibrary = library?.pcbLibrary || library || {}
45
+ const footprints = Array.isArray(pcbLibrary.footprints)
46
+ ? pcbLibrary.footprints
47
+ : []
48
+ const outputs = footprints.map((footprint, footprintIndex) =>
49
+ LibraryRenderManifestBuilder.#pcbFootprintOutput(
50
+ footprint,
51
+ footprintIndex
52
+ )
53
+ )
54
+
55
+ return {
56
+ schema: LibraryRenderManifestBuilder.#SCHEMA,
57
+ libraryKind: 'pcb-footprints',
58
+ outputs,
59
+ embeddedAssets: LibraryRenderManifestBuilder.#dedupeEmbeddedAssets([
60
+ ...(pcbLibrary.embeddedAssets || []),
61
+ ...outputs.flatMap((output) => output.embeddedAssets || [])
62
+ ])
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Builds a read-only manifest for symbols extractable from placed
68
+ * schematic components.
69
+ * @param {{ fileName?: string, schematic?: object } | { schematic?: object }} documentModel Parsed schematic document model.
70
+ * @returns {{ schema: string, sourceDocument: string, outputs: object[], embeddedAssets: object[] }}
71
+ */
72
+ static buildSchematicExtractionManifest(documentModel) {
73
+ const schematic = documentModel?.schematic || documentModel || {}
74
+ const components = Array.isArray(schematic.components)
75
+ ? schematic.components
76
+ : []
77
+ const outputs = components.map((component, componentIndex) =>
78
+ LibraryRenderManifestBuilder.#schematicExtractionOutput(
79
+ documentModel?.fileName || '',
80
+ schematic,
81
+ component,
82
+ componentIndex
83
+ )
84
+ )
85
+
86
+ return {
87
+ schema: 'altium-toolkit.schematic.extraction-manifest.a1',
88
+ sourceDocument: String(documentModel?.fileName || ''),
89
+ outputs,
90
+ embeddedAssets: LibraryRenderManifestBuilder.#dedupeEmbeddedAssets(
91
+ outputs.flatMap((output) => output.embeddedAssets || [])
92
+ )
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Builds render outputs for one schematic symbol.
98
+ * @param {object} symbol Symbol record.
99
+ * @param {number} symbolIndex Symbol index.
100
+ * @returns {object[]}
101
+ */
102
+ static #schematicSymbolOutputs(symbol, symbolIndex) {
103
+ const symbolKey =
104
+ 'symbol-' +
105
+ symbolIndex +
106
+ '-' +
107
+ LibraryRenderManifestBuilder.#slug(symbol?.name || symbolIndex)
108
+ const parts =
109
+ Array.isArray(symbol?.parts) && symbol.parts.length
110
+ ? symbol.parts
111
+ : [{ partId: 'default' }]
112
+ const embeddedAssets =
113
+ LibraryRenderManifestBuilder.#embeddedAssets(symbol)
114
+
115
+ return parts.map((part, partIndex) => {
116
+ const partId = String(part?.partId || part?.id || partIndex)
117
+ const partKey =
118
+ symbolKey +
119
+ '/part-' +
120
+ LibraryRenderManifestBuilder.#slug(partId)
121
+
122
+ return {
123
+ kind: 'symbol',
124
+ symbolKey,
125
+ name: String(symbol?.name || ''),
126
+ partKey,
127
+ partId,
128
+ outputSvgKey: 'schematic-library/' + partKey + '.svg',
129
+ embeddedAssets
130
+ }
131
+ })
132
+ }
133
+
134
+ /**
135
+ * Builds one PCB footprint output descriptor.
136
+ * @param {object} footprint Footprint record.
137
+ * @param {number} footprintIndex Footprint index.
138
+ * @returns {object}
139
+ */
140
+ static #pcbFootprintOutput(footprint, footprintIndex) {
141
+ const footprintKey =
142
+ 'footprint-' +
143
+ footprintIndex +
144
+ '-' +
145
+ LibraryRenderManifestBuilder.#slug(
146
+ footprint?.name || footprintIndex
147
+ )
148
+ const layerSvgs = LibraryRenderManifestBuilder.#footprintLayers(
149
+ footprint
150
+ ).map((layer) => ({
151
+ layerKey: layer.layerKey,
152
+ layerId: layer.layerId,
153
+ displayName: layer.displayName,
154
+ outputSvgKey:
155
+ 'pcb-library/' + footprintKey + '/' + layer.layerKey + '.svg'
156
+ }))
157
+
158
+ return {
159
+ kind: 'footprint',
160
+ footprintKey,
161
+ name: String(footprint?.name || ''),
162
+ sourceStorage: String(footprint?.sourceStorage || ''),
163
+ outputSvgKey: 'pcb-library/' + footprintKey + '.svg',
164
+ layerSvgs,
165
+ embeddedAssets:
166
+ LibraryRenderManifestBuilder.#embeddedAssets(footprint)
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Builds one placed-component extraction output.
172
+ * @param {string} sourceDocument Source document name.
173
+ * @param {object} schematic Schematic model.
174
+ * @param {object} component Component row.
175
+ * @param {number} componentIndex Component index.
176
+ * @returns {object}
177
+ */
178
+ static #schematicExtractionOutput(
179
+ sourceDocument,
180
+ schematic,
181
+ component,
182
+ componentIndex
183
+ ) {
184
+ const symbolName =
185
+ component?.libReference || component?.designator || ''
186
+ const symbolKey =
187
+ 'symbol-extract-' +
188
+ componentIndex +
189
+ '-' +
190
+ LibraryRenderManifestBuilder.#slug(symbolName || componentIndex)
191
+ const ownerIndex = String(component?.ownerIndex || '').trim()
192
+ const children = LibraryRenderManifestBuilder.#schematicOwnerChildren(
193
+ schematic,
194
+ ownerIndex
195
+ )
196
+ const embeddedAssets =
197
+ LibraryRenderManifestBuilder.#dedupeEmbeddedAssets(
198
+ children.images.map((image, index) =>
199
+ LibraryRenderManifestBuilder.#imageAssetDescriptor(
200
+ image,
201
+ 'symbol-image-' + index
202
+ )
203
+ )
204
+ )
205
+
206
+ return {
207
+ kind: 'symbol-extraction',
208
+ symbolKey,
209
+ sourceComponent: LibraryRenderManifestBuilder.#stripUndefined({
210
+ designator: component?.designator,
211
+ libReference: component?.libReference,
212
+ uniqueId: component?.uniqueId,
213
+ ownerIndex
214
+ }),
215
+ outputLibraryKey: 'schematic-extract/' + symbolKey + '.SchLib',
216
+ renderManifestKey:
217
+ 'schematic-extract/' + symbolKey + '.render.json',
218
+ partKeys: [symbolKey + '/part-default'],
219
+ childCounts: {
220
+ pins: children.pins.length,
221
+ graphics: children.graphics.length,
222
+ texts: children.texts.length,
223
+ images: children.images.length
224
+ },
225
+ embeddedAssets
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Collects schematic primitives owned by one component owner index.
231
+ * @param {object} schematic Schematic model.
232
+ * @param {string} ownerIndex Owner index.
233
+ * @returns {{ pins: object[], graphics: object[], texts: object[], images: object[] }}
234
+ */
235
+ static #schematicOwnerChildren(schematic, ownerIndex) {
236
+ const ownerMatches = (item) =>
237
+ ownerIndex && String(item?.ownerIndex || '').trim() === ownerIndex
238
+ const graphics = [
239
+ ...(schematic?.lines || []),
240
+ ...(schematic?.polygons || []),
241
+ ...(schematic?.rectangles || []),
242
+ ...(schematic?.ellipses || []),
243
+ ...(schematic?.arcs || []),
244
+ ...(schematic?.beziers || []),
245
+ ...(schematic?.pies || []),
246
+ ...(schematic?.regions || [])
247
+ ].filter(ownerMatches)
248
+
249
+ return {
250
+ pins: (schematic?.pins || []).filter(ownerMatches),
251
+ graphics,
252
+ texts: (schematic?.texts || []).filter(ownerMatches),
253
+ images: (schematic?.images || []).filter(ownerMatches)
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Collects layer descriptors touched by one footprint.
259
+ * @param {object} footprint Footprint record.
260
+ * @returns {{ layerKey: string, layerId?: number, displayName: string }[]}
261
+ */
262
+ static #footprintLayers(footprint) {
263
+ const layerMap = new Map()
264
+ const primitiveFamilies = [
265
+ 'pads',
266
+ 'tracks',
267
+ 'arcs',
268
+ 'vias',
269
+ 'fills',
270
+ 'texts',
271
+ 'regions',
272
+ 'shapeBasedRegions'
273
+ ]
274
+
275
+ for (const family of primitiveFamilies) {
276
+ for (const primitive of footprint?.[family] || []) {
277
+ const layer =
278
+ LibraryRenderManifestBuilder.#layerDescriptor(primitive)
279
+ if (layer) {
280
+ layerMap.set(layer.layerKey, layer)
281
+ }
282
+ }
283
+ }
284
+
285
+ return [...layerMap.values()].sort((left, right) =>
286
+ left.layerKey.localeCompare(right.layerKey)
287
+ )
288
+ }
289
+
290
+ /**
291
+ * Builds a normalized layer descriptor for one primitive.
292
+ * @param {object} primitive Primitive record.
293
+ * @returns {{ layerKey: string, layerId?: number, displayName: string } | null}
294
+ */
295
+ static #layerDescriptor(primitive) {
296
+ const layerId = Number.isInteger(primitive?.layerId)
297
+ ? primitive.layerId
298
+ : null
299
+ const layerName = String(
300
+ primitive?.layerName || primitive?.layer || ''
301
+ ).trim()
302
+
303
+ if (layerId === null && !layerName) {
304
+ return null
305
+ }
306
+
307
+ const layerKey =
308
+ layerId === null
309
+ ? 'layer-' + LibraryRenderManifestBuilder.#slug(layerName)
310
+ : 'L' + layerId
311
+
312
+ return {
313
+ layerKey,
314
+ ...(layerId === null ? {} : { layerId }),
315
+ displayName: layerName || layerKey
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Collects embedded assets from a library item.
321
+ * @param {object} item Library item.
322
+ * @returns {object[]}
323
+ */
324
+ static #embeddedAssets(item) {
325
+ return [
326
+ ...(Array.isArray(item?.embeddedAssets) ? item.embeddedAssets : []),
327
+ ...(Array.isArray(item?.embeddedModels)
328
+ ? item.embeddedModels.map((model, index) => ({
329
+ key: model.key || model.id || 'model-' + index,
330
+ format: model.format,
331
+ sourceStream: model.sourceStream,
332
+ name: model.name
333
+ }))
334
+ : []),
335
+ ...(Array.isArray(item?.embeddedFonts)
336
+ ? item.embeddedFonts.map((font, index) => ({
337
+ key: font.key || font.name || 'font-' + index,
338
+ format: 'font',
339
+ sourceStream: font.sourceStream,
340
+ name: font.name
341
+ }))
342
+ : [])
343
+ ].map((asset) =>
344
+ LibraryRenderManifestBuilder.#stripUndefined(asset || {})
345
+ )
346
+ }
347
+
348
+ /**
349
+ * Builds an asset descriptor for a schematic image payload.
350
+ * @param {object} image Schematic image record.
351
+ * @param {string} fallbackKey Fallback asset key.
352
+ * @returns {object}
353
+ */
354
+ static #imageAssetDescriptor(image, fallbackKey) {
355
+ return LibraryRenderManifestBuilder.#stripUndefined({
356
+ key: image?.key || image?.id || fallbackKey,
357
+ format: image?.format,
358
+ nativeFormat: image?.nativeFormat,
359
+ wrapperType: image?.wrapperType,
360
+ byteSize: image?.byteSize,
361
+ checksum: image?.checksum,
362
+ sourceStream: image?.sourceStream,
363
+ name: image?.name,
364
+ diagnostics: image?.diagnostics
365
+ })
366
+ }
367
+
368
+ /**
369
+ * Deduplicates embedded asset descriptors.
370
+ * @param {object[]} assets Asset descriptors.
371
+ * @returns {object[]}
372
+ */
373
+ static #dedupeEmbeddedAssets(assets) {
374
+ const seen = new Set()
375
+ const deduped = []
376
+
377
+ for (const asset of assets || []) {
378
+ const key = JSON.stringify(asset)
379
+ if (seen.has(key)) {
380
+ continue
381
+ }
382
+
383
+ seen.add(key)
384
+ deduped.push(asset)
385
+ }
386
+
387
+ return deduped
388
+ }
389
+
390
+ /**
391
+ * Removes undefined values from one object.
392
+ * @param {object} value Source object.
393
+ * @returns {object}
394
+ */
395
+ static #stripUndefined(value) {
396
+ return Object.fromEntries(
397
+ Object.entries(value).filter(
398
+ ([, entryValue]) => entryValue !== undefined
399
+ )
400
+ )
401
+ }
402
+
403
+ /**
404
+ * Converts a display value to a deterministic lowercase key segment.
405
+ * @param {unknown} value Source value.
406
+ * @returns {string}
407
+ */
408
+ static #slug(value) {
409
+ return (
410
+ String(value || '')
411
+ .trim()
412
+ .toLowerCase()
413
+ .replace(/[^a-z0-9]+/gu, '-')
414
+ .replace(/^-+|-+$/gu, '') || 'item'
415
+ )
416
+ }
417
+ }
@@ -0,0 +1,215 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Provides exact and lightweight fuzzy lookups over parsed library read models.
7
+ */
8
+ export class LibrarySearchIndex {
9
+ /**
10
+ * Searches PCB footprint records.
11
+ * @param {{ footprints?: object[] } | { pcbLibrary?: { footprints?: object[] } }} library Parsed PCB library.
12
+ * @param {string} query Search query.
13
+ * @param {{ limit?: number }} options Search options.
14
+ * @returns {{ query: string, matches: object[] }}
15
+ */
16
+ static searchPcbFootprints(library, query, options = {}) {
17
+ const pcbLibrary = library?.pcbLibrary || library || {}
18
+ const footprints = Array.isArray(pcbLibrary.footprints)
19
+ ? pcbLibrary.footprints
20
+ : []
21
+
22
+ return LibrarySearchIndex.#searchCollection(
23
+ 'footprint',
24
+ footprints,
25
+ query,
26
+ options
27
+ )
28
+ }
29
+
30
+ /**
31
+ * Searches schematic symbol records.
32
+ * @param {{ symbols?: object[] } | { schematicLibrary?: { symbols?: object[] } }} library Parsed schematic library.
33
+ * @param {string} query Search query.
34
+ * @param {{ limit?: number }} options Search options.
35
+ * @returns {{ query: string, matches: object[] }}
36
+ */
37
+ static searchSchematicSymbols(library, query, options = {}) {
38
+ const schematicLibrary = library?.schematicLibrary || library || {}
39
+ const symbols = Array.isArray(schematicLibrary.symbols)
40
+ ? schematicLibrary.symbols
41
+ : []
42
+
43
+ return LibrarySearchIndex.#searchCollection(
44
+ 'symbol',
45
+ symbols,
46
+ query,
47
+ options
48
+ )
49
+ }
50
+
51
+ /**
52
+ * Searches one library collection.
53
+ * @param {string} kind Public item kind.
54
+ * @param {object[]} items Library items.
55
+ * @param {string} query Search query.
56
+ * @param {{ limit?: number }} options Search options.
57
+ * @returns {{ query: string, matches: object[] }}
58
+ */
59
+ static #searchCollection(kind, items, query, options) {
60
+ const normalizedQuery = LibrarySearchIndex.#normalize(query)
61
+ const limit = Math.max(Number(options.limit || 25), 1)
62
+ const matches = (items || [])
63
+ .map((item, index) =>
64
+ LibrarySearchIndex.#scoreItem(
65
+ kind,
66
+ item,
67
+ index,
68
+ normalizedQuery
69
+ )
70
+ )
71
+ .filter(Boolean)
72
+ .sort(
73
+ (left, right) =>
74
+ left.score - right.score ||
75
+ left.name.localeCompare(right.name)
76
+ )
77
+ .slice(0, limit)
78
+
79
+ return {
80
+ query: String(query || ''),
81
+ matches
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Scores one candidate library item.
87
+ * @param {string} kind Public item kind.
88
+ * @param {object} item Library item.
89
+ * @param {number} index Source index.
90
+ * @param {string} normalizedQuery Normalized query.
91
+ * @returns {object | null}
92
+ */
93
+ static #scoreItem(kind, item, index, normalizedQuery) {
94
+ if (!normalizedQuery) {
95
+ return null
96
+ }
97
+
98
+ const name = String(item?.name || item?.libReference || '')
99
+ const normalizedName = LibrarySearchIndex.#normalize(name)
100
+ const keywords = LibrarySearchIndex.#keywords(item)
101
+ const match = LibrarySearchIndex.#match(
102
+ normalizedQuery,
103
+ normalizedName,
104
+ keywords
105
+ )
106
+
107
+ if (!match) {
108
+ return null
109
+ }
110
+
111
+ return {
112
+ kind,
113
+ name,
114
+ index,
115
+ score: match.score,
116
+ matchKind: match.matchKind,
117
+ keywords
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Matches a normalized query against name and keywords.
123
+ * @param {string} query Normalized query.
124
+ * @param {string} name Normalized name.
125
+ * @param {string[]} keywords Keyword list.
126
+ * @returns {{ score: number, matchKind: string } | null}
127
+ */
128
+ static #match(query, name, keywords) {
129
+ const normalizedKeywords = keywords.map((keyword) =>
130
+ LibrarySearchIndex.#normalize(keyword)
131
+ )
132
+ const compactQuery = LibrarySearchIndex.#compact(query)
133
+ const compactName = LibrarySearchIndex.#compact(name)
134
+
135
+ if (name === query) {
136
+ return { score: 0, matchKind: 'exact' }
137
+ }
138
+ if (name.startsWith(query)) {
139
+ return { score: 10, matchKind: 'prefix' }
140
+ }
141
+ if (name.includes(query)) {
142
+ return { score: 20, matchKind: 'substring' }
143
+ }
144
+ if (normalizedKeywords.some((keyword) => keyword.includes(query))) {
145
+ return { score: 30, matchKind: 'keyword' }
146
+ }
147
+ if (
148
+ compactName.includes(compactQuery) ||
149
+ LibrarySearchIndex.#isOrderedSubsequence(compactQuery, compactName)
150
+ ) {
151
+ return { score: 40, matchKind: 'fuzzy' }
152
+ }
153
+
154
+ return null
155
+ }
156
+
157
+ /**
158
+ * Builds keywords from item metadata.
159
+ * @param {object} item Library item.
160
+ * @returns {string[]}
161
+ */
162
+ static #keywords(item) {
163
+ return [
164
+ item?.name,
165
+ item?.dataName,
166
+ item?.sourceStorage,
167
+ ...Object.values(item?.parameters || {}),
168
+ ...Object.values(item?.componentParams || {}),
169
+ ...Object.values(item?.componentParams?.properties || {})
170
+ ]
171
+ .map((value) => String(value || '').trim())
172
+ .filter(Boolean)
173
+ }
174
+
175
+ /**
176
+ * Returns true when all query chars appear in order in the candidate.
177
+ * @param {string} query Normalized compact query.
178
+ * @param {string} candidate Normalized compact candidate.
179
+ * @returns {boolean}
180
+ */
181
+ static #isOrderedSubsequence(query, candidate) {
182
+ let cursor = 0
183
+
184
+ for (const char of candidate) {
185
+ if (char === query[cursor]) {
186
+ cursor += 1
187
+ }
188
+ if (cursor === query.length) {
189
+ return true
190
+ }
191
+ }
192
+
193
+ return false
194
+ }
195
+
196
+ /**
197
+ * Normalizes text for case-insensitive lookup.
198
+ * @param {unknown} value Source value.
199
+ * @returns {string}
200
+ */
201
+ static #normalize(value) {
202
+ return String(value || '')
203
+ .trim()
204
+ .toLowerCase()
205
+ }
206
+
207
+ /**
208
+ * Removes separators for fuzzy package-name matching.
209
+ * @param {string} value Normalized value.
210
+ * @returns {string}
211
+ */
212
+ static #compact(value) {
213
+ return String(value || '').replace(/[^a-z0-9]+/gu, '')
214
+ }
215
+ }
@@ -22,7 +22,43 @@ export class NormalizedModelSchema {
22
22
  ...model
23
23
  }
24
24
  normalizedModel.schema = NormalizedModelSchema.CURRENT_SCHEMA_ID
25
+ if (Array.isArray(normalizedModel.diagnostics)) {
26
+ normalizedModel.diagnostics =
27
+ NormalizedModelSchema.#normalizeDiagnostics(
28
+ normalizedModel.diagnostics
29
+ )
30
+ }
25
31
 
26
32
  return normalizedModel
27
33
  }
34
+
35
+ /**
36
+ * Adds machine-readable codes to parser diagnostics.
37
+ * @param {object[]} diagnostics Parser diagnostics.
38
+ * @returns {object[]}
39
+ */
40
+ static #normalizeDiagnostics(diagnostics) {
41
+ return diagnostics.map((diagnostic) => ({
42
+ code:
43
+ typeof diagnostic?.code === 'string' && diagnostic.code
44
+ ? diagnostic.code
45
+ : NormalizedModelSchema.#deriveDiagnosticCode(diagnostic),
46
+ ...diagnostic
47
+ }))
48
+ }
49
+
50
+ /**
51
+ * Derives a stable fallback code from one diagnostic message.
52
+ * @param {object} diagnostic Parser diagnostic.
53
+ * @returns {string}
54
+ */
55
+ static #deriveDiagnosticCode(diagnostic) {
56
+ const slug = String(diagnostic?.message || 'diagnostic')
57
+ .toLowerCase()
58
+ .replace(/[^a-z0-9]+/gu, '.')
59
+ .replace(/^\.+|\.+$/gu, '')
60
+ .slice(0, 80)
61
+
62
+ return 'parser.' + (slug || 'diagnostic')
63
+ }
28
64
  }