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,289 +1,67 @@
1
- import { CircuitJsonUnits } from './CircuitJsonUnits.mjs'
2
-
3
- const KNOWN_ELEMENT_TYPES = new Set([
4
- 'cad_component',
5
- 'circuit_json_footprint_load_error',
6
- 'external_footprint_load_error',
7
- 'pcb_autorouting_error',
8
- 'pcb_board',
9
- 'pcb_breakout_point',
10
- 'pcb_component',
11
- 'pcb_component_invalid_layer_error',
12
- 'pcb_component_not_on_board_edge_error',
13
- 'pcb_component_outside_board_error',
14
- 'pcb_connector_not_in_accessible_orientation_warning',
15
- 'pcb_copper_pour',
16
- 'pcb_copper_text',
17
- 'pcb_courtyard',
18
- 'pcb_courtyard_circle',
19
- 'pcb_courtyard_line',
20
- 'pcb_courtyard_outline',
21
- 'pcb_courtyard_path',
22
- 'pcb_courtyard_overlap_error',
23
- 'pcb_courtyard_pill',
24
- 'pcb_courtyard_polygon',
25
- 'pcb_courtyard_rect',
26
- 'pcb_cutout',
27
- 'pcb_fabrication_note_dimension',
28
- 'pcb_fabrication_note_path',
29
- 'pcb_fabrication_note_rect',
30
- 'pcb_fabrication_note_text',
31
- 'pcb_footprint_overlap_error',
32
- 'pcb_ground_plane',
33
- 'pcb_ground_plane_region',
34
- 'pcb_group',
35
- 'pcb_hole',
36
- 'pcb_keepout',
37
- 'pcb_manual_edit_conflict_warning',
38
- 'pcb_missing_footprint_error',
39
- 'pcb_net',
40
- 'pcb_note_dimension',
41
- 'pcb_note_line',
42
- 'pcb_note_path',
43
- 'pcb_note_rect',
44
- 'pcb_note_text',
45
- 'pcb_pad_pad_clearance_error',
46
- 'pcb_pad_trace_clearance_error',
47
- 'pcb_panel',
48
- 'pcb_panelization_placement_error',
49
- 'pcb_placement_error',
50
- 'pcb_plated_hole',
51
- 'pcb_port',
52
- 'pcb_port_not_connected_error',
53
- 'pcb_port_not_matched_error',
54
- 'pcb_silkscreen_circle',
55
- 'pcb_silkscreen_graphic',
56
- 'pcb_silkscreen_line',
57
- 'pcb_silkscreen_oval',
58
- 'pcb_silkscreen_path',
59
- 'pcb_silkscreen_pill',
60
- 'pcb_silkscreen_rect',
61
- 'pcb_silkscreen_text',
62
- 'pcb_smtpad',
63
- 'pcb_solder_paste',
64
- 'pcb_text',
65
- 'pcb_thermal_spoke',
66
- 'pcb_trace',
67
- 'pcb_trace_error',
68
- 'pcb_trace_hint',
69
- 'pcb_trace_missing_error',
70
- 'pcb_trace_warning',
71
- 'pcb_via',
72
- 'pcb_via_clearance_error',
73
- 'pcb_via_trace_clearance_error',
74
- 'schematic_arc',
75
- 'schematic_box',
76
- 'schematic_circle',
77
- 'schematic_component',
78
- 'schematic_debug_object',
79
- 'schematic_error',
80
- 'schematic_group',
81
- 'schematic_layout_error',
82
- 'schematic_line',
83
- 'schematic_manual_edit_conflict_warning',
84
- 'schematic_net_label',
85
- 'schematic_path',
86
- 'schematic_port',
87
- 'schematic_rect',
88
- 'schematic_sheet',
89
- 'schematic_symbol',
90
- 'schematic_table',
91
- 'schematic_table_cell',
92
- 'schematic_text',
93
- 'schematic_trace',
94
- 'schematic_voltage_probe',
95
- 'simulation_current_probe',
96
- 'simulation_current_source',
97
- 'simulation_experiment',
98
- 'simulation_op_amp',
99
- 'simulation_oscilloscope_trace',
100
- 'simulation_spice_subcircuit',
101
- 'simulation_switch',
102
- 'simulation_transient_current_graph',
103
- 'simulation_transient_voltage_graph',
104
- 'simulation_unknown_experiment_error',
105
- 'simulation_voltage_probe',
106
- 'simulation_voltage_source',
107
- 'source_ambiguous_port_reference',
108
- 'source_board',
109
- 'source_component',
110
- 'source_component_internal_connection',
111
- 'source_component_misconfigured_error',
112
- 'source_component_pins_underspecified_warning',
113
- 'source_failed_to_create_component_error',
114
- 'source_group',
115
- 'source_i2c_misconfigured_error',
116
- 'source_invalid_component_property_error',
117
- 'source_manually_placed_via',
118
- 'source_missing_manufacturer_part_number_warning',
119
- 'source_missing_property_error',
120
- 'source_net',
121
- 'source_no_ground_pin_defined_warning',
122
- 'source_no_power_pin_defined_warning',
123
- 'source_pcb_ground_plane',
124
- 'source_pin_missing_trace_warning',
125
- 'source_pin_must_be_connected_error',
126
- 'source_port',
127
- 'source_project_metadata',
128
- 'source_property_ignored_warning',
129
- 'source_trace',
130
- 'source_trace_not_connected_error',
131
- 'supplier_footprint_mismatch_warning',
132
- 'unknown_error_finding_part'
133
- ])
134
-
135
- const ID_FIELD_EXCEPTIONS = new Set([
136
- 'pcb_autorouting_error',
137
- 'pcb_courtyard_overlap_error',
138
- 'pcb_footprint_overlap_error',
139
- 'pcb_port_not_matched_error',
140
- 'pcb_via_clearance_error',
141
- 'schematic_box',
142
- 'schematic_debug_object',
143
- 'source_project_metadata'
144
- ])
145
-
146
- const SOURCE_COMPONENT_FTYPES = new Set([
147
- 'interconnect',
148
- 'simple_ammeter',
149
- 'simple_battery',
150
- 'simple_capacitor',
151
- 'simple_chip',
152
- 'simple_connector',
153
- 'simple_crystal',
154
- 'simple_current_source',
155
- 'simple_diode',
156
- 'simple_fiducial',
157
- 'simple_fuse',
158
- 'simple_ground',
159
- 'simple_inductor',
160
- 'simple_led',
161
- 'simple_mosfet',
162
- 'simple_op_amp',
163
- 'simple_pin_header',
164
- 'simple_pinout',
165
- 'simple_potentiometer',
166
- 'simple_power_source',
167
- 'simple_push_button',
168
- 'simple_resistor',
169
- 'simple_resonator',
170
- 'simple_switch',
171
- 'simple_test_point',
172
- 'simple_transistor',
173
- 'simple_voltage_probe',
174
- 'simple_voltage_source'
175
- ])
176
-
177
- const LAYERS = new Set(
178
- 'top bottom inner1 inner2 inner3 inner4 inner5 inner6'.split(' ')
179
- )
180
-
181
- const SMT_PAD_SHAPES = new Set([
182
- 'circle',
183
- 'rect',
184
- 'rotated_rect',
185
- 'rotated_pill',
186
- 'pill',
187
- 'polygon',
188
- 'rounded_rect'
189
- ])
190
-
191
- const PCB_HOLE_SHAPES = new Set([
192
- 'circle',
193
- 'circle_or_square',
194
- 'oval',
195
- 'pill',
196
- 'rect',
197
- 'rotated_pill',
198
- 'round'
199
- ])
200
-
201
- const PCB_PLATED_HOLE_SHAPES = new Set([
202
- 'circle',
203
- 'circular_hole_with_rect_pad',
204
- 'hole_with_polygon_pad',
205
- 'oval',
206
- 'pill',
207
- 'pill_hole_with_rect_pad',
208
- 'rotated_pill_hole_with_rect_pad'
209
- ])
210
-
211
- const PCB_PLATED_HOLE_HOLE_SHAPES = new Set([
212
- 'circle',
213
- 'oval',
214
- 'pill',
215
- 'rotated_pill'
216
- ])
217
-
218
- const PCB_SOLDER_PASTE_SHAPES = new Set([
219
- 'circle',
220
- 'oval',
221
- 'pill',
222
- 'rect',
223
- 'rotated_rect'
224
- ])
225
-
226
- const PCB_BOARD_SHAPES = new Set(['polygon', 'rect'])
227
-
228
- const PCB_CUTOUT_SHAPES = new Set(['circle', 'path', 'polygon', 'rect'])
229
-
230
- const PCB_COPPER_POUR_SHAPES = new Set(['brep', 'polygon', 'rect'])
231
-
232
- const SIMULATION_SOURCE_KINDS = new Set(['ac', 'dc'])
233
-
234
- const SIMULATION_WAVE_SHAPES = new Set(
235
- 'sawtooth sinewave square triangle'.split(' ')
1
+ import { CIRCUIT_JSON_ELEMENT_TYPES } from './CircuitJsonElementTypes.mjs'
2
+ import {
3
+ CIRCUIT_JSON_UPSTREAM_ELEMENT_TYPES,
4
+ CIRCUIT_JSON_UPSTREAM_ID_FIELD_EXCEPTIONS,
5
+ CIRCUIT_JSON_UPSTREAM_VARIANT_SETS
6
+ } from './CircuitJsonUpstreamSchema.mjs'
7
+ import { CircuitJsonToolkitElementSchema } from './CircuitJsonToolkitElementSchema.mjs'
8
+ import { CircuitJsonUpstreamValidator } from './CircuitJsonUpstreamValidator.mjs'
9
+ import { CircuitJsonModelFreezeTraversal } from './context/CircuitJsonModelFreezeTraversal.mjs'
10
+
11
+ const KNOWN_ELEMENT_TYPES = CIRCUIT_JSON_ELEMENT_TYPES
12
+ const UPSTREAM_ELEMENT_TYPES = new Set(CIRCUIT_JSON_UPSTREAM_ELEMENT_TYPES)
13
+ const ID_FIELD_EXCEPTIONS = new Set(CIRCUIT_JSON_UPSTREAM_ID_FIELD_EXCEPTIONS)
14
+ const VARIANT_SETS = Object.fromEntries(
15
+ Object.entries(CIRCUIT_JSON_UPSTREAM_VARIANT_SETS).map(([name, values]) => [
16
+ name,
17
+ new Set(values)
18
+ ])
236
19
  )
237
-
238
- const SIMULATION_EXPERIMENT_METHODS = new Set(['gear', 'trap'])
239
-
240
- const VARIANT_SETS = {
241
- sourceComponentFtypes: SOURCE_COMPONENT_FTYPES,
242
- pcbBoardShapes: PCB_BOARD_SHAPES,
243
- pcbSmtPadShapes: SMT_PAD_SHAPES,
244
- pcbHoleShapes: PCB_HOLE_SHAPES,
245
- pcbPlatedHoleShapes: PCB_PLATED_HOLE_SHAPES,
246
- pcbPlatedHoleHoleShapes: PCB_PLATED_HOLE_HOLE_SHAPES,
247
- pcbSolderPasteShapes: PCB_SOLDER_PASTE_SHAPES,
248
- pcbCutoutShapes: PCB_CUTOUT_SHAPES,
249
- pcbCopperPourShapes: PCB_COPPER_POUR_SHAPES,
250
- simulationSourceKinds: SIMULATION_SOURCE_KINDS,
251
- simulationWaveShapes: SIMULATION_WAVE_SHAPES,
252
- simulationExperimentMethods: SIMULATION_EXPERIMENT_METHODS
253
- }
254
-
255
20
  const SORTED_VARIANT_SET_NAMES = Object.keys(VARIANT_SETS).sort()
256
21
 
257
- /**
258
- * @typedef {{ set: string, value: string }} CircuitJsonVariantDiff
259
- */
260
-
261
- /**
262
- * @typedef {{ elementTypes: string[], idFieldExceptions: string[], variantSets: Record<string, string[]> }} CircuitJsonSchemaSnapshot
263
- */
22
+ /** @typedef {{ set: string, value: string }} CircuitJsonVariantDiff */
23
+ /** @typedef {{ elementTypes: string[], idFieldExceptions: string[], variantSets: Record<string, string[]> }} CircuitJsonSchemaSnapshot */
24
+ /** @typedef {{ matches: boolean, missingElementTypes: string[], unexpectedElementTypes: string[], missingIdFieldExceptions: string[], unexpectedIdFieldExceptions: string[], missingVariants: CircuitJsonVariantDiff[], unexpectedVariants: CircuitJsonVariantDiff[] }} CircuitJsonSchemaSnapshotComparison */
264
25
 
265
26
  /**
266
- * @typedef {{ matches: boolean, missingElementTypes: string[], unexpectedElementTypes: string[], missingIdFieldExceptions: string[], unexpectedIdFieldExceptions: string[], missingVariants: CircuitJsonVariantDiff[], unexpectedVariants: CircuitJsonVariantDiff[] }} CircuitJsonSchemaSnapshotComparison
267
- */
268
-
269
- /**
270
- * Validates serialized CircuitJSON element objects without external runtime
271
- * dependencies.
27
+ * Validates serialized CircuitJSON against the generated pinned upstream union.
272
28
  */
273
29
  export class CircuitJsonElementValidator {
274
30
  /**
275
31
  * Returns validation errors for a candidate model.
276
32
  * @param {unknown} value Candidate model.
33
+ * @param {{ freeze?: boolean }} [options] Validation options.
277
34
  * @returns {string[]}
278
35
  */
279
- static validateModel(value) {
36
+ static validateModel(value, options = {}) {
280
37
  if (!Array.isArray(value)) {
281
38
  return ['Expected a CircuitJSON element array.']
282
39
  }
283
-
284
- return value.flatMap((element, index) =>
285
- CircuitJsonElementValidator.validateElement(element, index)
286
- )
40
+ const traversal = new CircuitJsonModelFreezeTraversal(
41
+ value,
42
+ options.freeze === true
43
+ )
44
+ const errors = traversal.errors()
45
+ if (errors.length === 0) {
46
+ const length = Object.getOwnPropertyDescriptor(
47
+ value,
48
+ 'length'
49
+ ).value
50
+ for (let index = 0; index < length; index += 1) {
51
+ const element = Object.getOwnPropertyDescriptor(
52
+ value,
53
+ String(index)
54
+ ).value
55
+ errors.push(
56
+ ...CircuitJsonElementValidator.#validateElementData(
57
+ element,
58
+ index
59
+ )
60
+ )
61
+ }
62
+ }
63
+ traversal.commit(errors.length === 0)
64
+ return errors
287
65
  }
288
66
 
289
67
  /**
@@ -293,36 +71,39 @@ export class CircuitJsonElementValidator {
293
71
  * @returns {string[]}
294
72
  */
295
73
  static validateElement(value, index = -1) {
296
- const location = index >= 0 ? ' at index ' + index : ''
297
- if (!CircuitJsonElementValidator.#isObject(value)) {
298
- return ['Expected a CircuitJSON element object' + location + '.']
299
- }
300
-
301
- const type = String(value.type || '').trim()
302
- if (!type) {
303
- return ['CircuitJSON element type is required' + location + '.']
304
- }
305
-
306
- if (!KNOWN_ELEMENT_TYPES.has(type)) {
307
- return ['Unsupported CircuitJSON element type: ' + type + '.']
308
- }
309
-
310
- const errors = []
311
- CircuitJsonElementValidator.#validateId(value, type, errors)
312
- CircuitJsonElementValidator.#validateCoreShape(value, type, errors)
313
- return errors
74
+ const traversal = new CircuitJsonModelFreezeTraversal([value], false)
75
+ const shapeErrors = traversal.errors()
76
+ return shapeErrors.length
77
+ ? shapeErrors
78
+ : CircuitJsonElementValidator.#validateElementData(value, index)
314
79
  }
315
80
 
316
81
  /**
317
- * Returns all known serialized element type names.
82
+ * Returns all exact pinned upstream element type names.
318
83
  * @returns {string[]}
319
84
  */
320
85
  static knownElementTypes() {
86
+ return [...UPSTREAM_ELEMENT_TYPES]
87
+ }
88
+
89
+ /**
90
+ * Returns every accepted canonical type, including toolkit-owned additions.
91
+ * @returns {string[]}
92
+ */
93
+ static canonicalElementTypes() {
321
94
  return [...KNOWN_ELEMENT_TYPES]
322
95
  }
323
96
 
324
97
  /**
325
- * Returns id convention exceptions from the current schema snapshot.
98
+ * Returns canonical toolkit types newer than the pinned upstream union.
99
+ * @returns {string[]}
100
+ */
101
+ static extensionElementTypes() {
102
+ return CircuitJsonToolkitElementSchema.elementTypes()
103
+ }
104
+
105
+ /**
106
+ * Returns types without an unconditionally required conventional id field.
326
107
  * @returns {string[]}
327
108
  */
328
109
  static idFieldExceptions() {
@@ -330,7 +111,7 @@ export class CircuitJsonElementValidator {
330
111
  }
331
112
 
332
113
  /**
333
- * Returns schema metadata suitable for drift snapshots.
114
+ * Returns upstream-derived schema metadata suitable for drift snapshots.
334
115
  * @returns {CircuitJsonSchemaSnapshot}
335
116
  */
336
117
  static schemaSnapshot() {
@@ -342,7 +123,7 @@ export class CircuitJsonElementValidator {
342
123
  }
343
124
 
344
125
  /**
345
- * Returns known variant discriminants from the active schema metadata.
126
+ * Returns upstream-derived discriminant sets.
346
127
  * @returns {Record<string, string[]>}
347
128
  */
348
129
  static variantSets() {
@@ -372,7 +153,6 @@ export class CircuitJsonElementValidator {
372
153
  CircuitJsonElementValidator.#compareVariantSets(
373
154
  snapshot.variantSets || {}
374
155
  )
375
-
376
156
  return {
377
157
  matches:
378
158
  elementComparison.missing.length === 0 &&
@@ -391,549 +171,22 @@ export class CircuitJsonElementValidator {
391
171
  }
392
172
 
393
173
  /**
394
- * Validates the common id convention.
395
- * @param {Record<string, unknown>} element Element.
396
- * @param {string} type Element type.
397
- * @param {string[]} errors Error sink.
398
- * @returns {void}
399
- */
400
- static #validateId(element, type, errors) {
401
- if (ID_FIELD_EXCEPTIONS.has(type)) {
402
- return
403
- }
404
-
405
- const idField = type + '_id'
406
- if (!CircuitJsonElementValidator.#isNonEmptyString(element[idField])) {
407
- errors.push(type + ' ' + idField + ' is required.')
408
- }
409
- }
410
-
411
- /**
412
- * Validates type-specific fields used by core consumers.
413
- * @param {Record<string, unknown>} element Element.
414
- * @param {string} type Element type.
415
- * @param {string[]} errors Error sink.
416
- * @returns {void}
417
- */
418
- static #validateCoreShape(element, type, errors) {
419
- if (type === 'source_component') {
420
- CircuitJsonElementValidator.#validateSourceComponent(
421
- element,
422
- errors
423
- )
424
- return
425
- }
426
-
427
- if (type === 'source_port') {
428
- CircuitJsonElementValidator.#validateSourcePort(element, errors)
429
- return
430
- }
431
-
432
- if (type === 'schematic_component') {
433
- CircuitJsonElementValidator.#validateSchematicComponent(
434
- element,
435
- errors
436
- )
437
- return
438
- }
439
-
440
- if (type === 'pcb_board') {
441
- CircuitJsonElementValidator.#validatePcbBoard(element, errors)
442
- return
443
- }
444
-
445
- if (type === 'pcb_component') {
446
- CircuitJsonElementValidator.#validatePcbComponent(element, errors)
447
- return
448
- }
449
-
450
- if (type === 'pcb_smtpad') {
451
- CircuitJsonElementValidator.#validatePcbSmtPad(element, errors)
452
- return
453
- }
454
-
455
- if (type === 'simulation_oscilloscope_trace') {
456
- CircuitJsonElementValidator.#validateSimulationOscilloscopeTrace(
457
- element,
458
- errors
459
- )
460
- }
461
- }
462
-
463
- /**
464
- * Validates a source component.
465
- * @param {Record<string, unknown>} element Element.
466
- * @param {string[]} errors Error sink.
467
- * @returns {void}
468
- */
469
- static #validateSourceComponent(element, errors) {
470
- CircuitJsonElementValidator.#requireString(
471
- element,
472
- 'source_component',
473
- 'name',
474
- errors
475
- )
476
-
477
- if (
478
- Object.hasOwn(element, 'supplier_part_numbers') &&
479
- !CircuitJsonElementValidator.#isPlainObject(
480
- element.supplier_part_numbers
481
- )
482
- ) {
483
- errors.push(
484
- 'source_component supplier_part_numbers must be an object.'
485
- )
486
- }
487
-
488
- if (
489
- Object.hasOwn(element, 'ftype') &&
490
- !SOURCE_COMPONENT_FTYPES.has(String(element.ftype || ''))
491
- ) {
492
- errors.push('source_component ftype is not supported.')
493
- }
494
- }
495
-
496
- /**
497
- * Validates a source port.
498
- * @param {Record<string, unknown>} element Element.
499
- * @param {string[]} errors Error sink.
500
- * @returns {void}
501
- */
502
- static #validateSourcePort(element, errors) {
503
- if (
504
- Object.hasOwn(element, 'pin_number') &&
505
- !Number.isFinite(element.pin_number)
506
- ) {
507
- errors.push('source_port pin_number must be a number.')
508
- }
509
- }
510
-
511
- /**
512
- * Validates a schematic component.
513
- * @param {Record<string, unknown>} element Element.
514
- * @param {string[]} errors Error sink.
515
- * @returns {void}
516
- */
517
- static #validateSchematicComponent(element, errors) {
518
- CircuitJsonElementValidator.#requirePoint(
519
- element,
520
- 'schematic_component',
521
- 'center',
522
- errors
523
- )
524
- CircuitJsonElementValidator.#requireSize(
525
- element,
526
- 'schematic_component',
527
- 'size',
528
- errors
529
- )
530
- }
531
-
532
- /**
533
- * Validates a PCB board.
534
- * @param {Record<string, unknown>} element Element.
535
- * @param {string[]} errors Error sink.
536
- * @returns {void}
537
- */
538
- static #validatePcbBoard(element, errors) {
539
- CircuitJsonElementValidator.#requirePoint(
540
- element,
541
- 'pcb_board',
542
- 'center',
543
- errors
544
- )
545
- }
546
-
547
- /**
548
- * Validates a PCB component.
549
- * @param {Record<string, unknown>} element Element.
550
- * @param {string[]} errors Error sink.
551
- * @returns {void}
552
- */
553
- static #validatePcbComponent(element, errors) {
554
- CircuitJsonElementValidator.#requireString(
555
- element,
556
- 'pcb_component',
557
- 'source_component_id',
558
- errors
559
- )
560
- CircuitJsonElementValidator.#requirePoint(
561
- element,
562
- 'pcb_component',
563
- 'center',
564
- errors
565
- )
566
- CircuitJsonElementValidator.#requireLayer(
567
- element,
568
- 'pcb_component',
569
- 'layer',
570
- errors
571
- )
572
- CircuitJsonElementValidator.#optionalAngle(
573
- element,
574
- 'pcb_component',
575
- 'rotation',
576
- errors
577
- )
578
- CircuitJsonElementValidator.#optionalLength(
579
- element,
580
- 'pcb_component',
581
- 'width',
582
- errors
583
- )
584
- CircuitJsonElementValidator.#optionalLength(
585
- element,
586
- 'pcb_component',
587
- 'height',
588
- errors
589
- )
590
- }
591
-
592
- /**
593
- * Validates an SMT pad.
594
- * @param {Record<string, unknown>} element Element.
595
- * @param {string[]} errors Error sink.
596
- * @returns {void}
597
- */
598
- static #validatePcbSmtPad(element, errors) {
599
- const shape = String(element.shape || '')
600
- if (!SMT_PAD_SHAPES.has(shape)) {
601
- errors.push(
602
- 'pcb_smtpad shape must be one of: ' +
603
- [...SMT_PAD_SHAPES].join(', ') +
604
- '.'
605
- )
606
- return
607
- }
608
-
609
- CircuitJsonElementValidator.#requireLayer(
610
- element,
611
- 'pcb_smtpad',
612
- 'layer',
613
- errors
614
- )
615
-
616
- if (shape === 'polygon') {
617
- if (!Array.isArray(element.points)) {
618
- errors.push('pcb_smtpad points is required.')
619
- }
620
- return
621
- }
622
-
623
- CircuitJsonElementValidator.#requireLength(
624
- element,
625
- 'pcb_smtpad',
626
- 'x',
627
- errors
628
- )
629
- CircuitJsonElementValidator.#requireLength(
630
- element,
631
- 'pcb_smtpad',
632
- 'y',
633
- errors
634
- )
635
-
636
- if (shape === 'circle') {
637
- if (
638
- !CircuitJsonElementValidator.#hasLength(element, 'radius') &&
639
- !CircuitJsonElementValidator.#hasLength(element, 'diameter') &&
640
- !(
641
- CircuitJsonElementValidator.#hasLength(element, 'width') &&
642
- CircuitJsonElementValidator.#hasLength(element, 'height')
643
- )
644
- ) {
645
- errors.push(
646
- 'pcb_smtpad radius, diameter, or width and height is required.'
647
- )
648
- }
649
- return
650
- }
651
-
652
- CircuitJsonElementValidator.#requireLength(
653
- element,
654
- 'pcb_smtpad',
655
- 'width',
656
- errors
657
- )
658
- CircuitJsonElementValidator.#requireLength(
659
- element,
660
- 'pcb_smtpad',
661
- 'height',
662
- errors
663
- )
664
-
665
- if (shape.startsWith('rotated_')) {
666
- CircuitJsonElementValidator.#requireAngle(
667
- element,
668
- 'pcb_smtpad',
669
- 'ccw_rotation',
670
- errors
671
- )
672
- }
673
-
674
- if (shape.endsWith('pill')) {
675
- CircuitJsonElementValidator.#optionalLength(
676
- element,
677
- 'pcb_smtpad',
678
- 'radius',
679
- errors
680
- )
681
- }
682
- }
683
-
684
- /**
685
- * Validates oscilloscope trace reference and unit constraints.
686
- * @param {Record<string, unknown>} element Element.
687
- * @param {string[]} errors Error sink.
688
- * @returns {void}
689
- */
690
- static #validateSimulationOscilloscopeTrace(element, errors) {
691
- const voltageReferences = [
692
- element.simulation_transient_voltage_graph_id,
693
- element.simulation_voltage_probe_id
694
- ].filter((value) =>
695
- CircuitJsonElementValidator.#isNonEmptyString(value)
696
- ).length
697
- const currentReferences = [
698
- element.simulation_transient_current_graph_id,
699
- element.simulation_current_probe_id
700
- ].filter((value) =>
701
- CircuitJsonElementValidator.#isNonEmptyString(value)
702
- ).length
703
-
704
- if (voltageReferences + currentReferences !== 1) {
705
- errors.push(
706
- 'simulation_oscilloscope_trace must reference exactly one graph or probe.'
707
- )
708
- }
709
- if (voltageReferences > 0 && Object.hasOwn(element, 'amps_per_div')) {
710
- errors.push(
711
- 'voltage oscilloscope traces must use volts_per_div, not amps_per_div.'
712
- )
713
- }
714
- if (currentReferences > 0 && Object.hasOwn(element, 'volts_per_div')) {
715
- errors.push(
716
- 'current oscilloscope traces must use amps_per_div, not volts_per_div.'
717
- )
718
- }
719
- CircuitJsonElementValidator.#optionalPositiveNumber(
720
- element,
721
- 'simulation_oscilloscope_trace',
722
- 'volts_per_div',
723
- errors
724
- )
725
- CircuitJsonElementValidator.#optionalPositiveNumber(
726
- element,
727
- 'simulation_oscilloscope_trace',
728
- 'amps_per_div',
729
- errors
730
- )
731
- }
732
-
733
- /**
734
- * Requires a non-empty string field.
735
- * @param {Record<string, unknown>} element Element.
736
- * @param {string} type Element type.
737
- * @param {string} field Field name.
738
- * @param {string[]} errors Error sink.
739
- * @returns {void}
740
- */
741
- static #requireString(element, type, field, errors) {
742
- if (!CircuitJsonElementValidator.#isNonEmptyString(element[field])) {
743
- errors.push(type + ' ' + field + ' is required.')
744
- }
745
- }
746
-
747
- /**
748
- * Requires a finite number field.
749
- * @param {Record<string, unknown>} element Element.
750
- * @param {string} type Element type.
751
- * @param {string} field Field name.
752
- * @param {string[]} errors Error sink.
753
- * @returns {void}
754
- */
755
- static #requireNumber(element, type, field, errors) {
756
- if (!Number.isFinite(element[field])) {
757
- errors.push(type + ' ' + field + ' is required.')
758
- }
759
- }
760
-
761
- /**
762
- * Requires a finite length field.
763
- * @param {Record<string, unknown>} element Element.
764
- * @param {string} type Element type.
765
- * @param {string} field Field name.
766
- * @param {string[]} errors Error sink.
767
- * @returns {void}
768
- */
769
- static #requireLength(element, type, field, errors) {
770
- if (CircuitJsonUnits.optionalLength(element[field]) === null) {
771
- errors.push(type + ' ' + field + ' is required.')
772
- }
773
- }
774
-
775
- /**
776
- * Returns true when a field is a finite length.
777
- * @param {Record<string, unknown>} element Element.
778
- * @param {string} field Field name.
779
- * @returns {boolean}
780
- */
781
- static #hasLength(element, field) {
782
- return CircuitJsonUnits.optionalLength(element[field]) !== null
783
- }
784
-
785
- /**
786
- * Requires a finite angle field.
787
- * @param {Record<string, unknown>} element Element.
788
- * @param {string} type Element type.
789
- * @param {string} field Field name.
790
- * @param {string[]} errors Error sink.
791
- * @returns {void}
792
- */
793
- static #requireAngle(element, type, field, errors) {
794
- if (CircuitJsonUnits.optionalAngle(element[field]) === null) {
795
- errors.push(type + ' ' + field + ' is required.')
796
- }
797
- }
798
-
799
- /**
800
- * Validates an optional finite length field.
801
- * @param {Record<string, unknown>} element Element.
802
- * @param {string} type Element type.
803
- * @param {string} field Field name.
804
- * @param {string[]} errors Error sink.
805
- * @returns {void}
806
- */
807
- static #optionalLength(element, type, field, errors) {
808
- if (
809
- Object.hasOwn(element, field) &&
810
- CircuitJsonUnits.optionalLength(element[field]) === null
811
- ) {
812
- errors.push(type + ' ' + field + ' must be a finite length.')
813
- }
814
- }
815
-
816
- /**
817
- * Validates an optional finite angle field.
818
- * @param {Record<string, unknown>} element Element.
819
- * @param {string} type Element type.
820
- * @param {string} field Field name.
821
- * @param {string[]} errors Error sink.
822
- * @returns {void}
823
- */
824
- static #optionalAngle(element, type, field, errors) {
825
- if (
826
- Object.hasOwn(element, field) &&
827
- CircuitJsonUnits.optionalAngle(element[field]) === null
828
- ) {
829
- errors.push(type + ' ' + field + ' must be a finite angle.')
830
- }
831
- }
832
-
833
- /**
834
- * Validates an optional positive finite number field.
835
- * @param {Record<string, unknown>} element Element.
836
- * @param {string} type Element type.
837
- * @param {string} field Field name.
838
- * @param {string[]} errors Error sink.
839
- * @returns {void}
840
- */
841
- static #optionalPositiveNumber(element, type, field, errors) {
842
- if (!Object.hasOwn(element, field)) {
843
- return
844
- }
845
- const value = Number(element[field])
846
- if (!Number.isFinite(value) || value <= 0) {
847
- errors.push(type + ' ' + field + ' must be a positive number.')
848
- }
849
- }
850
-
851
- /**
852
- * Requires a finite point object field.
853
- * @param {Record<string, unknown>} element Element.
854
- * @param {string} type Element type.
855
- * @param {string} field Field name.
856
- * @param {string[]} errors Error sink.
857
- * @returns {void}
858
- */
859
- static #requirePoint(element, type, field, errors) {
860
- const point = element[field]
861
- if (!CircuitJsonElementValidator.#isObject(point)) {
862
- errors.push(type + ' ' + field + ' is required.')
863
- return
864
- }
865
-
866
- if (CircuitJsonUnits.optionalPoint(point) === null) {
867
- errors.push(type + ' ' + field + ' is required.')
868
- }
869
- }
870
-
871
- /**
872
- * Requires a finite size object field.
873
- * @param {Record<string, unknown>} element Element.
874
- * @param {string} type Element type.
875
- * @param {string} field Field name.
876
- * @param {string[]} errors Error sink.
877
- * @returns {void}
174
+ * Validates one element after descriptor-safety traversal.
175
+ * @param {unknown} value Candidate element.
176
+ * @param {number} index Element index.
177
+ * @returns {string[]} Validation errors.
878
178
  */
