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,493 @@
1
+ import { BinaryDataSnapshot } from '../context/BinaryDataSnapshot.mjs'
2
+ import { CircuitJsonReadOnlyDocument } from '../context/CircuitJsonReadOnlyDocument.mjs'
3
+ import { AttachedValueLimits } from '../AttachedValueLimits.mjs'
4
+
5
+ const ASSET_MODES = new Set(['none', 'metadata', 'full'])
6
+ const PREPARED_ASSETS = new WeakMap()
7
+ const SCALAR_FIELDS = ['id', 'kind', 'name', 'mediaType', 'byteLength']
8
+ const MEDIA_TYPES_BY_SUFFIX = new Map([
9
+ ['3mf', 'model/3mf'],
10
+ ['glb', 'model/gltf-binary'],
11
+ ['gltf', 'model/gltf+json'],
12
+ ['iges', 'model/iges'],
13
+ ['igs', 'model/iges'],
14
+ ['obj', 'model/obj'],
15
+ ['step', 'model/step'],
16
+ ['stl', 'model/stl'],
17
+ ['stp', 'model/step'],
18
+ ['vrml', 'model/vrml'],
19
+ ['wrl', 'model/vrml'],
20
+ ['bmp', 'image/bmp'],
21
+ ['gif', 'image/gif'],
22
+ ['jpeg', 'image/jpeg'],
23
+ ['jpg', 'image/jpeg'],
24
+ ['png', 'image/png'],
25
+ ['svg', 'image/svg+xml'],
26
+ ['webp', 'image/webp'],
27
+ ['json', 'application/json'],
28
+ ['pdf', 'application/pdf'],
29
+ ['zip', 'application/zip']
30
+ ])
31
+
32
+ /**
33
+ * Normalizes embedded and external asset records through one shared ownership
34
+ * and accounting boundary.
35
+ */
36
+ export class ToolkitAsset {
37
+ /**
38
+ * Creates one full clone-safe asset record.
39
+ * @param {Record<string, any>} [fields] Asset fields.
40
+ * @returns {{ id: string, kind: string, name: string, mediaType: string, byteLength: number, data: any, source: any }} Normalized asset.
41
+ */
42
+ static create(fields = {}) {
43
+ const prepared =
44
+ fields && typeof fields === 'object'
45
+ ? PREPARED_ASSETS.get(fields)
46
+ : null
47
+ return ToolkitAsset.prepare(
48
+ prepared ? fields : ToolkitAsset.#legacyPayload(fields),
49
+ { mode: 'full' }
50
+ )
51
+ }
52
+
53
+ /**
54
+ * Measures one asset's resident payload without copying it.
55
+ * @param {unknown} fields Asset candidate.
56
+ * @returns {number} Exact resident or declared payload byte length.
57
+ */
58
+ static measure(fields) {
59
+ const prepared =
60
+ fields && typeof fields === 'object'
61
+ ? PREPARED_ASSETS.get(fields)
62
+ : null
63
+ if (prepared) return prepared.byteLength
64
+ const descriptors = ToolkitAsset.#descriptors(fields)
65
+ return ToolkitAsset.#payloadByteLength(
66
+ ToolkitAsset.#dataValue(descriptors.data),
67
+ ToolkitAsset.#dataValue(descriptors.byteLength)
68
+ )
69
+ }
70
+
71
+ /**
72
+ * Prepares one descriptor-safe asset for a requested decode mode.
73
+ * @param {unknown} fields Asset candidate.
74
+ * @param {{ mode?: 'none' | 'metadata' | 'full', metadataBudget?: object, acceptPayload?: (byteLength: number, identity: { id: unknown, name: unknown }) => void }} [options] Preparation options.
75
+ * @returns {Record<string, any> | null} Prepared asset or null for none.
76
+ */
77
+ static prepare(fields = {}, options = {}) {
78
+ const normalizedOptions = ToolkitAsset.#options(options)
79
+ const existing =
80
+ fields && typeof fields === 'object'
81
+ ? PREPARED_ASSETS.get(fields)
82
+ : null
83
+ if (existing) {
84
+ ToolkitAsset.#acceptPrepared(
85
+ normalizedOptions.acceptPayload,
86
+ existing,
87
+ fields
88
+ )
89
+ if (normalizedOptions.mode === 'none') return null
90
+ if (
91
+ existing.mode === 'full' &&
92
+ normalizedOptions.mode === 'metadata'
93
+ ) {
94
+ return ToolkitAsset.prepare(
95
+ CircuitJsonReadOnlyDocument.copyAssetMetadataFields(fields),
96
+ { ...normalizedOptions, acceptPayload: null }
97
+ )
98
+ }
99
+ return fields
100
+ }
101
+ if (normalizedOptions.mode === 'none') {
102
+ const descriptors = ToolkitAsset.#descriptors(fields)
103
+ const byteLength = ToolkitAsset.#payloadByteLength(
104
+ ToolkitAsset.#dataValue(descriptors.data),
105
+ ToolkitAsset.#dataValue(descriptors.byteLength)
106
+ )
107
+ ToolkitAsset.#acceptPayload(
108
+ normalizedOptions.acceptPayload,
109
+ byteLength,
110
+ descriptors
111
+ )
112
+ return null
113
+ }
114
+
115
+ const descriptors = ToolkitAsset.#descriptors(fields)
116
+ const data = ToolkitAsset.#dataValue(descriptors.data)
117
+ const byteLength = ToolkitAsset.#payloadByteLength(
118
+ data,
119
+ ToolkitAsset.#dataValue(descriptors.byteLength)
120
+ )
121
+ ToolkitAsset.#acceptPayload(
122
+ normalizedOptions.acceptPayload,
123
+ byteLength,
124
+ descriptors
125
+ )
126
+
127
+ const kind = ToolkitAsset.#stringScalar(
128
+ ToolkitAsset.#dataValue(descriptors.kind),
129
+ 'asset',
130
+ 'kind'
131
+ )
132
+ const name = ToolkitAsset.#stringScalar(
133
+ ToolkitAsset.#dataValue(descriptors.name),
134
+ '',
135
+ 'name'
136
+ )
137
+ const budget =
138
+ normalizedOptions.metadataBudget ||
139
+ CircuitJsonReadOnlyDocument.createMetadataBudget()
140
+ const source = CircuitJsonReadOnlyDocument.copyReadonlyMetadataValue(
141
+ ToolkitAsset.#dataValue(descriptors.source) ?? null,
142
+ budget
143
+ )
144
+ const id = ToolkitAsset.#stringScalar(
145
+ ToolkitAsset.#dataValue(descriptors.id),
146
+ ToolkitAsset.#id(kind, name, source),
147
+ 'id'
148
+ )
149
+ const normalized = {
150
+ id,
151
+ kind,
152
+ name,
153
+ mediaType: ToolkitAsset.#stringScalar(
154
+ ToolkitAsset.#dataValue(descriptors.mediaType),
155
+ ToolkitAsset.#mediaTypeForName(name),
156
+ 'mediaType'
157
+ ),
158
+ byteLength,
159
+ data: normalizedOptions.mode === 'full' ? (data ?? null) : null,
160
+ source
161
+ }
162
+ const captured = CircuitJsonReadOnlyDocument.captureAsset(
163
+ normalized,
164
+ budget
165
+ )
166
+ PREPARED_ASSETS.set(captured, {
167
+ byteLength,
168
+ mode: normalizedOptions.mode
169
+ })
170
+ return captured
171
+ }
172
+
173
+ /**
174
+ * Prepares one dense asset list through data descriptors only.
175
+ * @param {unknown} assets Asset list candidate.
176
+ * @param {{ mode?: 'none' | 'metadata' | 'full', metadataBudget?: object, acceptPayload?: (byteLength: number, identity: { id: unknown, name: unknown }) => void }} [options] Preparation options.
177
+ * @returns {Record<string, any>[]} Prepared assets.
178
+ */
179
+ static prepareAll(assets, options = {}) {
180
+ AttachedValueLimits.add(assets)
181
+ const normalizedOptions = ToolkitAsset.#options(options)
182
+ const descriptors = ToolkitAsset.#arrayDescriptors(assets)
183
+ const length = descriptors.length.value
184
+ const metadataBudget =
185
+ normalizedOptions.metadataBudget ||
186
+ CircuitJsonReadOnlyDocument.createMetadataBudget()
187
+ const result = []
188
+ for (let index = 0; index < length; index += 1) {
189
+ const asset = ToolkitAsset.prepare(
190
+ descriptors[String(index)].value,
191
+ {
192
+ ...normalizedOptions,
193
+ metadataBudget
194
+ }
195
+ )
196
+ if (asset) result.push(asset)
197
+ }
198
+ return result
199
+ }
200
+
201
+ /**
202
+ * Reads and validates one options record without invoking accessors.
203
+ * @param {unknown} options Options candidate.
204
+ * @returns {{ mode: 'none' | 'metadata' | 'full', metadataBudget: object | null, acceptPayload: Function | null }} Normalized options.
205
+ */
206
+ static #options(options) {
207
+ const descriptors = ToolkitAsset.#plainDescriptors(
208
+ options,
209
+ 'Toolkit asset options must be a plain object.'
210
+ )
211
+ const allowed = new Set(['mode', 'metadataBudget', 'acceptPayload'])
212
+ for (const key of Reflect.ownKeys(descriptors)) {
213
+ if (typeof key !== 'string' || !allowed.has(key)) {
214
+ throw new TypeError(
215
+ `Unsupported ToolkitAsset option: ${String(key)}.`
216
+ )
217
+ }
218
+ }
219
+ const mode = ToolkitAsset.#dataValue(descriptors.mode) ?? 'full'
220
+ if (!ASSET_MODES.has(mode)) {
221
+ throw new TypeError(
222
+ 'ToolkitAsset mode must be none, metadata, or full.'
223
+ )
224
+ }
225
+ const acceptPayload = ToolkitAsset.#dataValue(descriptors.acceptPayload)
226
+ if (
227
+ acceptPayload !== undefined &&
228
+ acceptPayload !== null &&
229
+ typeof acceptPayload !== 'function'
230
+ ) {
231
+ throw new TypeError(
232
+ 'ToolkitAsset acceptPayload must be a function.'
233
+ )
234
+ }
235
+ return {
236
+ mode,
237
+ metadataBudget:
238
+ ToolkitAsset.#dataValue(descriptors.metadataBudget) || null,
239
+ acceptPayload: acceptPayload || null
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Reads one exact dense plain asset array.
245
+ * @param {unknown} value Array candidate.
246
+ * @returns {Record<string, PropertyDescriptor>} Array descriptors.
247
+ */
248
+ static #arrayDescriptors(value) {
249
+ if (!Array.isArray(value)) {
250
+ throw new TypeError('Toolkit assets must be a dense plain array.')
251
+ }
252
+ let prototype
253
+ let descriptors
254
+ try {
255
+ prototype = Object.getPrototypeOf(value)
256
+ descriptors = Object.getOwnPropertyDescriptors(value)
257
+ } catch {
258
+ throw new TypeError('Toolkit assets could not be inspected safely.')
259
+ }
260
+ const length = ToolkitAsset.#dataValue(descriptors.length)
261
+ if (
262
+ prototype !== Array.prototype ||
263
+ !Number.isSafeInteger(length) ||
264
+ length < 0 ||
265
+ Reflect.ownKeys(descriptors).length !== length + 1
266
+ ) {
267
+ throw new TypeError('Toolkit assets must be a dense plain array.')
268
+ }
269
+ for (let index = 0; index < length; index += 1) {
270
+ const descriptor = descriptors[String(index)]
271
+ if (
272
+ !descriptor ||
273
+ !Object.hasOwn(descriptor, 'value') ||
274
+ descriptor.enumerable !== true
275
+ ) {
276
+ throw new TypeError(
277
+ 'Toolkit assets must contain enumerable data properties.'
278
+ )
279
+ }
280
+ }
281
+ return descriptors
282
+ }
283
+
284
+ /**
285
+ * Reads one asset's own descriptors.
286
+ * @param {unknown} fields Asset candidate.
287
+ * @returns {Record<string, PropertyDescriptor>} Field descriptors.
288
+ */
289
+ static #descriptors(fields) {
290
+ const descriptors = ToolkitAsset.#plainDescriptors(
291
+ fields,
292
+ 'Toolkit asset must be a plain object.'
293
+ )
294
+ const allowed = new Set([...SCALAR_FIELDS, 'data', 'source'])
295
+ for (const key of Reflect.ownKeys(descriptors)) {
296
+ const descriptor = descriptors[key]
297
+ if (
298
+ typeof key !== 'string' ||
299
+ !allowed.has(key) ||
300
+ descriptor.enumerable !== true
301
+ ) {
302
+ throw new TypeError(
303
+ 'Toolkit assets may contain only canonical enumerable fields.'
304
+ )
305
+ }
306
+ }
307
+ return descriptors
308
+ }
309
+
310
+ /**
311
+ * Returns data descriptors for one plain object.
312
+ * @param {unknown} value Record candidate.
313
+ * @param {string} message Type error message.
314
+ * @returns {Record<string, PropertyDescriptor>} Data descriptors.
315
+ */
316
+ static #plainDescriptors(value, message) {
317
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
318
+ throw new TypeError(message)
319
+ }
320
+ let prototype
321
+ let descriptors
322
+ try {
323
+ prototype = Object.getPrototypeOf(value)
324
+ descriptors = Object.getOwnPropertyDescriptors(value)
325
+ } catch {
326
+ throw new TypeError(message)
327
+ }
328
+ if (prototype !== Object.prototype && prototype !== null) {
329
+ throw new TypeError(message)
330
+ }
331
+ for (const descriptor of Object.values(descriptors)) {
332
+ if (!Object.hasOwn(descriptor, 'value')) {
333
+ throw new TypeError(
334
+ 'Toolkit assets may contain only data properties.'
335
+ )
336
+ }
337
+ }
338
+ return descriptors
339
+ }
340
+
341
+ /**
342
+ * Reads one descriptor's value.
343
+ * @param {PropertyDescriptor | undefined} descriptor Descriptor.
344
+ * @returns {unknown} Data value.
345
+ */
346
+ static #dataValue(descriptor) {
347
+ return descriptor && Object.hasOwn(descriptor, 'value')
348
+ ? descriptor.value
349
+ : undefined
350
+ }
351
+
352
+ /**
353
+ * Measures supported binary, textual, or external metadata payloads.
354
+ * @param {unknown} data Asset payload.
355
+ * @param {unknown} declaredByteLength Declared metadata byte length.
356
+ * @returns {number} Exact byte length.
357
+ */
358
+ static #payloadByteLength(data, declaredByteLength) {
359
+ const binaryLength = BinaryDataSnapshot.byteLength(data)
360
+ if (binaryLength !== null) return binaryLength
361
+ if (typeof data === 'string') {
362
+ return CircuitJsonReadOnlyDocument.utf8ByteLength(data)
363
+ }
364
+ if (data !== null && data !== undefined) {
365
+ throw new TypeError(
366
+ 'Toolkit asset data must be text, binary data, or null.'
367
+ )
368
+ }
369
+ if (declaredByteLength === undefined) return 0
370
+ if (
371
+ !Number.isSafeInteger(declaredByteLength) ||
372
+ declaredByteLength < 0
373
+ ) {
374
+ throw new TypeError(
375
+ 'Toolkit asset byteLength must be a non-negative safe integer.'
376
+ )
377
+ }
378
+ return declaredByteLength
379
+ }
380
+
381
+ /**
382
+ * Retains the historical create() fallback for unsupported payload values.
383
+ * @param {unknown} fields Asset fields.
384
+ * @returns {unknown} Original fields or a descriptor-preserving null payload.
385
+ */
386
+ static #legacyPayload(fields) {
387
+ const descriptors = ToolkitAsset.#descriptors(fields)
388
+ const data = ToolkitAsset.#dataValue(descriptors.data)
389
+ if (
390
+ data === null ||
391
+ data === undefined ||
392
+ typeof data === 'string' ||
393
+ BinaryDataSnapshot.byteLength(data) !== null
394
+ ) {
395
+ return fields
396
+ }
397
+ const normalized = Object.create(Object.getPrototypeOf(fields))
398
+ for (const key of Reflect.ownKeys(descriptors)) {
399
+ const descriptor = descriptors[key]
400
+ Object.defineProperty(normalized, key, {
401
+ ...descriptor,
402
+ value: key === 'data' ? null : descriptor.value
403
+ })
404
+ }
405
+ return normalized
406
+ }
407
+
408
+ /**
409
+ * Normalizes one primitive scalar without object coercion.
410
+ * @param {unknown} value Scalar candidate.
411
+ * @param {string} fallback Missing fallback.
412
+ * @param {string} field Field name.
413
+ * @returns {string} String value.
414
+ */
415
+ static #stringScalar(value, fallback, field) {
416
+ if (value === undefined || value === null || value === '') {
417
+ return fallback
418
+ }
419
+ if (!['string', 'number', 'boolean', 'bigint'].includes(typeof value)) {
420
+ throw new TypeError(
421
+ `Toolkit asset ${field} must be a primitive scalar.`
422
+ )
423
+ }
424
+ return String(value)
425
+ }
426
+
427
+ /**
428
+ * Infers a stable media type from a canonical asset path.
429
+ * @param {string} name Asset name.
430
+ * @returns {string} Inferred type or the binary fallback.
431
+ */
432
+ static #mediaTypeForName(name) {
433
+ const path = name.split(/[?#]/u, 1)[0].toLowerCase()
434
+ const suffix = path.split('.').at(-1) || ''
435
+ return MEDIA_TYPES_BY_SUFFIX.get(suffix) || 'application/octet-stream'
436
+ }
437
+
438
+ /**
439
+ * Runs a pre-copy payload acceptance callback.
440
+ * @param {Function | null} acceptPayload Acceptance callback.
441
+ * @param {number} byteLength Measured payload bytes.
442
+ * @param {Record<string, PropertyDescriptor>} descriptors Asset descriptors.
443
+ * @returns {void}
444
+ */
445
+ static #acceptPayload(acceptPayload, byteLength, descriptors) {
446
+ if (!acceptPayload) return
447
+ acceptPayload(byteLength, {
448
+ id: ToolkitAsset.#dataValue(descriptors.id),
449
+ name: ToolkitAsset.#dataValue(descriptors.name)
450
+ })
451
+ }
452
+
453
+ /**
454
+ * Applies accounting to an already prepared asset without recopying it.
455
+ * @param {Function | null} acceptPayload Acceptance callback.
456
+ * @param {{ byteLength: number }} prepared Private preparation state.
457
+ * @param {Record<string, any>} fields Prepared asset.
458
+ * @returns {void}
459
+ */
460
+ static #acceptPrepared(acceptPayload, prepared, fields) {
461
+ if (!acceptPayload) return
462
+ const descriptors = Object.getOwnPropertyDescriptors(fields)
463
+ ToolkitAsset.#acceptPayload(
464
+ acceptPayload,
465
+ prepared.byteLength,
466
+ descriptors
467
+ )
468
+ }
469
+
470
+ /**
471
+ * Creates a deterministic source-identity asset id.
472
+ * @param {string} kind Asset kind.
473
+ * @param {string} name Asset name.
474
+ * @param {unknown} source Source reference.
475
+ * @returns {string} Stable id.
476
+ */
477
+ static #id(kind, name, source) {
478
+ let sourceText = ''
479
+ try {
480
+ sourceText = JSON.stringify(source, (_key, value) =>
481
+ typeof value === 'bigint' ? `${value}n` : value
482
+ )
483
+ } catch {
484
+ sourceText = '[cyclic-source]'
485
+ }
486
+ let hash = 2166136261
487
+ for (const character of `${kind}\u0000${name}\u0000${sourceText}`) {
488
+ hash ^= character.codePointAt(0)
489
+ hash = Math.imul(hash, 16777619)
490
+ }
491
+ return `asset-${(hash >>> 0).toString(16).padStart(8, '0')}`
492
+ }
493
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Creates a structured-clone-safe copy of one contract value.
3
+ * @param {unknown} value Candidate value.
4
+ * @param {unknown} fallback Value used when cloning is impossible.
5
+ * @returns {any} Clone-safe value.
6
+ */
7
+ export function cloneSafeValue(value, fallback = null) {
8
+ if (value === undefined) return fallback
9
+ try {
10
+ return structuredClone(value)
11
+ } catch {
12
+ return fallback
13
+ }
14
+ }
15
+
16
+ /**
17
+ * Normalizes toolkit diagnostics into the shared clone-safe record shape.
18
+ */
19
+ export class ToolkitDiagnostic {
20
+ /**
21
+ * Creates one diagnostic record.
22
+ * @param {Record<string, any>} [fields] Diagnostic fields.
23
+ * @returns {{ code: string, severity: string, message: string, source: string, location: any, details: any }} Normalized diagnostic.
24
+ */
25
+ static create(fields = {}) {
26
+ const severity = String(fields.severity || 'info')
27
+ return {
28
+ code: String(fields.code || 'TOOLKIT_DIAGNOSTIC'),
29
+ severity: ['info', 'warning', 'error'].includes(severity)
30
+ ? severity
31
+ : 'info',
32
+ message: String(fields.message || ''),
33
+ source: String(fields.source || ''),
34
+ location: cloneSafeValue(fields.location, null),
35
+ details: cloneSafeValue(fields.details, {})
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,176 @@
1
+ import { cloneSafeValue } from './ToolkitDiagnostic.mjs'
2
+ import { RuntimeProxyBoundary } from './RuntimeProxyBoundary.mjs'
3
+
4
+ const ERROR_CATEGORIES = new Set([
5
+ 'parse',
6
+ 'validation',
7
+ 'unsupported',
8
+ 'cancelled',
9
+ 'runtime'
10
+ ])
11
+
12
+ /**
13
+ * Shared typed toolkit failure with clone-safe serialization.
14
+ */
15
+ export class ToolkitError extends Error {
16
+ #trustedRecord
17
+
18
+ /**
19
+ * Creates a typed toolkit error.
20
+ * @param {unknown} message Error message.
21
+ * @param {Record<string, any>} [fields] Structured failure fields.
22
+ */
23
+ constructor(message, fields = {}) {
24
+ super(String(message || 'Toolkit operation failed.'))
25
+ this.name = 'ToolkitError'
26
+ this.code = String(fields.code || 'ERR_TOOLKIT_RUNTIME')
27
+ const category = String(fields.category || 'runtime')
28
+ this.category = ERROR_CATEGORIES.has(category) ? category : 'runtime'
29
+ this.format = String(fields.format || 'circuitjson')
30
+ this.source = String(fields.source || '')
31
+ this.location = cloneSafeValue(fields.location, null)
32
+ this.details = cloneSafeValue(fields.details, {})
33
+ this.cause = ToolkitError.cloneSafeCause(fields.cause)
34
+ this.#trustedRecord = {
35
+ name: this.name,
36
+ message: this.message,
37
+ code: this.code,
38
+ category: this.category,
39
+ format: this.format,
40
+ source: this.source,
41
+ location: cloneSafeValue(this.location, null),
42
+ details: cloneSafeValue(this.details, {}),
43
+ cause: cloneSafeValue(this.cause, null)
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Normalizes any thrown value into ToolkitError.
49
+ * @param {unknown} error Error candidate.
50
+ * @param {Record<string, any>} [fields] Override fields.
51
+ * @returns {ToolkitError} Typed error.
52
+ */
53
+ static from(error, fields = {}) {
54
+ if (ToolkitError.trustedRecord(error)) return error
55
+ const cause = ToolkitError.cloneSafeCause(error)
56
+ return new ToolkitError(cause?.message || 'Toolkit operation failed.', {
57
+ ...fields,
58
+ cause
59
+ })
60
+ }
61
+
62
+ /**
63
+ * Creates a clone-safe cause summary.
64
+ * @param {unknown} error Cause candidate.
65
+ * @returns {{ name: string, message: string, code: string | null } | null} Cause summary.
66
+ */
67
+ static cloneSafeCause(error) {
68
+ if (!error) return null
69
+ if (!['object', 'function'].includes(typeof error)) {
70
+ return {
71
+ name: 'Error',
72
+ message: ToolkitError.#primitiveText(error, 'Error'),
73
+ code: null
74
+ }
75
+ }
76
+ try {
77
+ RuntimeProxyBoundary.assert(error, 'Toolkit error cause')
78
+ } catch {
79
+ return {
80
+ name: 'Error',
81
+ message: 'Toolkit operation failed.',
82
+ code: null
83
+ }
84
+ }
85
+ const name = ToolkitError.#dataField(error, 'name')
86
+ const message = ToolkitError.#dataField(error, 'message')
87
+ const code = ToolkitError.#dataField(error, 'code')
88
+ return {
89
+ name: ToolkitError.#primitiveText(name, 'Error'),
90
+ message: ToolkitError.#primitiveText(
91
+ message,
92
+ 'Toolkit operation failed.'
93
+ ),
94
+ code:
95
+ code === null || code === undefined
96
+ ? null
97
+ : ToolkitError.#primitiveText(code, null)
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Reads one inherited data property without invoking an accessor.
103
+ * @param {object | Function} value Property owner.
104
+ * @param {string} key Property key.
105
+ * @returns {unknown} Data value or undefined.
106
+ */
107
+ static #dataField(value, key) {
108
+ let owner = value
109
+ for (let depth = 0; owner && depth < 16; depth += 1) {
110
+ let descriptor
111
+ try {
112
+ descriptor = Object.getOwnPropertyDescriptor(owner, key)
113
+ owner = Object.getPrototypeOf(owner)
114
+ } catch {
115
+ return undefined
116
+ }
117
+ if (!descriptor) continue
118
+ return Object.hasOwn(descriptor, 'value')
119
+ ? descriptor.value
120
+ : undefined
121
+ }
122
+ return undefined
123
+ }
124
+
125
+ /**
126
+ * Converts only primitive values to bounded error text.
127
+ * @param {unknown} value Text candidate.
128
+ * @param {string | null} fallback Missing or unsafe fallback.
129
+ * @returns {string | null} Safe text.
130
+ */
131
+ static #primitiveText(value, fallback) {
132
+ if (
133
+ value === null ||
134
+ value === undefined ||
135
+ ['object', 'function'].includes(typeof value)
136
+ ) {
137
+ return fallback
138
+ }
139
+ try {
140
+ return String(value)
141
+ } catch {
142
+ return fallback
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Returns construction-time fields only for a genuine unproxied instance.
148
+ * @param {unknown} error Error candidate.
149
+ * @returns {ReturnType<ToolkitError['toJSON']> | null} Trusted record.
150
+ */
151
+ static trustedRecord(error) {
152
+ try {
153
+ return cloneSafeValue(error.#trustedRecord, null)
154
+ } catch {
155
+ return null
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Serializes the error without prototype-only state.
161
+ * @returns {{ name: string, message: string, code: string, category: string, format: string, source: string, location: any, details: any, cause: any }} Clone-safe error record.
162
+ */
163
+ toJSON() {
164
+ return {
165
+ name: this.name,
166
+ message: this.message,
167
+ code: this.code,
168
+ category: this.category,
169
+ format: this.format,
170
+ source: this.source,
171
+ location: this.location,
172
+ details: this.details,
173
+ cause: this.cause
174
+ }
175
+ }
176
+ }