circuitjson-toolkit 1.0.16 → 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.
Files changed (155) hide show
  1. package/AGENTS.md +15 -0
  2. package/NOTICE.md +31 -0
  3. package/README.md +267 -107
  4. package/docs/api.md +501 -96
  5. package/docs/capabilities.md +70 -0
  6. package/docs/migration/behaviors.md +45 -0
  7. package/docs/migration/parser.md +60 -0
  8. package/docs/migration/renderers.md +515 -0
  9. package/docs/migration/root.md +740 -0
  10. package/docs/migration.md +120 -0
  11. package/docs/model-format.md +175 -57
  12. package/docs/provenance.md +206 -0
  13. package/docs/release-notes-v1.1.0.md +154 -0
  14. package/docs/testing.md +117 -7
  15. package/package.json +31 -5
  16. package/spec/api-baseline-v1.0.17.json +1 -0
  17. package/spec/baseline-provenance-v1.0.17.json +7 -0
  18. package/spec/circuitjson-schema-snapshot.json +321 -0
  19. package/spec/circuitjson-schema-source.json +28 -0
  20. package/spec/feature-preservation.json +1 -0
  21. package/spec/library-scope.md +27 -20
  22. package/src/capabilities.mjs +1 -0
  23. package/src/core/ArchiveEntryPath.mjs +93 -0
  24. package/src/core/ArchiveLimits.mjs +31 -0
  25. package/src/core/ArchiveLimitsValidator.mjs +107 -0
  26. package/src/core/AsyncInputOwnership.mjs +56 -0
  27. package/src/core/AttachedValueLimits.mjs +67 -0
  28. package/src/core/CircuitJsonDiagnosticIndexer.mjs +184 -0
  29. package/src/core/CircuitJsonDocument.mjs +19 -61
  30. package/src/core/CircuitJsonElementTypes.mjs +10 -0
  31. package/src/core/CircuitJsonElementValidator.mjs +98 -847
  32. package/src/core/CircuitJsonIndexer.mjs +274 -194
  33. package/src/core/CircuitJsonManufacturingBuilder.mjs +167 -164
  34. package/src/core/CircuitJsonParser.mjs +75 -13
  35. package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +12 -6
  36. package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +108 -10
  37. package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +1 -1
  38. package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +44 -38
  39. package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +146 -28
  40. package/src/core/CircuitJsonPcbPrimitiveFields.mjs +70 -4
  41. package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +18 -2
  42. package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +26 -9
  43. package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +7 -6
  44. package/src/core/CircuitJsonSerializedInputAudit.mjs +87 -0
  45. package/src/core/CircuitJsonSourceMetadata.mjs +5 -1
  46. package/src/core/CircuitJsonSupportMatrixBuilder.mjs +3 -1
  47. package/src/core/CircuitJsonToolkitElementSchema.mjs +218 -0
  48. package/src/core/CircuitJsonUnitParsers.mjs +101 -0
  49. package/src/core/CircuitJsonUnits.mjs +13 -87
  50. package/src/core/CircuitJsonUpstreamSchema.mjs +9 -0
  51. package/src/core/CircuitJsonUpstreamValidator.mjs +418 -0
  52. package/src/core/CircuitJsonValidationUnits.mjs +6 -0
  53. package/src/core/ManufacturingService.mjs +323 -0
  54. package/src/core/Parser.mjs +343 -0
  55. package/src/core/ParserOptions.mjs +333 -0
  56. package/src/core/PcbBoundsSelectionModel.mjs +55 -19
  57. package/src/core/PcbDiagnosticFocusModel.mjs +42 -11
  58. package/src/core/PcbInteractionIndex.mjs +368 -0
  59. package/src/core/PcbInteractionPrimitiveModel.mjs +393 -62
  60. package/src/core/ProjectAsyncInputOwner.mjs +70 -0
  61. package/src/core/ProjectLoader.mjs +975 -0
  62. package/src/core/SimulationService.mjs +790 -0
  63. package/src/core/ToolkitCapabilities.mjs +130 -0
  64. package/src/core/ZipArchiveInspector.mjs +649 -0
  65. package/src/core/context/BinaryDataSnapshot.mjs +217 -0
  66. package/src/core/context/CircuitJsonContextIndexes.mjs +96 -0
  67. package/src/core/context/CircuitJsonDerivedCache.mjs +114 -0
  68. package/src/core/context/CircuitJsonDocumentContext.mjs +353 -0
  69. package/src/core/context/CircuitJsonLegacyModel.mjs +147 -0
  70. package/src/core/context/CircuitJsonLegacyNormalizer.mjs +847 -0
  71. package/src/core/context/CircuitJsonMetadataBoundary.mjs +76 -0
  72. package/src/core/context/CircuitJsonModelFreezeTraversal.mjs +179 -0
  73. package/src/core/context/CircuitJsonReadOnlyDocument.mjs +920 -0
  74. package/src/core/context/CircuitJsonSchematicTableNormalizer.mjs +314 -0
  75. package/src/core/context/CircuitJsonValidationAuthority.mjs +39 -0
  76. package/src/core/context/CircuitJsonValidationProof.mjs +217 -0
  77. package/src/core/context/PcbPrimitivePreparation.mjs +198 -0
  78. package/src/core/context/PcbSpatialIndex.mjs +701 -0
  79. package/src/core/context/ProtectedExtensionBinaryBoundary.mjs +128 -0
  80. package/src/core/context/StructuredDataSnapshot.mjs +683 -0
  81. package/src/core/contracts/DocumentResult.mjs +198 -0
  82. package/src/core/contracts/ProjectResult.mjs +96 -0
  83. package/src/core/contracts/RuntimeProxyBoundary.mjs +48 -0
  84. package/src/core/contracts/ToolkitAsset.mjs +493 -0
  85. package/src/core/contracts/ToolkitDiagnostic.mjs +38 -0
  86. package/src/core/contracts/ToolkitError.mjs +176 -0
  87. package/src/core/contracts/ToolkitProgress.mjs +89 -0
  88. package/src/core/interaction/CanonicalInteractionOptions.mjs +246 -0
  89. package/src/core/interaction/PcbInteractionBounds.mjs +167 -0
  90. package/src/core/query/CircuitTraversal.mjs +343 -0
  91. package/src/core/query/ComponentGrouping.mjs +275 -0
  92. package/src/core/query/QueryNetlistBuilder.mjs +306 -0
  93. package/src/core/query/QueryService.mjs +435 -0
  94. package/src/core/query/RegexPattern.mjs +75 -0
  95. package/src/core/rendering/CanonicalBomOrder.mjs +81 -0
  96. package/src/core/rendering/CanonicalBomRows.mjs +92 -0
  97. package/src/core/rendering/CanonicalRenderOptions.mjs +498 -0
  98. package/src/core/rendering/CanonicalSvgDocument.mjs +102 -0
  99. package/src/core/rendering/PcbRenderPlan.mjs +429 -0
  100. package/src/core/rendering/SchematicSheetSelector.mjs +335 -0
  101. package/src/core/scene3d/PcbScene3dBuilder.mjs +906 -0
  102. package/src/core/scene3d/PcbScene3dPreparator.mjs +47 -0
  103. package/src/core/scene3d/Scene3dAssetIndex.mjs +284 -0
  104. package/src/core/scene3d/Scene3dBoardModel.mjs +596 -0
  105. package/src/core/scene3d/Scene3dDocumentMetadata.mjs +167 -0
  106. package/src/core/scene3d/Scene3dFreeze.mjs +37 -0
  107. package/src/core/scene3d/Scene3dIdRegistry.mjs +34 -0
  108. package/src/core/scene3d/Scene3dInputPreflight.mjs +193 -0
  109. package/src/core/scene3d/Scene3dMaterials.mjs +58 -0
  110. package/src/core/scene3d/Scene3dModelReference.mjs +134 -0
  111. package/src/core/scene3d/Scene3dOptions.mjs +363 -0
  112. package/src/core/scene3d/SceneAssetResolver.mjs +441 -0
  113. package/src/core/simulation/SimulationParameterCloner.mjs +543 -0
  114. package/src/core/worker/ParserWorkerClient.mjs +997 -0
  115. package/src/core/worker/ToolkitWorkerProtocol.mjs +412 -0
  116. package/src/core/worker/WorkerRequestData.mjs +623 -0
  117. package/src/core/worker/WorkerResponseData.mjs +905 -0
  118. package/src/extensions.mjs +37 -0
  119. package/src/index.mjs +14 -9
  120. package/src/interaction.mjs +2 -0
  121. package/src/manufacturing.mjs +1 -0
  122. package/src/parser.mjs +12 -2
  123. package/src/project.mjs +5 -0
  124. package/src/query.mjs +1 -0
  125. package/src/renderers.mjs +3 -29
  126. package/src/scene3d.mjs +3 -0
  127. package/src/simulation.mjs +1 -0
  128. package/src/styles/renderers.css +24 -0
  129. package/src/testing/ToolkitContractFixtures.mjs +124 -0
  130. package/src/testing/ToolkitLoopbackWorker.mjs +174 -0
  131. package/src/testing/runToolkitContract.mjs +705 -0
  132. package/src/testing.mjs +3 -0
  133. package/src/ui/BomTableRenderer.mjs +304 -0
  134. package/src/ui/CircuitJsonPcbBoardSvgRenderer.mjs +80 -0
  135. package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +3 -5
  136. package/src/ui/CircuitJsonPcbSvgRenderer.mjs +63 -43
  137. package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +3 -5
  138. package/src/ui/CircuitJsonSchematicDebugRenderer.mjs +164 -0
  139. package/src/ui/CircuitJsonSchematicImageSvgRenderer.mjs +210 -0
  140. package/src/ui/CircuitJsonSchematicLineRenderer.mjs +86 -0
  141. package/src/ui/CircuitJsonSchematicSheetSymbolSvgRenderer.mjs +98 -0
  142. package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +117 -17
  143. package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +67 -20
  144. package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +45 -9
  145. package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +151 -148
  146. package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +4 -292
  147. package/src/ui/PcbSvgRenderer.mjs +41 -0
  148. package/src/ui/SafeSvgPaint.mjs +26 -0
  149. package/src/ui/SafeXmlText.mjs +60 -0
  150. package/src/ui/SchematicGeometryBounds.mjs +540 -0
  151. package/src/ui/SchematicSvgRenderer.mjs +110 -0
  152. package/src/ui/SchematicTableGeometry.mjs +319 -0
  153. package/src/ui/SchematicTextAnchor.mjs +55 -0
  154. package/src/ui/SchematicTextBounds.mjs +98 -0
  155. package/src/workers/parser.worker.mjs +59 -0