879
- static #requireSize(element, type, field, errors) {
880
- const size = element[field]
881
- if (!CircuitJsonElementValidator.#isObject(size)) {
882
- errors.push(type + ' ' + field + ' is required.')
883
- return
179
+ static #validateElementData(value, index) {
180
+ const location = index >= 0 ? ` at index ${index}` : ''
181
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
182
+ return [`Expected a CircuitJSON element object${location}.`]
884
183
  }
885
-
886
- if (CircuitJsonUnits.optionalSize(size) === null) {
887
- errors.push(type + ' ' + field + ' is required.')
888
- }
889
- }
890
-
891
- /**
892
- * Requires a known layer field.
893
- * @param {Record<string, unknown>} element Element.
894
- * @param {string} type Element type.
895
- * @param {string} field Field name.
896
- * @param {string[]} errors Error sink.
897
- * @returns {void}
898
- */
899
- static #requireLayer(element, type, field, errors) {
900
- const layer =
901
- typeof element[field] === 'object' && element[field] !== null
902
- ? element[field].name
903
- : element[field]
904
- if (!LAYERS.has(String(layer || ''))) {
905
- errors.push(type + ' ' + field + ' is required.')
184
+ const typeValue = value.type
185
+ const type = typeof typeValue === 'string' ? typeValue.trim() : ''
186
+ if (!type) {
187
+ return [`CircuitJSON element type is required${location}.`]
906
188
  }
907
- }
908
-
909
- /**
910
- * Returns true for non-null objects.
911
- * @param {unknown} value Candidate.
912
- * @returns {boolean}
913
- */
914
- static #isObject(value) {
915
- return Boolean(value) && typeof value === 'object'
916
- }
917
-
918
- /**
919
- * Returns true for plain object values.
920
- * @param {unknown} value Candidate.
921
- * @returns {boolean}
922
- */
923
- static #isPlainObject(value) {
924
- return (
925
- CircuitJsonElementValidator.#isObject(value) &&
926
- !Array.isArray(value)
927
- )
928
- }
929
-
930
- /**
931
- * Returns true for non-empty strings.
932
- * @param {unknown} value Candidate.
933
- * @returns {boolean}
934
- */
935
- static #isNonEmptyString(value) {
936
- return typeof value === 'string' && value.trim().length > 0
189
+ return CircuitJsonUpstreamValidator.validate(value, type, location)
937
190
  }
