circuitjson-toolkit 1.0.17 → 1.1.1

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/NOTICE.md +31 -0
  2. package/README.md +274 -107
  3. package/docs/api.md +507 -96
  4. package/docs/capabilities.md +70 -0
  5. package/docs/migration/behaviors.md +45 -0
  6. package/docs/migration/parser.md +60 -0
  7. package/docs/migration/renderers.md +515 -0
  8. package/docs/migration/root.md +740 -0
  9. package/docs/migration.md +126 -0
  10. package/docs/model-format.md +175 -57
  11. package/docs/provenance.md +206 -0
  12. package/docs/release-notes-v1.1.0.md +154 -0
  13. package/docs/release-notes-v1.1.1.md +24 -0
  14. package/docs/testing.md +122 -7
  15. package/package.json +32 -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 +999 -0
  115. package/src/core/worker/ToolkitWorkerProtocol.mjs +412 -0
  116. package/src/core/worker/WorkerRequestData.mjs +650 -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
@@ -0,0 +1,790 @@
1
+ import { CircuitJsonDocumentContext } from './context/CircuitJsonDocumentContext.mjs'
2
+ import { ToolkitDiagnostic } from './contracts/ToolkitDiagnostic.mjs'
3
+ import { ToolkitError } from './contracts/ToolkitError.mjs'
4
+ import { ToolkitProgress } from './contracts/ToolkitProgress.mjs'
5
+ import { SimulationParameterCloner } from './simulation/SimulationParameterCloner.mjs'
6
+ import { SpiceSimulationService } from './spice/SpiceSimulationService.mjs'
7
+
8
+ const MODEL_TYPES = new Set([
9
+ 'simulation_current_probe',
10
+ 'simulation_current_source',
11
+ 'simulation_op_amp',
12
+ 'simulation_oscilloscope_trace',
13
+ 'simulation_switch',
14
+ 'simulation_voltage_probe',
15
+ 'simulation_voltage_source'
16
+ ])
17
+ const EXPORT = {
18
+ id: 'simulation-circuitjson-json',
19
+ format: 'json',
20
+ mediaType: 'application/json;charset=utf-8',
21
+ fileExtension: '.json'
22
+ }
23
+ const ABORTED_GETTER = Object.getOwnPropertyDescriptor(
24
+ AbortSignal.prototype,
25
+ 'aborted'
26
+ )?.get
27
+ const ADD_EVENT_LISTENER = EventTarget.prototype.addEventListener
28
+ const REMOVE_EVENT_LISTENER = EventTarget.prototype.removeEventListener
29
+ const APPLY = Reflect.apply
30
+
31
+ /**
32
+ * Exposes data-only simulation discovery, export, and injected execution.
33
+ */
34
+ export class SimulationService {
35
+ /**
36
+ * Builds a canonical simulation definition from CircuitJSON rows.
37
+ * @param {unknown} document DocumentInput or prepared context.
38
+ * @param {Record<string, any>} [options] Reserved build options.
39
+ * @returns {{ schema: string, circuits: object[], analyses: object[], models: object[], diagnostics: object[], statistics: object }} Simulation definition.
40
+ */
41
+ static build(document, options = {}) {
42
+ const context = SimulationService.#context(document, options)
43
+ return SimulationService.#definition(context)
44
+ }
45
+
46
+ /**
47
+ * Exports one canonical simulation definition file.
48
+ * @param {unknown} document DocumentInput or prepared context.
49
+ * @param {Record<string, any>} request Export request.
50
+ * @param {Record<string, any>} [options] Reserved build options.
51
+ * @returns {{ fileName: string, mediaType: string, data: string, diagnostics: object[] }} File result.
52
+ */
53
+ static export(document, request, options = {}) {
54
+ const normalized = SimulationService.#exportRequest(request)
55
+ const context = SimulationService.#context(document, options)
56
+ const built = SimulationService.#built(context)
57
+ const capability = SimulationService.#exports(built).find(
58
+ (entry) => entry.id === normalized.id
59
+ )
60
+ if (!capability || capability.status !== 'available') {
61
+ throw SimulationService.#unavailable(capability?.reason)
62
+ }
63
+ const simulation = SimulationService.#definition(context)
64
+ return {
65
+ fileName:
66
+ SimulationService.#fileBase(context.source.fileName) +
67
+ '-circuitjson.json',
68
+ mediaType: capability.mediaType,
69
+ data: JSON.stringify(simulation, null, 2) + '\n',
70
+ diagnostics: structuredClone(simulation.diagnostics)
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Runs one analysis only through an explicitly injected engine.
76
+ * @param {unknown} document DocumentInput or prepared context.
77
+ * @param {Record<string, any>} request Analysis request.
78
+ * @param {{ engine?: object, signal?: AbortSignal, onProgress?: Function }} [options] Execution dependencies.
79
+ * @returns {Promise<{ schema: string, status: string, traces: object[], measurements: object, diagnostics: object[], statistics: object }>} Simulation result.
80
+ */
81
+ static async run(document, request, options = {}) {
82
+ const normalizedRequest = SimulationService.#runRequest(request)
83
+ const normalizedOptions = SimulationService.#runOptions(options)
84
+ const engine = normalizedOptions.engine
85
+ const run = SimulationService.#method(engine, 'run')
86
+ const simulate = SimulationService.#method(engine, 'simulate')
87
+ if (!run && !simulate) throw SimulationService.#unavailable()
88
+ SimulationService.#throwIfAborted(normalizedOptions.signal)
89
+ let progress = SimulationService.#progress(
90
+ normalizedOptions.onProgress,
91
+ {
92
+ stage: 'detect',
93
+ detail: 'simulation-request',
94
+ completed: 0,
95
+ total: 4
96
+ },
97
+ null
98
+ )
99
+ SimulationService.#throwIfAborted(normalizedOptions.signal)
100
+
101
+ const context = SimulationService.#context(document, {})
102
+ const simulation = SimulationService.build(context)
103
+ const analysis = simulation.analyses.find(
104
+ (entry) =>
105
+ String(entry.simulation_experiment_id || '') ===
106
+ normalizedRequest.analysisId
107
+ )
108
+ if (!analysis) {
109
+ throw SimulationService.#unavailable(
110
+ 'The requested simulation analysis is unavailable.'
111
+ )
112
+ }
113
+ progress = SimulationService.#progress(
114
+ normalizedOptions.onProgress,
115
+ {
116
+ stage: 'decode',
117
+ detail: 'simulation-definition',
118
+ completed: 1,
119
+ total: 4
120
+ },
121
+ progress
122
+ )
123
+ SimulationService.#throwIfAborted(normalizedOptions.signal)
124
+ const supports = SimulationService.#method(engine, 'supportsAnalysis')
125
+ let supported = true
126
+ if (supports) {
127
+ const supportAnalysis = structuredClone(analysis)
128
+ const supportParameters = structuredClone(
129
+ normalizedRequest.parameters
130
+ )
131
+ const operation = SimulationService.#invokeEngine(() =>
132
+ supports(supportAnalysis, supportParameters, {
133
+ signal: normalizedOptions.signal
134
+ })
135
+ )
136
+ supported = await SimulationService.#raceWithAbort(
137
+ operation,
138
+ normalizedOptions.signal
139
+ )
140
+ }
141
+ if (!supported) {
142
+ throw SimulationService.#unavailable(
143
+ 'The injected engine does not support this analysis.'
144
+ )
145
+ }
146
+ SimulationService.#throwIfAborted(normalizedOptions.signal)
147
+ progress = SimulationService.#progress(
148
+ normalizedOptions.onProgress,
149
+ {
150
+ stage: 'project',
151
+ detail: 'simulation-execution',
152
+ completed: 2,
153
+ total: 4
154
+ },
155
+ progress
156
+ )
157
+ SimulationService.#throwIfAborted(normalizedOptions.signal)
158
+
159
+ const engineRequest = run
160
+ ? {
161
+ schema: 'ecad-toolkit.simulation-run.v1',
162
+ document: context.model,
163
+ simulation,
164
+ analysis: structuredClone(analysis),
165
+ analysisId: normalizedRequest.analysisId,
166
+ parameters: structuredClone(normalizedRequest.parameters),
167
+ signal: normalizedOptions.signal
168
+ }
169
+ : null
170
+ const operation = run
171
+ ? SimulationService.#invokeEngine(() => run(engineRequest))
172
+ : SimulationService.#simulate(engine, normalizedRequest.parameters)
173
+ const raw = await SimulationService.#raceWithAbort(
174
+ operation,
175
+ normalizedOptions.signal
176
+ )
177
+ SimulationService.#throwIfAborted(normalizedOptions.signal)
178
+ progress = SimulationService.#progress(
179
+ normalizedOptions.onProgress,
180
+ {
181
+ stage: 'validate',
182
+ detail: 'simulation-result',
183
+ completed: 3,
184
+ total: 4
185
+ },
186
+ progress
187
+ )
188
+ SimulationService.#throwIfAborted(normalizedOptions.signal)
189
+ const result = SimulationService.#result(raw, context)
190
+ SimulationService.#progress(
191
+ normalizedOptions.onProgress,
192
+ {
193
+ stage: 'complete',
194
+ detail: 'simulation-result',
195
+ completed: 4,
196
+ total: 4
197
+ },
198
+ progress
199
+ )
200
+ return result
201
+ }
202
+
203
+ /**
204
+ * Builds a detached canonical simulation definition from cached families.
205
+ * @param {CircuitJsonDocumentContext} context Prepared context.
206
+ * @returns {{ schema: string, circuits: object[], analyses: object[], models: object[], diagnostics: object[], statistics: object }} Simulation definition.
207
+ */
208
+ static #definition(context) {
209
+ const built = SimulationService.#built(context)
210
+ return {
211
+ schema: 'ecad-toolkit.simulation.v1',
212
+ circuits: structuredClone(built.circuits),
213
+ analyses: structuredClone(built.analyses),
214
+ models: structuredClone(built.models),
215
+ diagnostics: structuredClone(built.diagnostics),
216
+ statistics: SimulationService.#statistics(context)
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Builds the request-scoped internal simulation families.
222
+ * @param {CircuitJsonDocumentContext} context Prepared context.
223
+ * @returns {Record<string, any>} Internal simulation definition.
224
+ */
225
+ static #built(context) {
226
+ return context.getOrCreateDerived('simulation', 'build-v1', () => {
227
+ const elements = context.getIndex('elements').elements || []
228
+ const simulationRows = elements.filter((element) =>
229
+ String(element.type || '').startsWith('simulation_')
230
+ )
231
+ return {
232
+ circuits: simulationRows.filter(
233
+ (element) => element.type === 'simulation_spice_subcircuit'
234
+ ),
235
+ analyses: simulationRows.filter(
236
+ (element) => element.type === 'simulation_experiment'
237
+ ),
238
+ models: simulationRows.filter((element) =>
239
+ MODEL_TYPES.has(element.type)
240
+ ),
241
+ diagnostics: simulationRows
242
+ .filter((element) =>
243
+ /(?:error|warning)/u.test(String(element.type || ''))
244
+ )
245
+ .map((element) =>
246
+ ToolkitDiagnostic.create({
247
+ severity: String(element.type).includes('warning')
248
+ ? 'warning'
249
+ : 'error',
250
+ code: String(
251
+ element.error_type ||
252
+ element.warning_type ||
253
+ element.type ||
254
+ 'simulation_diagnostic'
255
+ ),
256
+ message: String(
257
+ element.message || 'Simulation diagnostic.'
258
+ )
259
+ })
260
+ )
261
+ }
262
+ })
263
+ }
264
+
265
+ /**
266
+ * Prepares one context and element index.
267
+ * @param {unknown} document Document input.
268
+ * @param {unknown} options Reserved options.
269
+ * @returns {CircuitJsonDocumentContext} Prepared context.
270
+ */
271
+ static #context(document, options) {
272
+ SimulationService.#record(options, new Set())
273
+ try {
274
+ return CircuitJsonDocumentContext.prepare(document, {
275
+ indexes: ['elements']
276
+ })
277
+ } catch (error) {
278
+ throw ToolkitError.from(error, {
279
+ code: 'ERR_SIMULATION_DOCUMENT',
280
+ category: 'validation',
281
+ format: 'circuitjson'
282
+ })
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Builds simulation export availability rows.
288
+ * @param {Record<string, any>} built Internal simulation definition.
289
+ * @returns {object[]} Export capability rows.
290
+ */
291
+ static #exports(built) {
292
+ const available =
293
+ built.circuits.length ||
294
+ built.analyses.length ||
295
+ built.models.length
296
+ return [
297
+ {
298
+ ...EXPORT,
299
+ status: available ? 'available' : 'unavailable',
300
+ reason: available
301
+ ? ''
302
+ : 'No simulation definition is available.'
303
+ }
304
+ ]
305
+ }
306
+
307
+ /**
308
+ * Normalizes one simulation export request.
309
+ * @param {unknown} request Request candidate.
310
+ * @returns {{ id: string, options: object }} Request.
311
+ */
312
+ static #exportRequest(request) {
313
+ const normalized = SimulationService.#record(
314
+ request,
315
+ new Set(['id', 'options'])
316
+ )
317
+ if (
318
+ typeof normalized.id !== 'string' ||
319
+ !normalized.id.trim() ||
320
+ normalized.id.length > 256
321
+ ) {
322
+ throw SimulationService.#requestError(
323
+ 'Simulation export id must be a bounded string.'
324
+ )
325
+ }
326
+ return {
327
+ id: normalized.id.trim(),
328
+ options: SimulationService.#record(
329
+ normalized.options ?? {},
330
+ new Set()
331
+ )
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Normalizes one analysis request.
337
+ * @param {unknown} request Request candidate.
338
+ * @returns {{ analysisId: string, parameters: object }} Request.
339
+ */
340
+ static #runRequest(request) {
341
+ const normalized = SimulationService.#record(
342
+ request,
343
+ new Set(['analysisId', 'parameters'])
344
+ )
345
+ if (
346
+ typeof normalized.analysisId !== 'string' ||
347
+ !normalized.analysisId.trim() ||
348
+ normalized.analysisId.length > 256
349
+ ) {
350
+ throw SimulationService.#requestError(
351
+ 'Simulation analysisId must be a bounded string.'
352
+ )
353
+ }
354
+ const parameters = SimulationService.#cloneRecord(
355
+ normalized.parameters ?? {}
356
+ )
357
+ return { analysisId: normalized.analysisId.trim(), parameters }
358
+ }
359
+
360
+ /**
361
+ * Normalizes injected execution dependencies without invoking accessors.
362
+ * @param {unknown} options Options candidate.
363
+ * @returns {{ engine: object | null, signal: AbortSignal | null, onProgress: Function | null }} Options.
364
+ */
365
+ static #runOptions(options) {
366
+ const normalized = SimulationService.#record(
367
+ options,
368
+ new Set(['engine', 'signal', 'onProgress'])
369
+ )
370
+ if (
371
+ normalized.onProgress !== undefined &&
372
+ typeof normalized.onProgress !== 'function'
373
+ ) {
374
+ throw SimulationService.#requestError(
375
+ 'Simulation onProgress must be a function.'
376
+ )
377
+ }
378
+ if (
379
+ normalized.signal !== undefined &&
380
+ normalized.signal !== null &&
381
+ !SimulationService.#isAbortSignal(normalized.signal)
382
+ ) {
383
+ throw SimulationService.#requestError(
384
+ 'Simulation signal must be an AbortSignal.'
385
+ )
386
+ }
387
+ return {
388
+ engine: normalized.engine || null,
389
+ signal: normalized.signal || null,
390
+ onProgress: normalized.onProgress || null
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Brand-checks an AbortSignal through its built-in state getter.
396
+ * @param {unknown} value Signal candidate.
397
+ * @returns {boolean} Whether the value owns AbortSignal internal state.
398
+ */
399
+ static #isAbortSignal(value) {
400
+ try {
401
+ return (
402
+ typeof ABORTED_GETTER === 'function' &&
403
+ typeof ABORTED_GETTER.call(value) === 'boolean'
404
+ )
405
+ } catch {
406
+ return false
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Reads cancellation state through the built-in AbortSignal getter.
412
+ * @param {AbortSignal | null} signal Validated signal.
413
+ * @returns {boolean} Whether cancellation was requested.
414
+ */
415
+ static #isAborted(signal) {
416
+ if (!signal) return false
417
+ try {
418
+ return ABORTED_GETTER.call(signal)
419
+ } catch {
420
+ throw SimulationService.#requestError(
421
+ 'Simulation signal could not be inspected safely.'
422
+ )
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Races pending engine work against a genuine AbortSignal.
428
+ * @param {unknown} operation Promise or result to observe.
429
+ * @param {AbortSignal | null} signal Validated signal.
430
+ * @returns {Promise<unknown>} Operation result or typed cancellation.
431
+ */
432
+ static #raceWithAbort(operation, signal) {
433
+ const pending = Promise.resolve(operation)
434
+ if (!signal) return pending
435
+ if (SimulationService.#isAborted(signal)) {
436
+ pending.catch(() => {})
437
+ return Promise.reject(SimulationService.#cancelled())
438
+ }
439
+
440
+ return new Promise((resolve, reject) => {
441
+ let settled = false
442
+ let listening = false
443
+ const cleanup = () => {
444
+ if (!listening) return
445
+ REMOVE_EVENT_LISTENER.call(signal, 'abort', onAbort)
446
+ listening = false
447
+ }
448
+ const settle = (callback, value) => {
449
+ if (settled) return
450
+ settled = true
451
+ cleanup()
452
+ callback(value)
453
+ }
454
+ const onAbort = () => settle(reject, SimulationService.#cancelled())
455
+
456
+ pending.then(
457
+ (value) => settle(resolve, value),
458
+ (error) => settle(reject, error)
459
+ )
460
+ try {
461
+ ADD_EVENT_LISTENER.call(signal, 'abort', onAbort, {
462
+ once: true
463
+ })
464
+ listening = true
465
+ if (SimulationService.#isAborted(signal)) onAbort()
466
+ } catch (error) {
467
+ settle(
468
+ reject,
469
+ ToolkitError.from(error, {
470
+ code: 'ERR_SIMULATION_REQUEST',
471
+ category: 'validation',
472
+ format: 'circuitjson'
473
+ })
474
+ )
475
+ }
476
+ })
477
+ }
478
+
479
+ /**
480
+ * Copies a bounded plain parameter record into isolated data.
481
+ * @param {unknown} value Parameter candidate.
482
+ * @returns {object} Clone-safe parameters.
483
+ */
484
+ static #cloneRecord(value) {
485
+ return SimulationParameterCloner.cloneRecord(value)
486
+ }
487
+
488
+ /**
489
+ * Reads an accessor-free plain record.
490
+ * @param {unknown} value Record candidate.
491
+ * @param {Set<string>} allowed Allowed keys.
492
+ * @returns {Record<string, any>} Safe shallow copy.
493
+ */
494
+ static #record(value, allowed) {
495
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
496
+ throw SimulationService.#requestError(
497
+ 'Simulation options must be a plain object.'
498
+ )
499
+ }
500
+ let prototype
501
+ let descriptors
502
+ try {
503
+ prototype = Object.getPrototypeOf(value)
504
+ descriptors = Object.getOwnPropertyDescriptors(value)
505
+ } catch {
506
+ throw SimulationService.#requestError(
507
+ 'Simulation options could not be inspected safely.'
508
+ )
509
+ }
510
+ if (prototype !== Object.prototype && prototype !== null) {
511
+ throw SimulationService.#requestError(
512
+ 'Simulation options must be a plain object.'
513
+ )
514
+ }
515
+ const result = {}
516
+ for (const key of Reflect.ownKeys(descriptors)) {
517
+ const descriptor = descriptors[key]
518
+ if (
519
+ typeof key !== 'string' ||
520
+ !allowed.has(key) ||
521
+ descriptor.enumerable !== true ||
522
+ descriptor.get ||
523
+ descriptor.set
524
+ ) {
525
+ throw SimulationService.#requestError(
526
+ 'Simulation options contain an unsupported field.'
527
+ )
528
+ }
529
+ result[key] = descriptor.value
530
+ }
531
+ return result
532
+ }
533
+
534
+ /**
535
+ * Resolves one data method without executing accessor properties.
536
+ * @param {unknown} target Method owner.
537
+ * @param {string} name Method name.
538
+ * @returns {Function | null} Bound method.
539
+ */
540
+ static #method(target, name) {
541
+ if (!target || !['object', 'function'].includes(typeof target)) {
542
+ return null
543
+ }
544
+ let owner = target
545
+ for (let depth = 0; owner && depth < 16; depth += 1) {
546
+ let descriptor
547
+ try {
548
+ descriptor = Object.getOwnPropertyDescriptor(owner, name)
549
+ owner = Object.getPrototypeOf(owner)
550
+ } catch {
551
+ return null
552
+ }
553
+ if (!descriptor) continue
554
+ if (descriptor.get || descriptor.set) return null
555
+ if (typeof descriptor.value !== 'function') return null
556
+ const method = descriptor.value
557
+ return (...args) => APPLY(method, target, args)
558
+ }
559
+ return null
560
+ }
561
+
562
+ /**
563
+ * Normalizes every value rejected across one injected callable boundary.
564
+ * @param {Function} invoke Deferred injected callable invocation.
565
+ * @returns {Promise<unknown>} Injected operation result.
566
+ */
567
+ static async #invokeEngine(invoke) {
568
+ try {
569
+ return await invoke()
570
+ } catch (error) {
571
+ throw SimulationService.#engineFailure(error)
572
+ }
573
+ }
574
+
575
+ /**
576
+ * Runs a legacy injected SPICE engine with an explicit netlist parameter.
577
+ * @param {object} engine Injected simulation engine.
578
+ * @param {Record<string, any>} parameters Analysis parameters.
579
+ * @returns {Promise<object>} Legacy simulation result.
580
+ */
581
+ static async #simulate(engine, parameters) {
582
+ const spiceString = parameters.spiceString
583
+ if (
584
+ typeof spiceString !== 'string' ||
585
+ !spiceString.trim() ||
586
+ spiceString.length > 10_000_000
587
+ ) {
588
+ throw SimulationService.#unavailable(
589
+ 'The injected SPICE engine requires parameters.spiceString.'
590
+ )
591
+ }
592
+ return new SpiceSimulationService({ engine }).simulate(spiceString)
593
+ }
594
+
595
+ /**
596
+ * Normalizes a generic or legacy engine result.
597
+ * @param {unknown} raw Engine result.
598
+ * @param {CircuitJsonDocumentContext} context Prepared context.
599
+ * @returns {Record<string, any>} Canonical simulation result.
600
+ */
601
+ static #result(raw, context) {
602
+ if (!raw || typeof raw !== 'object') {
603
+ throw new ToolkitError('Simulation engine returned no result.', {
604
+ code: 'ERR_SIMULATION_RESULT',
605
+ category: 'runtime',
606
+ format: 'circuitjson'
607
+ })
608
+ }
609
+ let traces
610
+ let measurements
611
+ let diagnostics
612
+ let status
613
+ try {
614
+ traces = structuredClone(
615
+ raw.traces ?? raw.simulationResultCircuitJson ?? []
616
+ )
617
+ measurements = structuredClone(
618
+ raw.measurements ?? raw.graphSummary ?? {}
619
+ )
620
+ const diagnosticRows = raw.diagnostics ?? []
621
+ if (!Array.isArray(diagnosticRows)) {
622
+ throw new TypeError('Simulation diagnostics must be an array.')
623
+ }
624
+ diagnostics = []
625
+ for (let index = 0; index < diagnosticRows.length; index += 1) {
626
+ diagnostics.push(
627
+ ToolkitDiagnostic.create(diagnosticRows[index])
628
+ )
629
+ }
630
+ const inferredStatus = diagnostics.some(
631
+ (entry) => entry.severity === 'error'
632
+ )
633
+ ? 'failed'
634
+ : 'success'
635
+ status = String(raw.status || inferredStatus)
636
+ } catch {
637
+ throw new ToolkitError(
638
+ 'Simulation engine result must be clone-safe data.',
639
+ {
640
+ code: 'ERR_SIMULATION_RESULT',
641
+ category: 'runtime',
642
+ format: 'circuitjson'
643
+ }
644
+ )
645
+ }
646
+ if (
647
+ !Array.isArray(traces) ||
648
+ !Array.isArray(diagnostics) ||
649
+ !measurements ||
650
+ typeof measurements !== 'object' ||
651
+ Array.isArray(measurements)
652
+ ) {
653
+ throw new ToolkitError('Simulation engine result is malformed.', {
654
+ code: 'ERR_SIMULATION_RESULT',
655
+ category: 'runtime',
656
+ format: 'circuitjson'
657
+ })
658
+ }
659
+ if (!['success', 'failed', 'cancelled'].includes(status)) {
660
+ throw new ToolkitError('Simulation status is invalid.', {
661
+ code: 'ERR_SIMULATION_RESULT',
662
+ category: 'runtime',
663
+ format: 'circuitjson'
664
+ })
665
+ }
666
+ return {
667
+ schema: 'ecad-toolkit.simulation-result.v1',
668
+ status,
669
+ traces,
670
+ measurements,
671
+ diagnostics,
672
+ statistics: {
673
+ ...SimulationService.#statistics(context),
674
+ engineRuns: 1
675
+ }
676
+ }
677
+ }
678
+
679
+ /**
680
+ * Emits one canonical progress event.
681
+ * @param {Function | null} callback Progress callback.
682
+ * @param {Record<string, any>} fields Progress fields.
683
+ * @param {Record<string, any> | null} previous Previous progress row.
684
+ * @returns {Record<string, any> | null} Current or previous progress row.
685
+ */
686
+ static #progress(callback, fields, previous) {
687
+ if (!callback) return previous
688
+ const progress = ToolkitProgress.create(fields, previous)
689
+ callback(progress)
690
+ return progress
691
+ }
692
+
693
+ /**
694
+ * Throws a typed cancellation when the signal is already aborted.
695
+ * @param {AbortSignal | null} signal Abort signal.
696
+ * @returns {void}
697
+ */
698
+ static #throwIfAborted(signal) {
699
+ if (SimulationService.#isAborted(signal)) {
700
+ throw SimulationService.#cancelled()
701
+ }
702
+ }
703
+
704
+ /**
705
+ * Builds stable context work counters.
706
+ * @param {CircuitJsonDocumentContext} context Prepared context.
707
+ * @returns {Record<string, number>} Statistics.
708
+ */
709
+ static #statistics(context) {
710
+ const statistics = context.statistics
711
+ return {
712
+ validationPasses: statistics.validationPasses,
713
+ elementIndexBuilds: statistics.indexBuilds.elements || 0,
714
+ simulationBuilds:
715
+ statistics.derivedBuilds['simulation:build-v1'] || 0
716
+ }
717
+ }
718
+
719
+ /**
720
+ * Creates a safe output base name.
721
+ * @param {unknown} fileName Source file name.
722
+ * @returns {string} File base.
723
+ */
724
+ static #fileBase(fileName) {
725
+ const base = String(fileName || 'simulation')
726
+ .replaceAll('\\', '/')
727
+ .split('/')
728
+ .at(-1)
729
+ .replace(/\.[^.]+$/u, '')
730
+ .replace(/[^a-z0-9._-]+/giu, '-')
731
+ .replace(/^-|-$/gu, '')
732
+ return base || 'simulation'
733
+ }
734
+
735
+ /**
736
+ * Contains any injected-engine failure in a fresh runtime ToolkitError.
737
+ * @param {unknown} error Engine failure candidate.
738
+ * @returns {ToolkitError} Fresh normalized runtime failure.
739
+ */
740
+ static #engineFailure(error) {
741
+ const cause = ToolkitError.cloneSafeCause(error)
742
+ return new ToolkitError(cause?.message || 'Simulation engine failed.', {
743
+ code: 'ERR_SIMULATION_RUNTIME',
744
+ category: 'runtime',
745
+ format: 'circuitjson',
746
+ cause
747
+ })
748
+ }
749
+
750
+ /**
751
+ * Creates a typed request error.
752
+ * @param {string} message Failure message.
753
+ * @returns {ToolkitError} Typed error.
754
+ */
755
+ static #requestError(message) {
756
+ return new ToolkitError(message, {
757
+ code: 'ERR_SIMULATION_REQUEST',
758
+ category: 'validation',
759
+ format: 'circuitjson'
760
+ })
761
+ }
762
+
763
+ /**
764
+ * Creates an unavailable-capability error.
765
+ * @param {string} [reason] Failure reason.
766
+ * @returns {ToolkitError} Typed error.
767
+ */
768
+ static #unavailable(reason = '') {
769
+ return new ToolkitError(
770
+ reason || 'Simulation capability is unavailable.',
771
+ {
772
+ code: 'ERR_CAPABILITY_UNAVAILABLE',
773
+ category: 'unsupported',
774
+ format: 'circuitjson'
775
+ }
776
+ )
777
+ }
778
+
779
+ /**
780
+ * Creates a typed cancellation error.
781
+ * @returns {ToolkitError} Typed error.
782
+ */
783
+ static #cancelled() {
784
+ return new ToolkitError('Simulation was cancelled.', {
785
+ code: 'ERR_CANCELLED',
786
+ category: 'cancelled',
787
+ format: 'circuitjson'
788
+ })
789
+ }
790
+ }