@@ -1,4 +1,7 @@
1
1
  import { CircuitJsonDocument } from './CircuitJsonDocument.mjs'
2
+ import { CircuitJsonDiagnosticIndexer } from './CircuitJsonDiagnosticIndexer.mjs'
3
+ import { CircuitJsonLegacyModel } from './context/CircuitJsonLegacyModel.mjs'
4
+ import { CircuitJsonValidationProof } from './context/CircuitJsonValidationProof.mjs'
2
5
 
3
6
  const ID_FIELDS_BY_TYPE = {
4
7
  pcb_board: 'pcb_board_id',
@@ -19,14 +22,31 @@ const ID_FIELDS_BY_TYPE = {
19
22
  }
20
23
 
21
24
  const GROUP_TYPES = new Set(['source_group', 'pcb_group', 'schematic_group'])
25
+ const GROUP_RELATION_FIELDS = [
26
+ 'source_group_id',
27
+ 'pcb_group_id',
28
+ 'schematic_group_id',
29
+ 'group_id',
30
+ 'member_source_group_id',
31
+ 'member_pcb_group_id',
32
+ 'member_schematic_group_id',
33
+ 'member_group_id',
34
+ 'group_ids',
35
+ 'member_source_group_ids',
36
+ 'member_pcb_group_ids',
37
+ 'member_schematic_group_ids',
38
+ 'member_group_ids'
39
+ ]
22
40
  const SCHEMATIC_PRIMITIVE_TYPES = new Set([
23
41
  'schematic_arc',
24
42
  'schematic_box',
25
43
  'schematic_circle',
44
+ 'schematic_image',
26
45
  'schematic_line',
27
46
  'schematic_net_label',
28
47
  'schematic_path',
29
48
  'schematic_rect',
49
+ 'schematic_sheet_symbol',
30
50
  'schematic_table',
31
51
  'schematic_table_cell',
32
52
  'schematic_text',
@@ -41,69 +61,145 @@ export class CircuitJsonIndexer {
41
61
  /**
42
62
  * Indexes a CircuitJSON model.
43
63
  * @param {object[]} circuitJson CircuitJSON model.
44
- * @returns {{ elements: object[], elementsByType: Map<string, object[]>, elementsById: Map<string, object>, relationsByField: Map<string, Map<string, object[]>>, sourceComponentById: Map<string, object>, pcbComponentById: Map<string, object>, sourceTraceById: Map<string, object>, sourceTraceConnectivity: Map<string, object>, componentsBySourceId: Map<string, object>, groupsById: Map<string, object>, elementsByGroupId: Map<string, object[]>, elementsBySubcircuitId: Map<string, object[]>, diagnostics: object[] }}
64
+ * @param {{ validated?: boolean, families?: string[] | null }} [options] Internal validation options.
65
+ * @returns {Record<string, any>} Full legacy index or requested index families.
45
66
  */
46
- static index(circuitJson) {
47
- CircuitJsonDocument.assertModel(circuitJson)
67
+ static index(circuitJson, options = {}) {
68
+ const prepared = CircuitJsonLegacyModel.takePreparedIndex(circuitJson)
69
+ if (prepared) return prepared
70
+ const trusted = CircuitJsonValidationProof.permitsIndex(
71
+ circuitJson,
72
+ options
73
+ )
74
+ const elementSlots = trusted
75
+ ? circuitJson
76
+ : CircuitJsonIndexer.#captureElementSlots(circuitJson)
77
+ const trustedLegacy =
78
+ !trusted && CircuitJsonLegacyModel.permitsIndex(circuitJson)
79
+ if (!trusted && !trustedLegacy) {
80
+ CircuitJsonDocument.assertModel(elementSlots)
81
+ }
82
+ const families = trusted
83
+ ? CircuitJsonValidationProof.indexFamilies(circuitJson, options)
84
+ : null
85
+ const requested = new Set(families || [])
86
+ const full = families === null
87
+ const buildElements = full || requested.has('elements')
88
+ const buildIdentifiers = !full && requested.has('identifiers')
89
+ const buildRelations = full || requested.has('relations')
90
+ const buildConnectivity = full || requested.has('connectivity')
91
+ const buildSpatial = full || requested.has('spatial')
48
92
  const elementsByType = new Map()
49
93
  const elementsById = new Map()
94
+ const elementIds = new Set()
50
95
  const relationsByField = new Map()
51
96
  const sourceComponentById = new Map()
52
97
  const pcbComponentById = new Map()
53
98
  const sourceTraceById = new Map()
99
+ const groupElements = []
54
100
 
55
- circuitJson.forEach((element) => {
56
- const type = String(element?.type || '')
57
- if (!elementsByType.has(type)) {
58
- elementsByType.set(type, [])
59
- }
60
- elementsByType.get(type).push(element)
61
-
62
- const id = CircuitJsonIndexer.getElementId(element)
63
- if (id) {
64
- elementsById.set(`${type}:${id}`, element)
65
- }
66
- if (type === 'source_component' && id) {
67
- sourceComponentById.set(id, element)
68
- }
69
- if (type === 'pcb_component' && id) {
70
- pcbComponentById.set(id, element)
71
- }
72
- if (type === 'source_trace' && id) {
73
- sourceTraceById.set(id, element)
101
+ if (
102
+ buildElements ||
103
+ buildIdentifiers ||
104
+ buildRelations ||
105
+ buildConnectivity
106
+ ) {
107
+ for (let slot = 0; slot < elementSlots.length; slot += 1) {
108
+ const element = elementSlots[slot]
109
+ const type = String(element?.type || '')
110
+ const id = CircuitJsonIndexer.getElementId(element)
111
+ if (buildElements || buildConnectivity) {
112
+ if (!elementsByType.has(type)) {
113
+ elementsByType.set(type, [])
114
+ }
115
+ elementsByType.get(type).push(element)
116
+ }
117
+ if (buildElements && id) {
118
+ elementsById.set(`${type}:${id}`, element)
119
+ }
120
+ if (buildIdentifiers && id) {
121
+ elementIds.add(`${type}:${id}`)
122
+ }
123
+ if ((buildElements || buildRelations) && id) {
124
+ if (type === 'source_component') {
125
+ sourceComponentById.set(id, element)
126
+ }
127
+ if (buildElements && type === 'pcb_component') {
128
+ pcbComponentById.set(id, element)
129
+ }
130
+ }
131
+ if ((buildElements || buildConnectivity) && id) {
132
+ if (type === 'source_trace') {
133
+ sourceTraceById.set(id, element)
134
+ }
135
+ }
136
+ if (buildRelations) {
137
+ if (GROUP_TYPES.has(type)) groupElements.push(element)
138
+ CircuitJsonIndexer.#indexRelations(
139
+ element,
140
+ relationsByField
141
+ )
142
+ }
74
143
  }
75
- CircuitJsonIndexer.#indexRelations(element, relationsByField)
76
- })
77
- const sourceTraceConnectivity =
78
- CircuitJsonIndexer.#sourceTraceConnectivity(sourceTraceById)
144
+ }
79
145
 
80
- return {
81
- elements: circuitJson,
82
- elementsByType,
83
- elementsById,
84
- relationsByField,
85
- sourceComponentById,
86
- pcbComponentById,
87
- sourceTraceById,
88
- sourceTraceConnectivity,
89
- componentsBySourceId: CircuitJsonIndexer.#componentsBySourceId(
146
+ const result = {}
147
+ if (buildElements || buildSpatial) {
148
+ result.elements = circuitJson
149
+ }
150
+ if (buildElements) {
151
+ Object.assign(result, {
152
+ elementsByType,
153
+ elementsById,
90
154
  sourceComponentById,
91
- relationsByField
92
- ),
93
- groupsById: CircuitJsonIndexer.#groupsById(circuitJson),
94
- elementsByGroupId:
95
- CircuitJsonIndexer.#elementsByGroupId(circuitJson),
96
- elementsBySubcircuitId:
97
- CircuitJsonIndexer.#elementsBySubcircuitId(circuitJson),
98
- diagnostics: [
99
- ...CircuitJsonIndexer.collectDiagnostics(circuitJson),
100
- ...CircuitJsonIndexer.#referenceDiagnostics(
101
- elementsByType,
102
- sourceTraceById,
103
- sourceTraceConnectivity
104
- )
105
- ]
155
+ pcbComponentById,
156
+ sourceTraceById
157
+ })
106
158
  }
159
+ if (buildIdentifiers) {
160
+ result.elementsById = elementIds
161
+ }
162
+ if (buildRelations) {
163
+ const hasGroupRelations = GROUP_RELATION_FIELDS.some((field) =>
164
+ relationsByField.has(field)
165
+ )
166
+ const elementsByGroupId = hasGroupRelations
167
+ ? CircuitJsonIndexer.#elementsByGroupId(elementSlots)
168
+ : new Map()
169
+ Object.assign(result, {
170
+ relationsByField,
171
+ componentsBySourceId: CircuitJsonIndexer.#componentsBySourceId(
172
+ sourceComponentById,
173
+ relationsByField
174
+ ),
175
+ groupsById: CircuitJsonIndexer.#groupsById(
176
+ groupElements,
177
+ elementsByGroupId
178
+ ),
179
+ elementsByGroupId,
180
+ elementsBySubcircuitId:
181
+ relationsByField.get('subcircuit_id') || new Map()
182
+ })
183
+ }
184
+ if (buildConnectivity) {
185
+ const sourceTraceConnectivity =
186
+ CircuitJsonIndexer.#sourceTraceConnectivity(sourceTraceById)
187
+ Object.assign(result, {
188
+ sourceTraceById,
189
+ sourceTraceConnectivity,
190
+ diagnostics: [
191
+ ...CircuitJsonIndexer.#collectIndexedDiagnostics(
192
+ elementsByType
193
+ ),
194
+ ...CircuitJsonIndexer.#referenceDiagnostics(
195
+ elementsByType,
196
+ sourceTraceById,
197
+ sourceTraceConnectivity
198
+ )
199
+ ]
200
+ })
201
+ }
202
+ return result
107
203
  }
