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
@@ -0,0 +1,323 @@
1
+ import { CircuitJsonManufacturingBuilder } from './CircuitJsonManufacturingBuilder.mjs'
2
+ import { CircuitJsonManufacturingDownloadBuilder } from './CircuitJsonManufacturingDownloadBuilder.mjs'
3
+ import { CircuitJsonDocumentContext } from './context/CircuitJsonDocumentContext.mjs'
4
+ import { ToolkitError } from './contracts/ToolkitError.mjs'
5
+
6
+ const EXPORTS = [
7
+ {
8
+ id: 'fabrication-notes-json',
9
+ format: 'json',
10
+ mediaType: 'application/json;charset=utf-8',
11
+ fileExtension: '.json'
12
+ },
13
+ {
14
+ id: 'pick-place-csv',
15
+ format: 'csv',
16
+ mediaType: 'text/csv;charset=utf-8',
17
+ fileExtension: '.csv'
18
+ },
19
+ {
20
+ id: 'routing-dsn',
21
+ format: 'specctra-dsn',
22
+ mediaType: 'application/specctra-dsn',
23
+ fileExtension: '.dsn'
24
+ }
25
+ ]
26
+ const ROUTING_TYPES = [
27
+ 'pcb_board',
28
+ 'pcb_component',
29
+ 'pcb_smtpad',
30
+ 'pcb_trace',
31
+ 'pcb_via',
32
+ 'pcb_plated_hole',
33
+ 'pcb_trace_hint',
34
+ 'pcb_breakout_point',
35
+ 'source_net'
36
+ ]
37
+
38
+ /**
39
+ * Exposes lazy CircuitJSON manufacturing inspection and export services.
40
+ */
41
+ export class ManufacturingService {
42
+ /**
43
+ * Inspects canonical placement and fabrication metadata.
44
+ * @param {unknown} document DocumentInput or prepared context.
45
+ * @param {Record<string, any>} [options] Reserved service options.
46
+ * @returns {{ schema: string, placements: object[], fabricationNotes: object[], exports: object[], diagnostics: object[], statistics: object }} Manufacturing inspection.
47
+ */
48
+ static inspect(document, options = {}) {
49
+ const context = ManufacturingService.#context(document, options)
50
+ const manufacturing = ManufacturingService.#build(context)
51
+ return {
52
+ schema: 'ecad-toolkit.manufacturing.v1',
53
+ placements: structuredClone(manufacturing.pickAndPlaceRows),
54
+ fabricationNotes: structuredClone(manufacturing.fabricationNotes),
55
+ exports: ManufacturingService.#exports(manufacturing, context),
56
+ diagnostics: [],
57
+ statistics: ManufacturingService.#statistics(context)
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Lists every canonical manufacturing export and its availability.
63
+ * @param {unknown} document DocumentInput or prepared context.
64
+ * @param {Record<string, any>} [options] Reserved service options.
65
+ * @returns {object[]} Detached export capability rows.
66
+ */
67
+ static listExports(document, options = {}) {
68
+ const context = ManufacturingService.#context(document, options)
69
+ const manufacturing = ManufacturingService.#build(context)
70
+ return ManufacturingService.#exports(manufacturing, context)
71
+ }
72
+
73
+ /**
74
+ * Builds one available canonical manufacturing file.
75
+ * @param {unknown} document DocumentInput or prepared context.
76
+ * @param {Record<string, any>} request Export request.
77
+ * @param {Record<string, any>} [options] Reserved service options.
78
+ * @returns {{ fileName: string, mediaType: string, data: Uint8Array, diagnostics: object[] }} File result.
79
+ */
80
+ static export(document, request, options = {}) {
81
+ const normalized = ManufacturingService.#request(request)
82
+ const context = ManufacturingService.#context(document, options)
83
+ const manufacturing = ManufacturingService.#build(context)
84
+ const capability = ManufacturingService.#exports(
85
+ manufacturing,
86
+ context
87
+ ).find((entry) => entry.id === normalized.id)
88
+ if (!capability || capability.status !== 'available') {
89
+ throw ManufacturingService.#unavailable(capability?.reason)
90
+ }
91
+ try {
92
+ const built = CircuitJsonManufacturingDownloadBuilder.build(
93
+ {
94
+ fileName: context.source.fileName,
95
+ manufacturing
96
+ },
97
+ normalized.id,
98
+ normalized.options
99
+ )
100
+ return {
101
+ fileName: built.fileName,
102
+ mediaType: built.contentType,
103
+ data: new Uint8Array(built.bytes),
104
+ diagnostics: []
105
+ }
106
+ } catch (error) {
107
+ throw ToolkitError.from(error, {
108
+ code: 'ERR_MANUFACTURING_EXPORT',
109
+ category: 'runtime',
110
+ format: 'circuitjson'
111
+ })
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Prepares one shared context and its element index.
117
+ * @param {unknown} document Document input.
118
+ * @param {unknown} options Service options.
119
+ * @returns {CircuitJsonDocumentContext} Prepared context.
120
+ */
121
+ static #context(document, options) {
122
+ ManufacturingService.#record(options, new Set())
123
+ try {
124
+ return CircuitJsonDocumentContext.prepare(document, {
125
+ indexes: ['elements']
126
+ })
127
+ } catch (error) {
128
+ throw ToolkitError.from(error, {
129
+ code: 'ERR_MANUFACTURING_DOCUMENT',
130
+ category: 'validation',
131
+ format: 'circuitjson'
132
+ })
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Returns the one request-scoped manufacturing model.
138
+ * @param {CircuitJsonDocumentContext} context Prepared context.
139
+ * @returns {Record<string, any>} Internal manufacturing model.
140
+ */
141
+ static #build(context) {
142
+ return context.getOrCreateDerived(
143
+ 'manufacturing',
144
+ 'inspection-v1',
145
+ () =>
146
+ CircuitJsonManufacturingBuilder.build(
147
+ context.model,
148
+ context.getIndex('elements')
149
+ )
150
+ )
151
+ }
152
+
153
+ /**
154
+ * Computes stable format availability rows.
155
+ * @param {Record<string, any>} manufacturing Internal manufacturing model.
156
+ * @param {CircuitJsonDocumentContext} context Prepared context.
157
+ * @returns {object[]} Detached export capability rows.
158
+ */
159
+ static #exports(manufacturing, context) {
160
+ return EXPORTS.map((entry) => {
161
+ const reason = ManufacturingService.#reason(
162
+ entry.id,
163
+ manufacturing,
164
+ context
165
+ )
166
+ return {
167
+ ...entry,
168
+ status: reason ? 'unavailable' : 'available',
169
+ reason
170
+ }
171
+ })
172
+ }
173
+
174
+ /**
175
+ * Explains why one core export lacks source data.
176
+ * @param {string} id Export id.
177
+ * @param {Record<string, any>} manufacturing Internal manufacturing model.
178
+ * @param {CircuitJsonDocumentContext} context Prepared context.
179
+ * @returns {string} Empty string when available.
180
+ */
181
+ static #reason(id, manufacturing, context) {
182
+ if (id === 'pick-place-csv' && !manufacturing.pickAndPlaceRows.length) {
183
+ return 'No placement metadata is available.'
184
+ }
185
+ if (
186
+ id === 'fabrication-notes-json' &&
187
+ !manufacturing.fabricationNotes.length
188
+ ) {
189
+ return 'No fabrication note metadata is available.'
190
+ }
191
+ if (
192
+ id === 'routing-dsn' &&
193
+ (!manufacturing.routingDsn.trim() ||
194
+ !ManufacturingService.#hasRoutingData(context))
195
+ ) {
196
+ return 'No routing metadata is available.'
197
+ }
198
+ return ''
199
+ }
200
+
201
+ /**
202
+ * Returns whether the canonical model contains data represented by DSN.
203
+ * @param {CircuitJsonDocumentContext} context Prepared context.
204
+ * @returns {boolean} Whether a routing export is meaningful.
205
+ */
206
+ static #hasRoutingData(context) {
207
+ const byType = context.getIndex('elements').elementsByType
208
+ return ROUTING_TYPES.some((type) => (byType.get(type) || []).length)
209
+ }
210
+
211
+ /**
212
+ * Normalizes one safe manufacturing export request.
213
+ * @param {unknown} request Request candidate.
214
+ * @returns {{ id: string, options: Record<string, any> }} Request.
215
+ */
216
+ static #request(request) {
217
+ const normalized = ManufacturingService.#record(
218
+ request,
219
+ new Set(['id', 'options'])
220
+ )
221
+ const id = normalized.id
222
+ if (typeof id !== 'string' || !id.trim() || id.length > 256) {
223
+ throw ManufacturingService.#requestError(
224
+ 'Manufacturing export id must be a bounded string.'
225
+ )
226
+ }
227
+ const options = ManufacturingService.#record(
228
+ normalized.options ?? {},
229
+ new Set()
230
+ )
231
+ return { id: id.trim(), options }
232
+ }
233
+
234
+ /**
235
+ * Reads a plain accessor-free request record.
236
+ * @param {unknown} value Record candidate.
237
+ * @param {Set<string>} allowed Allowed own keys.
238
+ * @returns {Record<string, any>} Safe shallow copy.
239
+ */
240
+ static #record(value, allowed) {
241
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
242
+ throw ManufacturingService.#requestError(
243
+ 'Manufacturing options must be a plain object.'
244
+ )
245
+ }
246
+ let prototype
247
+ let descriptors
248
+ try {
249
+ prototype = Object.getPrototypeOf(value)
250
+ descriptors = Object.getOwnPropertyDescriptors(value)
251
+ } catch {
252
+ throw ManufacturingService.#requestError(
253
+ 'Manufacturing options could not be inspected safely.'
254
+ )
255
+ }
256
+ if (prototype !== Object.prototype && prototype !== null) {
257
+ throw ManufacturingService.#requestError(
258
+ 'Manufacturing options must be a plain object.'
259
+ )
260
+ }
261
+ const result = {}
262
+ for (const key of Reflect.ownKeys(descriptors)) {
263
+ const descriptor = descriptors[key]
264
+ if (
265
+ typeof key !== 'string' ||
266
+ !allowed.has(key) ||
267
+ descriptor.enumerable !== true ||
268
+ descriptor.get ||
269
+ descriptor.set
270
+ ) {
271
+ throw ManufacturingService.#requestError(
272
+ 'Manufacturing options contain an unsupported field.'
273
+ )
274
+ }
275
+ result[key] = descriptor.value
276
+ }
277
+ return result
278
+ }
279
+
280
+ /**
281
+ * Returns stable context work statistics.
282
+ * @param {CircuitJsonDocumentContext} context Prepared context.
283
+ * @returns {Record<string, number>} Statistics.
284
+ */
285
+ static #statistics(context) {
286
+ const statistics = context.statistics
287
+ return {
288
+ validationPasses: statistics.validationPasses,
289
+ elementIndexBuilds: statistics.indexBuilds.elements || 0,
290
+ manufacturingBuilds:
291
+ statistics.derivedBuilds['manufacturing:inspection-v1'] || 0
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Creates a typed request validation failure.
297
+ * @param {string} message Failure message.
298
+ * @returns {ToolkitError} Typed error.
299
+ */
300
+ static #requestError(message) {
301
+ return new ToolkitError(message, {
302
+ code: 'ERR_MANUFACTURING_REQUEST',
303
+ category: 'validation',
304
+ format: 'circuitjson'
305
+ })
306
+ }
307
+
308
+ /**
309
+ * Creates a typed unavailable-capability failure.
310
+ * @param {string} [reason] Capability reason.
311
+ * @returns {ToolkitError} Typed error.
312
+ */
313
+ static #unavailable(reason = '') {
314
+ return new ToolkitError(
315
+ reason || 'Manufacturing export is unavailable.',
316
+ {
317
+ code: 'ERR_CAPABILITY_UNAVAILABLE',
318
+ category: 'unsupported',
319
+ format: 'circuitjson'
320
+ }
321
+ )
322
+ }
323
+ }
@@ -0,0 +1,343 @@
1
+ import { DocumentResult } from './contracts/DocumentResult.mjs'
2
+ import { ToolkitDiagnostic } from './contracts/ToolkitDiagnostic.mjs'
3
+ import { ToolkitError } from './contracts/ToolkitError.mjs'
4
+ import { ToolkitProgress } from './contracts/ToolkitProgress.mjs'
5
+ import { AsyncInputOwnership } from './AsyncInputOwnership.mjs'
6
+ import { BinaryDataSnapshot } from './context/BinaryDataSnapshot.mjs'
7
+ import { ParserOptions } from './ParserOptions.mjs'
8
+ import { CircuitJsonLegacyNormalizer } from './context/CircuitJsonLegacyNormalizer.mjs'
9
+ import { ParserWorkerClient } from './worker/ParserWorkerClient.mjs'
10
+
11
+ const PROGRESS_MESSAGES = {
12
+ detect: 'Detecting CircuitJSON input.',
13
+ decode: 'Decoding CircuitJSON.',
14
+ validate: 'Validating CircuitJSON.',
15
+ complete: 'CircuitJSON parsing complete.'
16
+ }
17
+
18
+ /**
19
+ * Parses standalone CircuitJSON inputs into canonical document envelopes.
20
+ */
21
+ export class Parser {
22
+ /**
23
+ * Parses one CircuitJSON input synchronously.
24
+ * @param {{ fileName: string, data: string | ArrayBuffer | Uint8Array, assets?: object[] }} input Parser input.
25
+ * @param {Record<string, any>} [options] Common parser options.
26
+ * @returns {Record<string, any>} Canonical document result.
27
+ */
28
+ static parse(input, options = {}) {
29
+ try {
30
+ const normalized = ParserOptions.normalize(input, options)
31
+ if (normalized.options.worker === true) {
32
+ throw Parser.#workerSyncError(normalized.input.fileName)
33
+ }
34
+ return Parser.#parseNormalized(normalized)
35
+ } catch (error) {
36
+ throw Parser.#parseError(error, input)
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Parses one CircuitJSON input without throwing public parse failures.
42
+ * @param {{ fileName: string, data: string | ArrayBuffer | Uint8Array, assets?: object[] }} input Parser input.
43
+ * @param {Record<string, any>} [options] Common parser options.
44
+ * @returns {{ ok: true, value: Record<string, any> } | { ok: false, error: ToolkitError, diagnostics: object[] }} Discriminated parse result.
45
+ */
46
+ static tryParse(input, options = {}) {
47
+ try {
48
+ return { ok: true, value: Parser.parse(input, options) }
49
+ } catch (error) {
50
+ const normalized = Parser.#parseError(error, input)
51
+ return {
52
+ ok: false,
53
+ error: normalized,
54
+ diagnostics: [
55
+ ToolkitDiagnostic.create({
56
+ code: normalized.code,
57
+ severity: 'error',
58
+ message: normalized.message,
59
+ source: normalized.source
60
+ })
61
+ ]
62
+ }
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Parses one CircuitJSON input through the currently available direct path.
68
+ * @param {{ fileName: string, data: string | ArrayBuffer | Uint8Array, assets?: object[] }} input Parser input.
69
+ * @param {Record<string, any>} [options] Common parser options.
70
+ * @returns {Promise<Record<string, any>>} Canonical document result.
71
+ */
72
+ static async parseAsync(input, options = {}) {
73
+ let normalized
74
+ const inputOwned = AsyncInputOwnership.ownsParser(input)
75
+ try {
76
+ normalized = ParserOptions.normalize(input, options)
77
+ if (Parser.#isAborted(normalized.options.signal)) {
78
+ throw Parser.#cancelledError(normalized.input.fileName)
79
+ }
80
+ Parser.#assertReports(
81
+ normalized.options.reports,
82
+ normalized.input.fileName
83
+ )
84
+ } catch (error) {
85
+ throw Parser.#parseError(error, input)
86
+ }
87
+
88
+ const useWorker =
89
+ normalized.options.worker === true ||
90
+ (normalized.options.worker === 'auto' &&
91
+ normalized.options.retainSource !== 'reference' &&
92
+ ParserWorkerClient.isDefaultAvailable())
93
+ if (useWorker) {
94
+ const attempt = await ParserWorkerClient.parseDefault(
95
+ normalized.input,
96
+ normalized.options
97
+ )
98
+ if (attempt.ok) return attempt.value
99
+ if (normalized.options.worker !== 'auto' || !attempt.unavailable) {
100
+ throw attempt.error
101
+ }
102
+ ParserWorkerClient.disposeDefault()
103
+ }
104
+
105
+ try {
106
+ if (!inputOwned) {
107
+ normalized = Parser.#ownAsyncInput(normalized)
108
+ }
109
+ } catch (error) {
110
+ throw Parser.#parseError(error, input)
111
+ }
112
+
113
+ let progress = Parser.#progress(normalized, 'detect')
114
+ Parser.#assertNotCancelled(normalized)
115
+ progress = Parser.#progress(normalized, 'decode', progress)
116
+ Parser.#assertNotCancelled(normalized)
117
+ let model
118
+ try {
119
+ model = Parser.#decode(normalized)
120
+ } catch (error) {
121
+ throw Parser.#parseError(error, input)
122
+ }
123
+
124
+ Parser.#assertNotCancelled(normalized)
125
+ progress = Parser.#progress(normalized, 'validate', progress)
126
+ Parser.#assertNotCancelled(normalized)
127
+ let document
128
+ try {
129
+ document = Parser.#document(normalized, model)
130
+ } catch (error) {
131
+ throw Parser.#parseError(error, input)
132
+ }
133
+ Parser.#assertNotCancelled(normalized)
134
+ Parser.#progress(normalized, 'complete', progress)
135
+ Parser.#assertNotCancelled(normalized)
136
+ return document
137
+ }
138
+
139
+ /**
140
+ * Performs bounded detection for canonical CircuitJSON array inputs.
141
+ * @param {unknown} input Parser input candidate.
142
+ * @returns {boolean} Whether the input is supported.
143
+ */
144
+ static supports(input) {
145
+ return ParserOptions.supports(input)
146
+ }
147
+
148
+ /**
149
+ * Decodes and validates one normalized request exactly once.
150
+ * @param {{ input: { fileName: string, data: string | ArrayBuffer | Uint8Array, assets: object[] }, options: Record<string, any> }} normalized Normalized request.
151
+ * @returns {Record<string, any>} Canonical document result.
152
+ */
153
+ static #parseNormalized(normalized) {
154
+ Parser.#assertReports(
155
+ normalized.options.reports,
156
+ normalized.input.fileName
157
+ )
158
+ const model = Parser.#decode(normalized)
159
+ return Parser.#document(normalized, model)
160
+ }
161
+
162
+ /**
163
+ * Captures parser bytes and selected assets before the first host callback.
164
+ * @param {{ input: { fileName: string, data: string | ArrayBuffer | Uint8Array, assets: object[] }, options: { decodeAssets: string } }} normalized Normalized request.
165
+ * @returns {Record<string, any>} Request with one owned input boundary.
166
+ */
167
+ static #ownAsyncInput(normalized) {
168
+ const data = normalized.input.data
169
+ return {
170
+ ...normalized,
171
+ input: {
172
+ ...normalized.input,
173
+ data:
174
+ typeof data === 'string'
175
+ ? data
176
+ : BinaryDataSnapshot.clone(data),
177
+ assets: ParserOptions.assets(
178
+ normalized.input.assets,
179
+ normalized.options.decodeAssets
180
+ )
181
+ }
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Decodes one normalized payload and enforces the model-array boundary.
187
+ * @param {{ input: { data: string | ArrayBuffer | Uint8Array } }} normalized Normalized request.
188
+ * @returns {object[]} Decoded CircuitJSON model.
189
+ */
190
+ static #decode(normalized) {
191
+ let model = JSON.parse(ParserOptions.text(normalized.input.data))
192
+ if (!Array.isArray(model)) {
193
+ throw new TypeError('Expected a CircuitJSON element array.')
194
+ }
195
+ if (normalized.options.extensions === 'full') {
196
+ model = CircuitJsonLegacyNormalizer.normalize(model, {
197
+ owned: true
198
+ })
199
+ }
200
+ return model
201
+ }
202
+
203
+ /**
204
+ * Validates one model and creates its canonical document envelope.
205
+ * @param {{ input: { fileName: string, assets: object[] }, sourceReference: object, options: { decodeAssets: string, retainSource: string } }} normalized Normalized request.
206
+ * @param {object[]} model Decoded CircuitJSON model.
207
+ * @returns {Record<string, any>} Canonical document result.
208
+ */
209
+ static #document(normalized, model) {
210
+ const runtime =
211
+ normalized.options.retainSource === 'reference'
212
+ ? { sourceReference: normalized.sourceReference }
213
+ : {}
214
+ return DocumentResult.createValidated(
215
+ {
216
+ fileName: normalized.input.fileName,
217
+ fileType: 'circuitjson',
218
+ format: 'circuitjson',
219
+ model,
220
+ extensions: {},
221
+ assets: ParserOptions.assets(
222
+ normalized.input.assets,
223
+ normalized.options.decodeAssets
224
+ )
225
+ },
226
+ runtime
227
+ )
228
+ }
229
+
230
+ /**
231
+ * Emits one ordered direct-parser progress row when requested.
232
+ * @param {{ options: { onProgress?: Function } }} normalized Normalized request.
233
+ * @param {'detect' | 'decode' | 'validate' | 'complete'} stage Common progress stage.
234
+ * @param {Record<string, any> | null} [previous] Previous progress row.
235
+ * @returns {Record<string, any> | null} Emitted row or the prior state.
236
+ */
237
+ static #progress(normalized, stage, previous = null) {
238
+ if (!normalized.options.onProgress) return previous
239
+ const row = ToolkitProgress.create(
240
+ { stage, message: PROGRESS_MESSAGES[stage] },
241
+ previous
242
+ )
243
+ normalized.options.onProgress(row)
244
+ return row
245
+ }
246
+
247
+ /**
248
+ * Rejects report ids because CircuitJSON exposes no eager parser reports.
249
+ * @param {string[]} reports Requested report ids.
250
+ * @param {string} source Source file name.
251
+ * @returns {void}
252
+ */
253
+ static #assertReports(reports, source) {
254
+ if (!reports.length) return
255
+ throw new ToolkitError(
256
+ `CircuitJSON parser report is unavailable: ${reports[0]}.`,
257
+ {
258
+ code: 'ERR_CAPABILITY_UNAVAILABLE',
259
+ category: 'unsupported',
260
+ format: 'circuitjson',
261
+ source,
262
+ details: { reports }
263
+ }
264
+ )
265
+ }
266
+
267
+ /**
268
+ * Normalizes public parse failures while preserving typed option failures.
269
+ * @param {unknown} error Failure candidate.
270
+ * @param {unknown} input Original parser input.
271
+ * @returns {ToolkitError} Typed parser failure.
272
+ */
273
+ static #parseError(error, input) {
274
+ return ToolkitError.from(error, {
275
+ code: 'ERR_CIRCUITJSON_PARSE',
276
+ category: 'parse',
277
+ format: 'circuitjson',
278
+ source: ParserOptions.fileName(input)
279
+ })
280
+ }
281
+
282
+ /**
283
+ * Creates the synchronous worker-mode error.
284
+ * @param {string} source Source file name.
285
+ * @returns {ToolkitError} Typed unsupported error.
286
+ */
287
+ static #workerSyncError(source) {
288
+ return new ToolkitError(
289
+ 'Synchronous CircuitJSON parsing cannot use a worker.',
290
+ {
291
+ code: 'ERR_WORKER_SYNC_UNAVAILABLE',
292
+ category: 'unsupported',
293
+ format: 'circuitjson',
294
+ source
295
+ }
296
+ )
297
+ }
298
+
299
+ /**
300
+ * Creates a pre-start cancellation error.
301
+ * @param {string} source Source file name.
302
+ * @returns {ToolkitError} Typed cancellation error.
303
+ */
304
+ static #cancelledError(source) {
305
+ return new ToolkitError('CircuitJSON parsing was cancelled.', {
306
+ code: 'ERR_CANCELLED',
307
+ category: 'cancelled',
308
+ format: 'circuitjson',
309
+ source
310
+ })
311
+ }
312
+
313
+ /**
314
+ * Rejects an aborted direct request between parser phases.
315
+ * @param {{ input: { fileName: string }, options: { signal?: unknown } }} normalized Normalized request.
316
+ * @returns {void}
317
+ */
318
+ static #assertNotCancelled(normalized) {
319
+ if (!Parser.#isAborted(normalized.options.signal)) return
320
+ throw Parser.#cancelledError(normalized.input.fileName)
321
+ }
322
+
323
+ /**
324
+ * Reads a genuine optional AbortSignal through its platform getter.
325
+ * @param {unknown} signal Signal candidate.
326
+ * @returns {boolean} Whether the signal is aborted.
327
+ */
328
+ static #isAborted(signal) {
329
+ if (signal === undefined || signal === null) return false
330
+ const descriptor = Object.getOwnPropertyDescriptor(
331
+ AbortSignal.prototype,
332
+ 'aborted'
333
+ )
334
+ if (typeof descriptor?.get !== 'function') {
335
+ throw new TypeError('AbortSignal state is unavailable.')
336
+ }
337
+ try {
338
+ return Boolean(descriptor.get.call(signal))
339
+ } catch {
340
+ throw new TypeError('CircuitJSON signal must be an AbortSignal.')
341
+ }
342
+ }
343
+ }