938
191
 
939
192
  /**
@@ -954,37 +207,35 @@ export class CircuitJsonElementValidator {
954
207
  }
955
208
 
956
209
  /**
957
- * Compares expected and actual variant metadata sets.
210
+ * Compares expected and actual upstream variant sets.
958
211
  * @param {Record<string, string[]>} snapshotSets Snapshot variant sets.
959
212
  * @returns {{ missing: CircuitJsonVariantDiff[], unexpected: CircuitJsonVariantDiff[] }}
960
213
  */
961
214
  static #compareVariantSets(snapshotSets) {
962
215
  const missing = []
963
216
  const unexpected = []
964
- const setNames = new Set([
217
+ const names = new Set([
965
218
  ...SORTED_VARIANT_SET_NAMES,
966
- ...Object.keys(snapshotSets || {})
219
+ ...Object.keys(snapshotSets)
967
220
  ])
968
-
969
- for (const setName of [...setNames].sort()) {
221
+ for (const name of [...names].sort()) {
970
222
  const comparison = CircuitJsonElementValidator.#compareSets(
971
- new Set((snapshotSets?.[setName] || []).map(String)),
972
- new Set(VARIANT_SETS[setName] || [])
223
+ new Set((snapshotSets[name] || []).map(String)),
224
+ new Set(VARIANT_SETS[name] || [])
973
225
  )
974
226
  missing.push(
975
- ...comparison.missing.map((value) => ({
976
- set: setName,
977
- value
978
- }))
227
+ ...comparison.missing.map((value) => ({ set: name, value }))
979
228
  )
980
229
  unexpected.push(
981
230
  ...comparison.unexpected.map((value) => ({
982
- set: setName,
231
+ set: name,
983
232
  value
984
233
  }))
985
234
  )
986
235
  }
987
-
988
236
  return { missing, unexpected }
989
237
  }
990
238
  }
239
+
240
+ Object.freeze(CircuitJsonElementValidator.prototype)
241
+ Object.freeze(CircuitJsonElementValidator)