108
204
 
109
205
  /**
@@ -123,10 +219,107 @@ export class CircuitJsonIndexer {
123
219
  * @returns {object[]}
124
220
  */
125
221
  static collectDiagnostics(circuitJson) {
126
- CircuitJsonDocument.assertModel(circuitJson)
222
+ const elementSlots =
223
+ CircuitJsonIndexer.#captureElementSlots(circuitJson)
224
+ CircuitJsonDocument.assertModel(elementSlots)
225
+ return CircuitJsonIndexer.#collectDiagnostics(elementSlots)
226
+ }
227
+
228
+ /**
229
+ * Captures dense element data slots without caller-dispatchable iteration.
230
+ * @param {unknown} circuitJson CircuitJSON array candidate.
231
+ * @returns {object[]} Plain intrinsic array containing the captured slots.
232
+ */
233
+ static #captureElementSlots(circuitJson) {
234
+ if (!Array.isArray(circuitJson)) return circuitJson
235
+ let prototype
236
+ let lengthDescriptor
237
+ try {
238
+ prototype = Object.getPrototypeOf(circuitJson)
239
+ lengthDescriptor = Object.getOwnPropertyDescriptor(
240
+ circuitJson,
241
+ 'length'
242
+ )
243
+ } catch {
244
+ throw new TypeError(
245
+ 'CircuitJSON arrays must expose dense own data properties.'
246
+ )
247
+ }
248
+ const length = lengthDescriptor?.value
249
+ if (
250
+ prototype !== Array.prototype ||
251
+ !Number.isSafeInteger(length) ||
252
+ length < 0
253
+ ) {
254
+ throw new TypeError(
255
+ 'CircuitJSON arrays must expose dense own data properties.'
256
+ )
257
+ }
258
+ const elements = new Array(length)
259
+ for (let index = 0; index < length; index += 1) {
260
+ let descriptor
261
+ try {
262
+ descriptor = Object.getOwnPropertyDescriptor(
263
+ circuitJson,
264
+ String(index)
265
+ )
266
+ } catch {
267
+ throw new TypeError(
268
+ 'CircuitJSON arrays must expose dense own data properties.'
269
+ )
270
+ }
271
+ if (
272
+ !descriptor ||
273
+ !Object.hasOwn(descriptor, 'value') ||
274
+ descriptor.enumerable !== true
275
+ ) {
276
+ throw new TypeError(
277
+ 'CircuitJSON arrays must expose dense own data properties.'
278
+ )
279
+ }
280
+ elements[index] = descriptor.value
281
+ }
282
+ return elements
283
+ }
284
+
285
+ /**
286
+ * Collects normalized diagnostic rows from an already validated model.
287
+ * @param {object[]} circuitJson CircuitJSON model.
288
+ * @returns {object[]}
289
+ */
290
+ static #collectDiagnostics(circuitJson) {
127
291
  return circuitJson
