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,920 @@
1
+ import { BinaryDataSnapshot } from './BinaryDataSnapshot.mjs'
2
+ import { CircuitJsonMetadataBoundary } from './CircuitJsonMetadataBoundary.mjs'
3
+ import { CircuitJsonValidationAuthority } from './CircuitJsonValidationAuthority.mjs'
4
+ import { ProtectedExtensionBinaryBoundary } from './ProtectedExtensionBinaryBoundary.mjs'
5
+ import { StructuredDataSnapshot } from './StructuredDataSnapshot.mjs'
6
+
7
+ const PROTECTED_ASSET_BYTES = new WeakMap()
8
+ const PROTECTED_ASSET_DATA_GETTERS = new WeakSet()
9
+ const SEALED_ASSETS = new WeakSet()
10
+ const ASSET_PAYLOAD_LENGTHS = new WeakMap()
11
+ const OWNED_METADATA_ROOTS = new WeakSet()
12
+ const METADATA_BUDGETS = new WeakMap()
13
+ const EXTENSION_METADATA_LIMITS = Object.freeze({
14
+ label: 'Canonical extension data',
15
+ maxBytes: 128 * 1024 * 1024,
16
+ maxItems: 2_000_000,
17
+ preserveBinary: true
18
+ })
19
+ const ASSET_SCALAR_FIELDS = new Set([
20
+ 'id',
21
+ 'kind',
22
+ 'name',
23
+ 'mediaType',
24
+ 'byteLength'
25
+ ])
26
+
27
+ /**
28
+ * Owns canonical document records while leaving binary views usable.
29
+ */
30
+ export class CircuitJsonReadOnlyDocument {
31
+ /**
32
+ * Captures caller-owned boundaries and deeply freezes the canonical envelope.
33
+ * @param {Record<string, any>} document Canonical document envelope.
34
+ * @returns {Record<string, any>} The same read-only envelope.
35
+ */
36
+ static freeze(document) {
37
+ return CircuitJsonReadOnlyDocument.#freezeDocument(document, new Set())
38
+ }
39
+
40
+ /**
41
+ * Seals an envelope whose exact model was already deeply frozen by validation.
42
+ * @param {Record<string, any>} document Canonical document envelope.
43
+ * @param {object[]} model Exact deeply frozen model owned by the proof.
44
+ * @returns {Record<string, any>} The same read-only envelope.
45
+ */
46
+ static freezeValidated(document, model) {
47
+ if (!CircuitJsonValidationAuthority.permitsSeal(model)) {
48
+ throw new TypeError(
49
+ 'Validated document sealing requires an unforgeable validation proof.'
50
+ )
51
+ }
52
+ const descriptor = Object.getOwnPropertyDescriptor(document, 'model')
53
+ if (
54
+ !descriptor ||
55
+ !Object.hasOwn(descriptor, 'value') ||
56
+ descriptor.value !== model ||
57
+ !Array.isArray(model) ||
58
+ !Object.isFrozen(model)
59
+ ) {
60
+ throw new TypeError(
61
+ 'Validated document sealing requires its exact frozen model.'
62
+ )
63
+ }
64
+ const frozenRoots = new Set([model])
65
+ const sourceReference = Object.getOwnPropertyDescriptor(
66
+ document,
67
+ 'sourceReference'
68
+ )
69
+ if (
70
+ sourceReference &&
71
+ Object.hasOwn(sourceReference, 'value') &&
72
+ sourceReference.value &&
73
+ typeof sourceReference.value === 'object'
74
+ ) {
75
+ frozenRoots.add(sourceReference.value)
76
+ }
77
+ return CircuitJsonReadOnlyDocument.#freezeDocument(
78
+ document,
79
+ frozenRoots
80
+ )
81
+ }
82
+
83
+ /**
84
+ * Captures owned boundaries and freezes an envelope with known frozen roots.
85
+ * @param {Record<string, any>} document Canonical document envelope.
86
+ * @param {Set<object>} frozenRoots Deeply frozen roots that need no revisit.
87
+ * @returns {Record<string, any>} The same read-only envelope.
88
+ */
89
+ static #freezeDocument(document, frozenRoots) {
90
+ const assets = CircuitJsonReadOnlyDocument.#documentAssets(document)
91
+ const metadataState = StructuredDataSnapshot.createState()
92
+ CircuitJsonReadOnlyDocument.#captureAssetData(assets, metadataState)
93
+ const extensions =
94
+ CircuitJsonReadOnlyDocument.#captureDocumentExtensions(document)
95
+ if (extensions && typeof extensions === 'object') {
96
+ frozenRoots.add(extensions)
97
+ }
98
+ CircuitJsonMetadataBoundary.normalize(document, assets, (value) =>
99
+ CircuitJsonReadOnlyDocument.#captureMetadataRoot(
100
+ value,
101
+ metadataState
102
+ )
103
+ )
104
+ CircuitJsonReadOnlyDocument.#freezeValue(document, frozenRoots)
105
+ return document
106
+ }
107
+
108
+ /**
109
+ * Reads only an internally-created defensive asset data getter.
110
+ * @param {PropertyDescriptor | undefined} descriptor Data descriptor.
111
+ * @returns {{ trusted: boolean, value: unknown }} Trusted read result.
112
+ */
113
+ static readProtectedAssetData(descriptor) {
114
+ if (
115
+ !descriptor ||
116
+ typeof descriptor.get !== 'function' ||
117
+ descriptor.set !== undefined ||
118
+ descriptor.enumerable !== true ||
119
+ descriptor.configurable !== false ||
120
+ !PROTECTED_ASSET_DATA_GETTERS.has(descriptor.get)
121
+ ) {
122
+ return { trusted: false, value: null }
123
+ }
124
+ return {
125
+ trusted: true,
126
+ value: Reflect.apply(descriptor.get, undefined, [])
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Protects one newly owned canonical asset without first recopying it.
132
+ * @param {Record<string, any>} asset Toolkit-owned canonical asset.
133
+ * @returns {Record<string, any>} The same protected asset.
134
+ */
135
+ static protectOwnedAsset(asset) {
136
+ CircuitJsonReadOnlyDocument.#protectAsset(asset)
137
+ return asset
138
+ }
139
+
140
+ /**
141
+ * Atomically captures one canonical asset for accounting and later ownership.
142
+ * @param {unknown} asset Canonical asset candidate.
143
+ * @param {object | null} [metadataBudget] Shared metadata budget token.
144
+ * @param {boolean} [rejectFrozenBytes] Whether raw frozen bytes are invalid.
145
+ * @param {((byteLength: number, identity: { id: unknown, name: unknown }) => void) | null} [acceptPayload] Pre-copy byte-limit check.
146
+ * @returns {Record<string, any>} Owned asset snapshot.
147
+ */
148
+ static captureAsset(
149
+ asset,
150
+ metadataBudget = null,
151
+ rejectFrozenBytes = false,
152
+ acceptPayload = null
153
+ ) {
154
+ return CircuitJsonReadOnlyDocument.#captureAsset(
155
+ asset,
156
+ CircuitJsonReadOnlyDocument.#metadataState(metadataBudget),
157
+ rejectFrozenBytes,
158
+ acceptPayload
159
+ )
160
+ }
161
+
162
+ /**
163
+ * Validates, normalizes, and isolates one clone-safe metadata value.
164
+ * @param {unknown} value Metadata candidate.
165
+ * @returns {unknown} Isolated metadata snapshot.
166
+ */
167
+ static copyMetadataValue(value) {
168
+ return StructuredDataSnapshot.capture(value)
169
+ }
170
+
171
+ /**
172
+ * Creates a deeply frozen plain-data snapshot of clone-safe metadata.
173
+ * @param {unknown} value Metadata candidate.
174
+ * @param {object | null} [metadataBudget] Shared metadata budget token.
175
+ * @returns {unknown} Deeply immutable owned metadata.
176
+ */
177
+ static copyReadonlyMetadataValue(value, metadataBudget = null) {
178
+ if (
179
+ value &&
180
+ typeof value === 'object' &&
181
+ OWNED_METADATA_ROOTS.has(value) &&
182
+ Object.isFrozen(value)
183
+ ) {
184
+ return value
185
+ }
186
+ const snapshot = StructuredDataSnapshot.capture(
187
+ value,
188
+ CircuitJsonReadOnlyDocument.#metadataState(metadataBudget)
189
+ )
190
+ CircuitJsonReadOnlyDocument.#freezeValue(snapshot, new Set())
191
+ if (snapshot && typeof snapshot === 'object') {
192
+ OWNED_METADATA_ROOTS.add(snapshot)
193
+ }
194
+ return snapshot
195
+ }
196
+
197
+ /**
198
+ * Creates one deeply frozen source-extension snapshot under its separate
199
+ * transfer-sized item and byte ceilings.
200
+ * @param {unknown} value Extension candidate.
201
+ * @param {((snapshot: unknown) => unknown) | null} [normalize] Optional normalization over the owned mutable snapshot.
202
+ * @returns {unknown} Deeply immutable owned extension metadata.
203
+ */
204
+ static copyReadonlyExtensionValue(value, normalize = null) {
205
+ if (
206
+ value &&
207
+ typeof value === 'object' &&
208
+ OWNED_METADATA_ROOTS.has(value) &&
209
+ Object.isFrozen(value) &&
210
+ normalize === null
211
+ ) {
212
+ return value
213
+ }
214
+ if (normalize !== null && typeof normalize !== 'function') {
215
+ throw new TypeError('Extension normalizer must be a function.')
216
+ }
217
+ const snapshot = StructuredDataSnapshot.capture(
218
+ value,
219
+ StructuredDataSnapshot.createState(EXTENSION_METADATA_LIMITS)
220
+ )
221
+ ProtectedExtensionBinaryBoundary.protect(snapshot)
222
+ const normalized = normalize ? normalize(snapshot) : snapshot
223
+ if (normalized !== snapshot) {
224
+ ProtectedExtensionBinaryBoundary.protect(normalized)
225
+ }
226
+ CircuitJsonReadOnlyDocument.#freezeValue(normalized, new Set())
227
+ if (normalized && typeof normalized === 'object') {
228
+ OWNED_METADATA_ROOTS.add(normalized)
229
+ }
230
+ return normalized
231
+ }
232
+
233
+ /**
234
+ * Creates an opaque request-global source-metadata traversal budget.
235
+ * @returns {object} Branded metadata budget token.
236
+ */
237
+ static createMetadataBudget() {
238
+ const budget = Object.freeze({})
239
+ METADATA_BUDGETS.set(budget, StructuredDataSnapshot.createState())
240
+ return budget
241
+ }
242
+
243
+ /**
244
+ * Measures UTF-8 text without allocating encoded bytes.
245
+ * @param {string} value Text payload.
246
+ * @returns {number} UTF-8 byte length.
247
+ */
248
+ static utf8ByteLength(value) {
249
+ if (typeof value !== 'string') {
250
+ throw new TypeError('Expected a string payload.')
251
+ }
252
+ return CircuitJsonReadOnlyDocument.#utf8ByteLength(value)
253
+ }
254
+
255
+ /**
256
+ * Copies one canonical asset through its trusted defensive payload slot.
257
+ * @param {unknown} asset Canonical asset candidate.
258
+ * @returns {Record<string, any>} Own data fields with copied payload bytes.
259
+ */
260
+ static copyAssetFields(asset) {
261
+ return CircuitJsonReadOnlyDocument.#copyAssetFields(asset, true)
262
+ }
263
+
264
+ /**
265
+ * Copies canonical asset metadata without materializing payload bytes.
266
+ * @param {unknown} asset Canonical asset candidate.
267
+ * @returns {Record<string, any>} Own metadata fields with null data.
268
+ */
269
+ static copyAssetMetadataFields(asset) {
270
+ return CircuitJsonReadOnlyDocument.#copyAssetFields(asset, false)
271
+ }
272
+
273
+ /**
274
+ * Returns the payload length captured with the owned asset value.
275
+ * @param {unknown} asset Canonical asset candidate.
276
+ * @param {object | null} [metadataBudget] Shared metadata budget token.
277
+ * @returns {number} Exact resident payload byte length.
278
+ */
279
+ static assetPayloadByteLength(asset, metadataBudget = null) {
280
+ const captured = CircuitJsonReadOnlyDocument.#captureAsset(
281
+ asset,
282
+ CircuitJsonReadOnlyDocument.#metadataState(metadataBudget)
283
+ )
284
+ return ASSET_PAYLOAD_LENGTHS.get(captured)
285
+ }
286
+
287
+ /**
288
+ * Reads scalar/index fields while retaining already-owned source metadata.
289
+ * @param {unknown} asset Canonical asset candidate.
290
+ * @param {object | null} [metadataBudget] Shared metadata budget token.
291
+ * @returns {Record<string, any>} Descriptor-safe index fields.
292
+ */
293
+ static copyAssetIndexFields(asset, metadataBudget = null) {
294
+ return CircuitJsonReadOnlyDocument.#copyAssetFields(
295
+ asset,
296
+ false,
297
+ false,
298
+ CircuitJsonReadOnlyDocument.#metadataState(metadataBudget)
299
+ )
300
+ }
301
+
302
+ /**
303
+ * Reads the optional document asset array as an own data property.
304
+ * @param {unknown} document Canonical document envelope.
305
+ * @returns {unknown} Asset array candidate.
306
+ */
307
+ static #documentAssets(document) {
308
+ if (!document || typeof document !== 'object') return undefined
309
+ let descriptor
310
+ try {
311
+ descriptor = Object.getOwnPropertyDescriptor(document, 'assets')
312
+ } catch {
313
+ throw new TypeError(
314
+ 'Canonical document assets could not be inspected safely.'
315
+ )
316
+ }
317
+ if (!descriptor) return undefined
318
+ if (!Object.hasOwn(descriptor, 'value')) {
319
+ throw new TypeError(
320
+ 'Canonical document assets must be an own data property.'
321
+ )
322
+ }
323
+ return descriptor.value
324
+ }
325
+
326
+ /**
327
+ * Captures an unowned worker or caller extension root with extension-sized
328
+ * bounds before the generic source metadata pass begins.
329
+ * @param {Record<string, any>} document Canonical document envelope.
330
+ * @returns {unknown} Owned extension root or undefined.
331
+ */
332
+ static #captureDocumentExtensions(document) {
333
+ let descriptor
334
+ try {
335
+ descriptor = Object.getOwnPropertyDescriptor(document, 'extensions')
336
+ } catch {
337
+ throw new TypeError(
338
+ 'Canonical document extensions could not be inspected safely.'
339
+ )
340
+ }
341
+ if (!descriptor) return undefined
342
+ if (!Object.hasOwn(descriptor, 'value')) {
343
+ throw new TypeError(
344
+ 'Canonical document extensions must be an own data property.'
345
+ )
346
+ }
347
+ const captured = CircuitJsonReadOnlyDocument.copyReadonlyExtensionValue(
348
+ descriptor.value
349
+ )
350
+ if (captured === descriptor.value) return captured
351
+ Object.defineProperty(document, 'extensions', {
352
+ ...descriptor,
353
+ value: captured
354
+ })
355
+ return captured
356
+ }
357
+
358
+ /**
359
+ * Captures one asset with a single caller-owned descriptor traversal.
360
+ * @param {unknown} asset Canonical asset candidate.
361
+ * @param {{ seen: Map<object, unknown>, items: number }} metadataState Shared metadata state.
362
+ * @param {boolean} [rejectFrozenBytes] Whether raw frozen bytes are invalid.
363
+ * @param {((byteLength: number, identity: { id: unknown, name: unknown }) => void) | null} [acceptPayload] Pre-copy byte-limit check.
364
+ * @returns {Record<string, any>} Owned sealed asset.
365
+ */
366
+ static #captureAsset(
367
+ asset,
368
+ metadataState,
369
+ rejectFrozenBytes = false,
370
+ acceptPayload = null
371
+ ) {
372
+ if (SEALED_ASSETS.has(asset)) {
373
+ CircuitJsonReadOnlyDocument.#accountSealedAsset(
374
+ asset,
375
+ metadataState,
376
+ acceptPayload
377
+ )
378
+ return asset
379
+ }
380
+ if (!asset || typeof asset !== 'object' || Array.isArray(asset)) {
381
+ throw new TypeError('Canonical asset must be a plain object.')
382
+ }
383
+ const previouslyCaptured = metadataState.seen.get(asset)
384
+ if (SEALED_ASSETS.has(previouslyCaptured)) return previouslyCaptured
385
+ let prototype
386
+ let keys
387
+ try {
388
+ prototype = Object.getPrototypeOf(asset)
389
+ keys = Reflect.ownKeys(asset)
390
+ } catch {
391
+ throw new TypeError(
392
+ 'Canonical asset must expose inspectable plain object data.'
393
+ )
394
+ }
395
+ if (prototype !== Object.prototype && prototype !== null) {
396
+ throw new TypeError('Canonical asset must be a plain object.')
397
+ }
398
+
399
+ const fields = []
400
+ const identity = { id: undefined, name: undefined }
401
+ let payloadLength = null
402
+ let binaryPayload = false
403
+ for (const key of keys) {
404
+ if (typeof key !== 'string') {
405
+ throw new TypeError('Canonical asset keys must be strings.')
406
+ }
407
+ let descriptor
408
+ try {
409
+ descriptor = Object.getOwnPropertyDescriptor(asset, key)
410
+ } catch {
411
+ throw new TypeError(
412
+ 'Canonical asset properties could not be inspected.'
413
+ )
414
+ }
415
+ if (!descriptor) {
416
+ throw new TypeError(
417
+ 'Canonical asset properties changed during inspection.'
418
+ )
419
+ }
420
+ if (key === 'data' && PROTECTED_ASSET_BYTES.has(asset)) {
421
+ const bytes = PROTECTED_ASSET_BYTES.get(asset)
422
+ fields.push({ key, descriptor, bytes, range: null })
423
+ payloadLength = bytes.byteLength
424
+ binaryPayload = true
425
+ continue
426
+ }
427
+ if (!Object.hasOwn(descriptor, 'value')) {
428
+ throw new TypeError(
429
+ 'Canonical assets may contain only data properties.'
430
+ )
431
+ }
432
+ if (ASSET_SCALAR_FIELDS.has(key)) {
433
+ CircuitJsonReadOnlyDocument.#validateAssetScalar(
434
+ key,
435
+ descriptor.value
436
+ )
437
+ }
438
+ if (key === 'id' || key === 'name') {
439
+ identity[key] = descriptor.value
440
+ }
441
+ let range = null
442
+ if (key === 'data') {
443
+ CircuitJsonReadOnlyDocument.#validateAssetPayload(
444
+ descriptor.value
445
+ )
446
+ range = BinaryDataSnapshot.describe(descriptor.value)
447
+ if (range) {
448
+ payloadLength = range.byteLength
449
+ binaryPayload = true
450
+ } else {
451
+ payloadLength =
452
+ typeof descriptor.value === 'string'
453
+ ? CircuitJsonReadOnlyDocument.#utf8ByteLength(
454
+ descriptor.value
455
+ )
456
+ : 0
457
+ }
458
+ }
459
+ fields.push({ key, descriptor, bytes: null, range })
460
+ }
461
+ if (payloadLength === null) {
462
+ throw new TypeError(
463
+ 'Canonical asset data must be an own data property.'
464
+ )
465
+ }
466
+ if (binaryPayload && rejectFrozenBytes && Object.isFrozen(asset)) {
467
+ throw new TypeError(
468
+ 'Frozen canonical asset bytes cannot be protected.'
469
+ )
470
+ }
471
+ if (acceptPayload !== null) {
472
+ if (typeof acceptPayload !== 'function') {
473
+ throw new TypeError('Asset payload check must be a function.')
474
+ }
475
+ acceptPayload(payloadLength, identity)
476
+ }
477
+
478
+ const result = Object.create(prototype)
479
+ metadataState.seen.set(asset, result)
480
+ for (const field of fields) {
481
+ if (field.key === 'data' && field.bytes) {
482
+ CircuitJsonReadOnlyDocument.#defineProtectedData(
483
+ result,
484
+ new Uint8Array(field.bytes),
485
+ field.descriptor.enumerable
486
+ )
487
+ continue
488
+ }
489
+ if (field.key === 'source') {
490
+ const source = CircuitJsonReadOnlyDocument.#captureMetadataRoot(
491
+ field.descriptor.value,
492
+ metadataState
493
+ )
494
+ Object.defineProperty(result, field.key, {
495
+ ...field.descriptor,
496
+ value: source
497
+ })
498
+ continue
499
+ }
500
+ if (field.key === 'data' && field.range) {
501
+ CircuitJsonReadOnlyDocument.#defineProtectedData(
502
+ result,
503
+ BinaryDataSnapshot.copyBytes(
504
+ field.descriptor.value,
505
+ field.range
506
+ ),
507
+ field.descriptor.enumerable
508
+ )
509
+ continue
510
+ }
511
+ Object.defineProperty(result, field.key, field.descriptor)
512
+ }
513
+ SEALED_ASSETS.add(result)
514
+ ASSET_PAYLOAD_LENGTHS.set(result, payloadLength)
515
+ metadataState.seen.set(result, result)
516
+ return result
517
+ }
518
+
519
+ /**
520
+ * Accounts one pre-owned asset in a new request without cloning metadata.
521
+ * @param {Record<string, any>} asset Owned sealed asset.
522
+ * @param {{ seen: Map<object, unknown>, accounted: Set<object>, items: number }} metadataState Shared metadata state.
523
+ * @param {Function | null} acceptPayload Optional payload-limit callback.
524
+ * @returns {void}
525
+ */
526
+ static #accountSealedAsset(asset, metadataState, acceptPayload) {
527
+ if (acceptPayload !== null && typeof acceptPayload !== 'function') {
528
+ throw new TypeError('Asset payload check must be a function.')
529
+ }
530
+ if (metadataState.seen.get(asset) !== asset) {
531
+ const source = Object.getOwnPropertyDescriptor(asset, 'source')
532
+ if (source && !Object.hasOwn(source, 'value')) {
533
+ throw new TypeError(
534
+ 'Canonical asset source must be an own data property.'
535
+ )
536
+ }
537
+ StructuredDataSnapshot.account(
538
+ source ? source.value : null,
539
+ metadataState
540
+ )
541
+ metadataState.seen.set(asset, asset)
542
+ }
543
+ if (!acceptPayload) return
544
+ const identity = { id: undefined, name: undefined }
545
+ for (const field of ['id', 'name']) {
546
+ const descriptor = Object.getOwnPropertyDescriptor(asset, field)
547
+ if (descriptor && Object.hasOwn(descriptor, 'value')) {
548
+ identity[field] = descriptor.value
549
+ }
550
+ }
551
+ acceptPayload(ASSET_PAYLOAD_LENGTHS.get(asset), identity)
552
+ }
553
+
554
+ /**
555
+ * Copies one already-captured asset with optional payload materialization.
556
+ * @param {unknown} asset Canonical asset candidate.
557
+ * @param {boolean} includeData Whether to copy payload bytes.
558
+ * @param {boolean} [copySource] Whether to isolate source metadata.
559
+ * @param {{ seen: Map<object, unknown>, items: number } | null} [metadataState] Shared metadata state.
560
+ * @returns {Record<string, any>} Copied fields.
561
+ */
562
+ static #copyAssetFields(
563
+ asset,
564
+ includeData,
565
+ copySource = true,
566
+ metadataState = null
567
+ ) {
568
+ const captured = CircuitJsonReadOnlyDocument.#captureAsset(
569
+ asset,
570
+ metadataState || StructuredDataSnapshot.createState()
571
+ )
572
+ const result = Object.create(null)
573
+ let keys
574
+ try {
575
+ keys = Reflect.ownKeys(captured)
576
+ } catch {
577
+ throw new TypeError('Canonical asset keys could not be inspected.')
578
+ }
579
+ for (const key of keys) {
580
+ let descriptor
581
+ try {
582
+ descriptor = Object.getOwnPropertyDescriptor(captured, key)
583
+ } catch {
584
+ throw new TypeError(
585
+ 'Canonical asset properties could not be inspected.'
586
+ )
587
+ }
588
+ if (key === 'data' && PROTECTED_ASSET_BYTES.has(captured)) {
589
+ result.data = includeData
590
+ ? new Uint8Array(PROTECTED_ASSET_BYTES.get(captured))
591
+ : null
592
+ continue
593
+ }
594
+ if (!descriptor || !Object.hasOwn(descriptor, 'value')) {
595
+ throw new TypeError(
596
+ 'Canonical assets may contain only data properties.'
597
+ )
598
+ }
599
+ if (key === 'source') {
600
+ result.source = copySource
601
+ ? StructuredDataSnapshot.capture(descriptor.value)
602
+ : descriptor.value
603
+ continue
604
+ }
605
+ result[key] =
606
+ key === 'data' && !includeData ? null : descriptor.value
607
+ }
608
+ return result
609
+ }
610
+
611
+ /**
612
+ * Rejects object coercion in canonical scalar asset fields.
613
+ * @param {string} key Asset field name.
614
+ * @param {unknown} value Asset field value.
615
+ * @returns {void}
616
+ */
617
+ static #validateAssetScalar(key, value) {
618
+ const type = typeof value
619
+ if (
620
+ value === null ||
621
+ value === undefined ||
622
+ ['string', 'number', 'boolean', 'bigint'].includes(type)
623
+ ) {
624
+ return
625
+ }
626
+ throw new TypeError(
627
+ `Canonical asset ${key} must be a primitive scalar.`
628
+ )
629
+ }
630
+
631
+ /**
632
+ * Rejects unsupported or coercion-capable asset payload values.
633
+ * @param {unknown} value Asset data value.
634
+ * @returns {void}
635
+ */
636
+ static #validateAssetPayload(value) {
637
+ if (
638
+ value === null ||
639
+ value === undefined ||
640
+ typeof value === 'string' ||
641
+ BinaryDataSnapshot.byteLength(value) !== null
642
+ ) {
643
+ return
644
+ }
645
+ throw new TypeError(
646
+ 'Canonical asset data must be text, binary data, or null.'
647
+ )
648
+ }
649
+
650
+ /**
651
+ * Resolves an opaque metadata budget token or creates local capture state.
652
+ * @param {object | null} budget Metadata budget token.
653
+ * @returns {{ seen: Map<object, unknown>, items: number }} Capture state.
654
+ */
655
+ static #metadataState(budget) {
656
+ if (budget === null || budget === undefined) {
657
+ return StructuredDataSnapshot.createState()
658
+ }
659
+ const state = METADATA_BUDGETS.get(budget)
660
+ if (!state) throw new TypeError('Invalid source-metadata budget.')
661
+ return state
662
+ }
663
+
664
+ /**
665
+ * Captures and brands one metadata root for idempotent document sealing.
666
+ * @param {unknown} value Metadata candidate.
667
+ * @param {{ seen: Map<object, unknown>, items: number }} state Capture state.
668
+ * @returns {unknown} Owned normalized root.
669
+ */
670
+ static #captureMetadataRoot(value, state) {
671
+ if (
672
+ value &&
673
+ typeof value === 'object' &&
674
+ OWNED_METADATA_ROOTS.has(value)
675
+ ) {
676
+ return value
677
+ }
678
+ const snapshot = StructuredDataSnapshot.capture(value, state)
679
+ if (snapshot && typeof snapshot === 'object') {
680
+ OWNED_METADATA_ROOTS.add(snapshot)
681
+ }
682
+ return snapshot
683
+ }
684
+
685
+ /**
686
+ * Replaces every dense array item with its captured asset snapshot.
687
+ * @param {unknown} assets Canonical asset array candidate.
688
+ * @param {{ seen: Map<object, unknown>, items: number }} metadataState Shared metadata state.
689
+ * @returns {void}
690
+ */
691
+ static #captureAssetData(assets, metadataState) {
692
+ if (!Array.isArray(assets)) return
693
+ let prototype
694
+ let keys
695
+ let lengthDescriptor
696
+ try {
697
+ prototype = Object.getPrototypeOf(assets)
698
+ keys = Reflect.ownKeys(assets)
699
+ lengthDescriptor = Object.getOwnPropertyDescriptor(assets, 'length')
700
+ } catch {
701
+ throw new TypeError(
702
+ 'Canonical assets must expose a dense plain array.'
703
+ )
704
+ }
705
+ const length = lengthDescriptor?.value
706
+ if (
707
+ prototype !== Array.prototype ||
708
+ !Number.isSafeInteger(length) ||
709
+ length < 0 ||
710
+ keys.length !== length + 1
711
+ ) {
712
+ throw new TypeError(
713
+ 'Canonical assets must expose a dense plain array.'
714
+ )
715
+ }
716
+ for (let index = 0; index < length; index += 1) {
717
+ let itemDescriptor
718
+ try {
719
+ itemDescriptor = Object.getOwnPropertyDescriptor(
720
+ assets,
721
+ String(index)
722
+ )
723
+ } catch {
724
+ throw new TypeError(
725
+ 'Canonical assets could not be inspected safely.'
726
+ )
727
+ }
728
+ if (!itemDescriptor || !Object.hasOwn(itemDescriptor, 'value')) {
729
+ throw new TypeError(
730
+ 'Canonical assets must contain only dense data properties.'
731
+ )
732
+ }
733
+ const captured = CircuitJsonReadOnlyDocument.#captureAsset(
734
+ itemDescriptor.value,
735
+ metadataState,
736
+ true
737
+ )
738
+ if (captured !== itemDescriptor.value) {
739
+ Object.defineProperty(assets, String(index), {
740
+ ...itemDescriptor,
741
+ value: captured
742
+ })
743
+ }
744
+ }
745
+ }
746
+
747
+ /**
748
+ * Protects one toolkit-owned binary asset before it crosses a public boundary.
749
+ * @param {unknown} asset Canonical asset candidate.
750
+ * @returns {void}
751
+ */
752
+ static #protectAsset(asset) {
753
+ if (!asset || typeof asset !== 'object') return
754
+ if (PROTECTED_ASSET_BYTES.has(asset)) return
755
+ let descriptor
756
+ try {
757
+ descriptor = Object.getOwnPropertyDescriptor(asset, 'data')
758
+ } catch {
759
+ throw new TypeError(
760
+ 'Canonical asset data could not be inspected safely.'
761
+ )
762
+ }
763
+ if (!descriptor || !Object.hasOwn(descriptor, 'value')) {
764
+ throw new TypeError(
765
+ 'Canonical asset data must be an own data property.'
766
+ )
767
+ }
768
+ CircuitJsonReadOnlyDocument.#validateAssetPayload(descriptor.value)
769
+ const range = BinaryDataSnapshot.describe(descriptor.value)
770
+ if (!range) return
771
+ if (Object.isFrozen(asset)) {
772
+ throw new TypeError(
773
+ 'Frozen canonical asset bytes cannot be protected.'
774
+ )
775
+ }
776
+ CircuitJsonReadOnlyDocument.#defineProtectedData(
777
+ asset,
778
+ BinaryDataSnapshot.copyBytes(descriptor.value, range),
779
+ descriptor.enumerable
780
+ )
781
+ }
782
+
783
+ /**
784
+ * Installs one private byte snapshot and a defensive-copy getter.
785
+ * @param {object} asset Owned asset.
786
+ * @param {Uint8Array} bytes Isolated bytes.
787
+ * @param {boolean} enumerable Original enumerability.
788
+ * @returns {void}
789
+ */
790
+ static #defineProtectedData(asset, bytes, enumerable) {
791
+ PROTECTED_ASSET_BYTES.set(asset, bytes)
792
+ /** @returns {Uint8Array} A defensive payload copy. */
793
+ const readAssetData = () => new Uint8Array(bytes)
794
+ PROTECTED_ASSET_DATA_GETTERS.add(readAssetData)
795
+ Object.defineProperty(asset, 'data', {
796
+ configurable: false,
797
+ enumerable,
798
+ get: readAssetData
799
+ })
800
+ }
801
+
802
+ /**
803
+ * Measures UTF-8 bytes without allocating an encoded copy.
804
+ * @param {string} value Text payload.
805
+ * @returns {number} UTF-8 byte length.
806
+ */
807
+ static #utf8ByteLength(value) {
808
+ let length = 0
809
+ for (const character of value) {
810
+ const codePoint = character.codePointAt(0)
811
+ length +=
812
+ codePoint <= 0x7f
813
+ ? 1
814
+ : codePoint <= 0x7ff
815
+ ? 2
816
+ : codePoint <= 0xffff
817
+ ? 3
818
+ : 4
819
+ }
820
+ return length
821
+ }
822
+
823
+ /**
824
+ * Freezes one owned clone-safe container in child-first order.
825
+ * @param {unknown} value Candidate value.
826
+ * @param {Set<object>} seen Visited object identities.
827
+ * @returns {void}
828
+ */
829
+ static #freezeValue(value, seen) {
830
+ const stack = [{ exit: false, value }]
831
+ while (stack.length) {
832
+ const frame = stack.pop()
833
+ const current = frame.value
834
+ if (frame.exit) {
835
+ try {
836
+ Object.freeze(current)
837
+ } catch {
838
+ throw new TypeError(
839
+ 'Canonical document values could not be frozen safely.'
840
+ )
841
+ }
842
+ continue
843
+ }
844
+ if (!CircuitJsonReadOnlyDocument.#container(current)) continue
845
+ if (seen.has(current)) continue
846
+ seen.add(current)
847
+
848
+ let keys
849
+ try {
850
+ keys = Reflect.ownKeys(current)
851
+ } catch {
852
+ throw new TypeError(
853
+ 'Canonical document values could not be inspected safely.'
854
+ )
855
+ }
856
+ const children = []
857
+ for (const key of keys) {
858
+ let descriptor
859
+ try {
860
+ descriptor = Object.getOwnPropertyDescriptor(current, key)
861
+ } catch {
862
+ throw new TypeError(
863
+ 'Canonical document properties could not be inspected safely.'
864
+ )
865
+ }
866
+ if (!descriptor || !Object.hasOwn(descriptor, 'value')) {
867
+ const protectedData =
868
+ key === 'data' && PROTECTED_ASSET_BYTES.has(current)
869
+ const protectedExtensionBinary =
870
+ ProtectedExtensionBinaryBoundary.isProtected(descriptor)
871
+ if (protectedData || protectedExtensionBinary) continue
872
+ throw new TypeError(
873
+ 'Canonical document may contain only data properties.'
874
+ )
875
+ }
876
+ children.push(descriptor.value)
877
+ }
878
+ stack.push({ exit: true, value: current })
879
+ for (let index = children.length - 1; index >= 0; index -= 1) {
880
+ stack.push({ exit: false, value: children[index] })
881
+ }
882
+ }
883
+ }
884
+
885
+ /**
886
+ * Returns true for containers that can be frozen without breaking views.
887
+ * @param {unknown} value Candidate value.
888
+ * @returns {boolean} Whether the value is a supported container.
889
+ */
890
+ static #container(value) {
891
+ if (!value || typeof value !== 'object') return false
892
+ try {
893
+ if (Array.isArray(value)) {
894
+ if (Object.getPrototypeOf(value) !== Array.prototype) {
895
+ throw new TypeError(
896
+ 'Canonical document arrays must use Array.prototype.'
897
+ )
898
+ }
899
+ return true
900
+ }
901
+ const prototype = Object.getPrototypeOf(value)
902
+ if (prototype === Object.prototype || prototype === null) {
903
+ return true
904
+ }
905
+
906
+ // Binary values and other platform objects are deliberately not
907
+ // frozen here. Their payload-specific protection happens before
908
+ // this traversal; canonical arrays and records are the only
909
+ // containers whose children need recursive sealing.
910
+ return false
911
+ } catch {
912
+ throw new TypeError(
913
+ 'Canonical document values could not be inspected safely.'
914
+ )
915
+ }
916
+ }
917
+ }
918
+
919
+ Object.freeze(CircuitJsonReadOnlyDocument.prototype)
920
+ Object.freeze(CircuitJsonReadOnlyDocument)