128
- .filter((element) => CircuitJsonIndexer.#isDiagnostic(element))
129
- .map((element) => CircuitJsonIndexer.#diagnostic(element))
292
+ .filter((element) =>
293
+ CircuitJsonDiagnosticIndexer.isElement(element)
294
+ )
295
+ .map((element) =>
296
+ CircuitJsonDiagnosticIndexer.fromElement(
297
+ element,
298
+ CircuitJsonIndexer.getElementId(element)
299
+ )
300
+ )
301
+ }
302
+
303
+ /**
304
+ * Collects diagnostics from validated type buckets without rescanning every
305
+ * non-diagnostic row in large models.
306
+ * @param {Map<string, object[]>} elementsByType Element rows by type.
307
+ * @returns {object[]}
308
+ */
309
+ static #collectIndexedDiagnostics(elementsByType) {
310
+ const diagnostics = []
311
+ for (const [type, elements] of elementsByType) {
312
+ if (!CircuitJsonDiagnosticIndexer.isType(type)) continue
313
+ for (const element of elements) {
314
+ diagnostics.push(
315
+ CircuitJsonDiagnosticIndexer.fromElement(
316
+ element,
317
+ CircuitJsonIndexer.getElementId(element)
318
+ )
319
+ )
320
+ }
321
+ }
322
+ return diagnostics
130
323
  }
131
324
 
132
325
  /**
@@ -139,17 +332,21 @@ export class CircuitJsonIndexer {
139
332
  const primaryIdField =
140
333
  ID_FIELDS_BY_TYPE[String(element?.type || '')] ||
141
334
  String(element?.type || '') + '_id'
142
- for (const [field, value] of Object.entries(element || {})) {
335
+ for (const field in element || {}) {
336
+ if (!Object.hasOwn(element, field)) continue
143
337
  if (!field.endsWith('_id') && !field.endsWith('_ids')) continue
144
338
  if (field === primaryIdField) continue
145
-
146
- if (!relationsByField.has(field)) {
147
- relationsByField.set(field, new Map())
339
+ const value = element[field]
340
+ if (value == null || value === '') continue
341
+ if (Array.isArray(value) && value.length === 0) continue
342
+ const relationValues = CircuitJsonIndexer.#relationValues(value)
343
+ if (relationValues.length === 0) continue
344
+ let byValue = relationsByField.get(field)
345
+ if (!byValue) {
346
+ byValue = new Map()
347
+ relationsByField.set(field, byValue)
148
348
  }
149
- const byValue = relationsByField.get(field)
150
- for (const relationValue of CircuitJsonIndexer.#relationValues(
151
- value
152
- )) {
349
+ for (const relationValue of relationValues) {
153
350
  if (!byValue.has(relationValue)) {
154
351
  byValue.set(relationValue, [])
155
352
  }
@@ -239,6 +436,17 @@ export class CircuitJsonIndexer {
239
436
  sourceTraceById,
240
437
  sourceTraceConnectivity
241
438
  ) {
439
+ if (
440
+ sourceTraceConnectivity.size === 0 &&
441
+ !elementsByType.has('pcb_trace') &&
442
+ !elementsByType.has('schematic_component') &&
443
+ !elementsByType.has('schematic_port') &&
444
+ ![...SCHEMATIC_PRIMITIVE_TYPES].some((type) =>
445
+ elementsByType.has(type)
446
+ )
447
+ ) {
448
+ return []
449
+ }
242
450
  const sourcePortIds = new Set(
243
451
  CircuitJsonIndexer.#all(elementsByType, 'source_port')
244
452
  .map((port) => String(port.source_port_id || '').trim())
@@ -609,15 +817,13 @@ export class CircuitJsonIndexer {
609
817
 
610
818
  /**
611
819
  * Builds group rows keyed by group id.
612
- * @param {object[]} elements Element rows.
820
+ * @param {object[]} groupElements Group element rows.
821
+ * @param {Map<string, object[]>} elementsByGroupId Element membership.
613
822
  * @returns {Map<string, object>}
614
823
  */
615
- static #groupsById(elements) {
616
- const elementsByGroupId =
617
- CircuitJsonIndexer.#elementsByGroupId(elements)
824
+ static #groupsById(groupElements, elementsByGroupId) {
618
825
  const groups = new Map()
619
- for (const element of elements) {
620
- if (!GROUP_TYPES.has(String(element?.type || ''))) continue
826
+ for (const element of groupElements) {
621
827
  const id = CircuitJsonIndexer.getElementId(element)
622
828
  if (!id) continue
623
829
  groups.set(id, {
@@ -648,22 +854,6 @@ export class CircuitJsonIndexer {
648
854
  return byGroupId
649
855
  }
650
856
 
651
- /**
652
- * Builds elements by subcircuit id.
653
- * @param {object[]} elements Element rows.
654
- * @returns {Map<string, object[]>}
655
- */
656
- static #elementsBySubcircuitId(elements) {
657
- const bySubcircuitId = new Map()
658
- for (const element of elements) {
659
- const id = String(element?.subcircuit_id || '').trim()
660
- if (!id) continue
661
- if (!bySubcircuitId.has(id)) bySubcircuitId.set(id, [])
662
- bySubcircuitId.get(id).push(element)
663
- }
664
- return bySubcircuitId
665
- }
666
-
667
857
  /**
668
858
  * Builds a set of known element ids for one type.
669
859
  * @param {Map<string, object[]>} elementsByType Element rows by type.
@@ -718,114 +908,4 @@ export class CircuitJsonIndexer {
718
908
  static #all(elementsByType, type) {
719
909
  return elementsByType.get(type) || []
720
910
  }
721
-
722
- /**
723
- * Returns true when an element is a warning or error row.
724
- * @param {object} element Element.
725
- * @returns {boolean}
726
- */
727
- static #isDiagnostic(element) {
728
- const type = String(element?.type || '')
729
- return (
730
- type.endsWith('_error') ||
731
- type.endsWith('_warning') ||
732
- Boolean(element?.error_type || element?.warning_type)
733
- )
734
- }
735
-
736
- /**
737
- * Builds one normalized diagnostic.
738
- * @param {object} element Diagnostic element.
739
- * @returns {object}
740
- */
741
- static #diagnostic(element) {
742
- const type = String(
743
- element?.error_type || element?.warning_type || element?.type || ''
744
- )
745
- return {
746
- severity: element?.warning_type ? 'warning' : 'error',
747
- sourceFormat: 'circuitjson',
748
- type,
749
- category: CircuitJsonIndexer.#diagnosticCategory(type),
750
- message: String(element?.message || type || 'CircuitJSON issue'),
751
- elementId: CircuitJsonIndexer.getElementId(element),
752
- ...CircuitJsonIndexer.#diagnosticRelations(element)
753
- }
754
- }
755
-
756
- /**
757
- * Extracts optional relation ids from one diagnostic element.
758
- * @param {object} element Diagnostic element.
759
- * @returns {object}
760
- */
761
- static #diagnosticRelations(element) {
762
- return Object.fromEntries(
763
- [
764
- ['sourceComponentId', element?.source_component_id],
765
- ['sourcePortId', element?.source_port_id],
766
- ['sourceNetId', element?.source_net_id],
767
- ['sourceTraceId', element?.source_trace_id],
768
- ['pcbComponentId', element?.pcb_component_id],
769
- ['pcbPortId', element?.pcb_port_id],
770
- ['pcbTraceId', element?.pcb_trace_id],
771
- ['pcbSmtpadId', element?.pcb_smtpad_id],
772
- ['pcbViaId', element?.pcb_via_id],
773
- ['pcbPlatedHoleId', element?.pcb_plated_hole_id],
774
- ['pcbHoleId', element?.pcb_hole_id],
775
- ['schematicComponentId', element?.schematic_component_id],
776
- ['schematicSymbolId', element?.schematic_symbol_id],
777
- ['schematicPortId', element?.schematic_port_id]
778
- ]
779
- .map(([key, value]) => [key, String(value || '').trim()])
780
- .filter(([_key, value]) => value)
781
- )
782
- }
783
-
784
- /**
785
- * Resolves a broad diagnostic category.
786
- * @param {string} type Diagnostic type or code.
787
- * @returns {string}
788
- */
789
- static #diagnosticCategory(type) {
790
- const text = String(type || '').toLowerCase()
791
- if (text.includes('clearance')) return 'clearance'
792
- if (text.includes('autorouting') || text.includes('trace_error')) {
793
- return 'routing'
794
- }
795
- if (text.includes('placement') || text.includes('outside_board')) {
796
- return 'placement'
797
- }
798
- if (
799
- text.includes('trace_missing') ||
800
- text.includes('not_connected') ||
801
- text.includes('missing_trace') ||
802
- text.includes('pin_missing_trace') ||
803
- text.includes('pin_must_be_connected')
804
- ) {
805
- return 'connectivity'
806
- }
807
- if (text.includes('layout')) return 'layout'
808
- if (text.includes('simulation')) return 'simulation'
809
- if (text.includes('footprint')) return 'footprint'
810
- if (
811
- text.includes('pin_defined') ||
812
- text.includes('pins_underspecified') ||
813
- text.includes('ground_pin') ||
814
- text.includes('power_pin')
815
- ) {
816
- return 'pin-definition'
817
- }
818
- if (
819
- text.includes('manufacturer_part') ||
820
- text.includes('missing_property') ||
821
- text.includes('property_ignored')
822
- ) {
823
- return 'metadata'
824
- }
825
- if (text.includes('manual_edit_conflict')) return 'edit-conflict'
826
- if (text.includes('property') || text.includes('misconfigured')) {
827
- return 'configuration'
828
- }
829
- return 'general'
830
- }
831
911
  }