circuitjson-toolkit 1.2.1 → 1.3.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.
package/README.md CHANGED
@@ -61,6 +61,14 @@ million structured items, while exact project envelopes receive independent
61
61
  per-document, project-metadata, aggregate-byte, and repeated-work limits. See
62
62
  the [1.2.1 release notes](docs/release-notes-v1.2.1.md).
63
63
 
64
+ Version 1.3.0 adds an explicit fast preparation path for documents received
65
+ through platform structured clone. Large native extension graphs can be owned
66
+ without exception-driven binary probing of every ordinary record, while the
67
+ general preparation path remains prototype-independent for arbitrary caller
68
+ input. Cross-realm and altered-prototype buffers, typed views, byte limits,
69
+ defensive copies, and all existing return shapes remain supported.
70
+ See the [1.3.0 release notes](docs/release-notes-v1.3.0.md).
71
+
64
72
  Before 1.1.0:
65
73
 
66
74
  ```js
@@ -329,6 +337,7 @@ copy while keeping sync, direct async, and worker results mutation-isolated.
329
337
  - [1.1.2 release notes](docs/release-notes-v1.1.2.md)
330
338
  - [1.2.0 release notes](docs/release-notes-v1.2.0.md)
331
339
  - [1.2.1 release notes](docs/release-notes-v1.2.1.md)
340
+ - [1.3.0 release notes](docs/release-notes-v1.3.0.md)
332
341
  - [Library scope](spec/library-scope.md)
333
342
 
334
343
  ## Package scope
package/docs/api.md CHANGED
@@ -109,6 +109,28 @@ Validation, indexes, render primitives, and other derived values are cached by
109
109
  the context. Public outputs remain detached or immutable; a caller cannot
110
110
  mutate later results through a returned object.
111
111
 
112
+ Hosts that receive a document from the platform structured-clone algorithm may
113
+ use the explicit provenance-aware entry point:
114
+
115
+ ```js
116
+ const context = CircuitJsonDocumentContext.prepareStructuredClone(
117
+ message.data,
118
+ {
119
+ indexes: ['elements', 'relations']
120
+ }
121
+ )
122
+ ```
123
+
124
+ `prepareStructuredClone()` has the same options, return type, validation,
125
+ ownership, limits, and derived-cache behavior as `prepare()`. It avoids
126
+ exception-driven raw-buffer brand checks for ordinary objects by relying on the
127
+ standard local prototypes guaranteed by a completed platform structured clone.
128
+ Use it only for the exact structured-cloned result (or a graph created entirely
129
+ by the toolkit after that boundary). Use `prepare()` for arbitrary caller-owned,
130
+ cross-realm, proxy-backed, or prototype-modified input. The general path keeps
131
+ intrinsic binary slots authoritative and preserves altered-prototype
132
+ `ArrayBuffer`, `SharedArrayBuffer`, typed-array, and `DataView` values.
133
+
112
134
  ## Root entrypoint
113
135
 
114
136
  `circuitjson-toolkit` has an exact 17-class root. The 14 canonical classes are:
@@ -0,0 +1,45 @@
1
+ # circuitjson-toolkit 1.3.0
2
+
3
+ ## Faster canonical extension ownership
4
+
5
+ This minor release adds an explicit fast ownership path for large native
6
+ extension graphs received through the platform structured-clone algorithm.
7
+ Ordinary records, arrays, and standard local data containers are classified
8
+ without deliberately invoking incompatible intrinsic getters, while genuine
9
+ buffers and views still use captured platform slots as the final authority.
10
+
11
+ The new `CircuitJsonDocumentContext.prepareStructuredClone(document, options?)`
12
+ method has the same options and return shape as `prepare()`. It is intended for
13
+ the exact result of a completed platform structured clone, or for a graph
14
+ created entirely by the toolkit after that boundary. Hosts must continue to use
15
+ `prepare()` for arbitrary caller-owned, cross-realm, proxy-backed, or
16
+ prototype-modified input. The optimization is source-format-neutral and
17
+ requires no example-specific handling.
18
+
19
+ Measured on the deterministic standard-built-in metadata workload used during
20
+ development, capturing 50,000 populated records fell from about 3.7 seconds to
21
+ about 0.16 seconds. Browser timing for the combined library and ECAD Forge
22
+ release is documented by the application release because network transfer,
23
+ application interaction preparation, and SVG mounting are outside this
24
+ library's ownership boundary.
25
+
26
+ ## Compatibility and API changes
27
+
28
+ - No public class, package subpath, parameter, or return field is removed or
29
+ renamed.
30
+ - `CircuitJsonDocumentContext.prepareStructuredClone(document, options?)` is a
31
+ new opt-in method for the explicit structured-clone provenance contract.
32
+ - Parser, project, renderer, worker, and extension result shapes are unchanged.
33
+ - The existing `prepare()` path remains exact and prototype-independent.
34
+ Cross-realm and altered-prototype `ArrayBuffer`, `SharedArrayBuffer`, typed
35
+ array, and `DataView` values, resizable buffers, byte ceilings, and
36
+ defensive-copy behavior are retained.
37
+ - Proven standard plain-data graphs no longer generate caught exceptions merely
38
+ to prove that each ordinary node is not binary data.
39
+
40
+ The new regression coverage pauses on every thrown exception in an isolated
41
+ runtime, proving that a representative proven-standard metadata graph completes
42
+ with zero binary-probe exceptions. Exact-path regressions cover altered and
43
+ cross-realm binary objects plus proxy prototype traps. Existing adversarial and
44
+ full-suite contracts continue to cover hostile accessors, worker parity,
45
+ mutation isolation, and bounded extension ownership.
package/docs/testing.md CHANGED
@@ -21,6 +21,13 @@ checked-in artifacts. Check mode is read-only. It also preserves upstream
21
21
  transform rejection boundaries, including malformed SI-unit strings that the
22
22
  upstream schema rejects by throwing instead of returning a failed parse.
23
23
 
24
+ `npm test` first runs the benchmark contract in an uncontended process, then
25
+ runs the functional test modules with normal Node test concurrency, and finally
26
+ runs the remaining elapsed-time modules in isolated phases. This preserves
27
+ complete test coverage while preventing unrelated CPU-heavy suites from
28
+ contaminating timing comparisons. `npm test -- <node-test-arguments>` still
29
+ forwards focused arguments directly to the Node test runner.
30
+
24
31
  The schema differential suite samples every upstream union leaf, compares
25
32
  required-field failures, exercises transform-owned resistor, capacitor,
26
33
  inductor, current-source, and crystal fields, and verifies that a fresh compile
@@ -92,6 +99,13 @@ round-trip behavior, and visible rejection beyond the separate 128 MiB
92
99
  extension payload ceiling. A 3 MiB binary regression additionally requires
93
100
  byte-backed `Uint8Array` shape, defensive-copy mutation isolation, direct and
94
101
  worker parity, bounded elapsed time, and bounded JavaScript heap growth.
102
+ An isolated inspector regression pauses on all thrown exceptions and requires
103
+ ordinary populated records and arrays in an explicitly proven standard-built-in
104
+ graph to complete ownership without exception-driven binary probing. Separate
105
+ exact-path regressions require cross-realm buffers, altered-prototype
106
+ `ArrayBuffer` and `SharedArrayBuffer` values, altered view prototypes, and
107
+ proxy-backed non-binary candidates to retain the prior prototype-independent
108
+ behavior.
95
109
 
96
110
  Variant-geometry tests also lock rotation-local polygon-plated pad extents and
97
111
  pill drill dimensions, independent outer/drill rotations, and every legal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circuitjson-toolkit",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Canonical CircuitJSON parsing, project, rendering, query, manufacturing, simulation, and scene contracts",
5
5
  "keywords": [
6
6
  "circuitjson",
@@ -55,6 +55,7 @@
55
55
  "docs/release-notes-v1.1.2.md",
56
56
  "docs/release-notes-v1.2.0.md",
57
57
  "docs/release-notes-v1.2.1.md",
58
+ "docs/release-notes-v1.3.0.md",
58
59
  "docs/testing.md",
59
60
  "spec",
60
61
  "LICENSE",
@@ -67,7 +68,7 @@
67
68
  "REUSE.toml"
68
69
  ],
69
70
  "scripts": {
70
- "test": "node --test",
71
+ "test": "node scripts/run-tests.mjs",
71
72
  "format": "prettier --write .",
72
73
  "check:format": "prettier --check .",
73
74
  "sync:schema": "node scripts/sync-circuit-json-schema.mjs",
@@ -22,6 +22,10 @@ const TYPED_ARRAY_LENGTH = Object.getOwnPropertyDescriptor(
22
22
  TYPED_ARRAY_PROTOTYPE,
23
23
  'byteLength'
24
24
  )?.get
25
+ const TYPED_ARRAY_TAG = Object.getOwnPropertyDescriptor(
26
+ TYPED_ARRAY_PROTOTYPE,
27
+ Symbol.toStringTag
28
+ )?.get
25
29
  const DATA_VIEW_BUFFER = Object.getOwnPropertyDescriptor(
26
30
  DataView.prototype,
27
31
  'buffer'
@@ -34,7 +38,40 @@ const DATA_VIEW_LENGTH = Object.getOwnPropertyDescriptor(
34
38
  DataView.prototype,
35
39
  'byteLength'
36
40
  )?.get
41
+ const ARRAY_BUFFER_IS_VIEW = ArrayBuffer.isView
42
+ const ARRAY_IS_ARRAY = Array.isArray
43
+ const OBJECT_GET_PROTOTYPE_OF = Object.getPrototypeOf
44
+ const REFLECT_APPLY = Reflect.apply
45
+ const ARRAY_BUFFER_PROTOTYPE = ArrayBuffer.prototype
46
+ const SHARED_ARRAY_BUFFER_PROTOTYPE =
47
+ typeof SharedArrayBuffer === 'function' ? SharedArrayBuffer.prototype : null
48
+ const ARRAY_PROTOTYPE = Array.prototype
49
+ const OBJECT_PROTOTYPE = Object.prototype
50
+ const DATE_PROTOTYPE = Date.prototype
51
+ const REGEXP_PROTOTYPE = RegExp.prototype
52
+ const MAP_PROTOTYPE = Map.prototype
53
+ const SET_PROTOTYPE = Set.prototype
37
54
  const UINT8_ARRAY_SET = Uint8Array.prototype.set
55
+ const TYPED_ARRAY_CONSTRUCTORS = new Map([
56
+ ['Int8Array', Int8Array],
57
+ ['Uint8Array', Uint8Array],
58
+ ['Uint8ClampedArray', Uint8ClampedArray],
59
+ ['Int16Array', Int16Array],
60
+ ['Uint16Array', Uint16Array],
61
+ ['Int32Array', Int32Array],
62
+ ['Uint32Array', Uint32Array],
63
+ ...(typeof globalThis.Float16Array === 'function'
64
+ ? [['Float16Array', globalThis.Float16Array]]
65
+ : []),
66
+ ['Float32Array', Float32Array],
67
+ ['Float64Array', Float64Array],
68
+ ...(typeof BigInt64Array === 'function'
69
+ ? [
70
+ ['BigInt64Array', BigInt64Array],
71
+ ['BigUint64Array', BigUint64Array]
72
+ ]
73
+ : [])
74
+ ])
38
75
 
39
76
  /**
40
77
  * Reads and copies binary platform objects through captured intrinsic slots.
@@ -46,6 +83,104 @@ export class BinaryDataSnapshot {
46
83
  * @returns {{ buffer: ArrayBuffer | SharedArrayBuffer, byteOffset: number, byteLength: number, kind: 'buffer' | 'typed-array' | 'data-view' } | null} Intrinsic binary range.
47
84
  */
48
85
  static describe(value) {
86
+ return BinaryDataSnapshot.#describe(value, false)
87
+ }
88
+
89
+ /**
90
+ * Describes binary data in a graph whose platform built-ins are proven to
91
+ * have their standard local prototypes.
92
+ * @param {unknown} value Binary candidate from a normalized graph.
93
+ * @returns {{ buffer: ArrayBuffer | SharedArrayBuffer, byteOffset: number, byteLength: number, kind: 'buffer' | 'typed-array' | 'data-view' } | null} Intrinsic binary range.
94
+ */
95
+ static describeStandard(value) {
96
+ return BinaryDataSnapshot.#describe(value, true)
97
+ }
98
+
99
+ /**
100
+ * Classifies one binary candidate with either exact or proven-standard
101
+ * raw-buffer handling.
102
+ * @param {unknown} value Binary candidate.
103
+ * @param {boolean} standardBuiltins Whether local prototype identity is proven.
104
+ * @returns {{ buffer: ArrayBuffer | SharedArrayBuffer, byteOffset: number, byteLength: number, kind: 'buffer' | 'typed-array' | 'data-view' } | null} Intrinsic binary range.
105
+ */
106
+ static #describe(value, standardBuiltins) {
107
+ if (value === null || typeof value !== 'object') return null
108
+
109
+ if (BinaryDataSnapshot.#isView(value)) {
110
+ const typed = BinaryDataSnapshot.#view(
111
+ value,
112
+ TYPED_ARRAY_BUFFER,
113
+ TYPED_ARRAY_OFFSET,
114
+ TYPED_ARRAY_LENGTH,
115
+ 'typed-array'
116
+ )
117
+ if (typed) return typed
118
+ return BinaryDataSnapshot.#view(
119
+ value,
120
+ DATA_VIEW_BUFFER,
121
+ DATA_VIEW_OFFSET,
122
+ DATA_VIEW_LENGTH,
123
+ 'data-view'
124
+ )
125
+ }
126
+
127
+ if (!standardBuiltins) {
128
+ return BinaryDataSnapshot.#rawBuffer(value)
129
+ }
130
+
131
+ let prototype
132
+ try {
133
+ prototype = OBJECT_GET_PROTOTYPE_OF(value)
134
+ } catch {
135
+ return null
136
+ }
137
+
138
+ if (prototype === ARRAY_BUFFER_PROTOTYPE) {
139
+ const byteLength = BinaryDataSnapshot.#callLength(
140
+ ARRAY_BUFFER_LENGTH,
141
+ value
142
+ )
143
+ return byteLength === null
144
+ ? null
145
+ : {
146
+ buffer: value,
147
+ byteOffset: 0,
148
+ byteLength,
149
+ kind: 'buffer'
150
+ }
151
+ }
152
+ if (
153
+ SHARED_ARRAY_BUFFER_PROTOTYPE &&
154
+ prototype === SHARED_ARRAY_BUFFER_PROTOTYPE
155
+ ) {
156
+ const byteLength = BinaryDataSnapshot.#callLength(
157
+ SHARED_ARRAY_BUFFER_LENGTH,
158
+ value
159
+ )
160
+ return byteLength === null
161
+ ? null
162
+ : {
163
+ buffer: value,
164
+ byteOffset: 0,
165
+ byteLength,
166
+ kind: 'buffer'
167
+ }
168
+ }
169
+ if (BinaryDataSnapshot.#isKnownNonBinary(value, prototype)) {
170
+ return null
171
+ }
172
+
173
+ // Unknown prototypes can represent genuine buffers from another realm.
174
+ // Keep intrinsic brand checks as the authority for that uncommon path.
175
+ return BinaryDataSnapshot.#rawBuffer(value)
176
+ }
177
+
178
+ /**
179
+ * Applies exact raw ArrayBuffer and SharedArrayBuffer brand getters.
180
+ * @param {object} value Raw-buffer candidate.
181
+ * @returns {{ buffer: ArrayBuffer | SharedArrayBuffer, byteOffset: 0, byteLength: number, kind: 'buffer' } | null} Raw buffer range.
182
+ */
183
+ static #rawBuffer(value) {
49
184
  const arrayBufferLength = BinaryDataSnapshot.#callLength(
50
185
  ARRAY_BUFFER_LENGTH,
51
186
  value
@@ -71,21 +206,7 @@ export class BinaryDataSnapshot {
71
206
  }
72
207
  }
73
208
 
74
- const typed = BinaryDataSnapshot.#view(
75
- value,
76
- TYPED_ARRAY_BUFFER,
77
- TYPED_ARRAY_OFFSET,
78
- TYPED_ARRAY_LENGTH,
79
- 'typed-array'
80
- )
81
- if (typed) return typed
82
- return BinaryDataSnapshot.#view(
83
- value,
84
- DATA_VIEW_BUFFER,
85
- DATA_VIEW_OFFSET,
86
- DATA_VIEW_LENGTH,
87
- 'data-view'
88
- )
209
+ return null
89
210
  }
90
211
 
91
212
  /**
@@ -133,8 +254,7 @@ export class BinaryDataSnapshot {
133
254
  if (range.kind === 'buffer') return bytes.buffer
134
255
  if (range.kind === 'data-view') return new DataView(bytes.buffer)
135
256
 
136
- const prototype = Object.getPrototypeOf(value)
137
- const Constructor = BinaryDataSnapshot.#typedArrayConstructor(prototype)
257
+ const Constructor = BinaryDataSnapshot.#typedArrayConstructor(value)
138
258
  if (!Constructor || Constructor === Uint8Array) return bytes
139
259
  const bytesPerElement = Constructor.BYTES_PER_ELEMENT
140
260
  if (bytes.byteLength % bytesPerElement !== 0) return bytes
@@ -150,12 +270,44 @@ export class BinaryDataSnapshot {
150
270
  static #callLength(getter, value) {
151
271
  if (typeof getter !== 'function') return null
152
272
  try {
153
- return getter.call(value)
273
+ return REFLECT_APPLY(getter, value, [])
154
274
  } catch {
155
275
  return null
156
276
  }
157
277
  }
158
278
 
279
+ /**
280
+ * Uses the platform's side-effect-free view brand classifier.
281
+ * @param {unknown} value Candidate value.
282
+ * @returns {boolean} Whether the value has ArrayBuffer view slots.
283
+ */
284
+ static #isView(value) {
285
+ try {
286
+ return REFLECT_APPLY(ARRAY_BUFFER_IS_VIEW, ArrayBuffer, [value])
287
+ } catch {
288
+ return false
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Returns true for common local containers that cannot be binary objects.
294
+ * @param {object} value Candidate object.
295
+ * @param {object | null} prototype Captured prototype.
296
+ * @returns {boolean} Whether intrinsic buffer probing can be skipped.
297
+ */
298
+ static #isKnownNonBinary(value, prototype) {
299
+ return (
300
+ ARRAY_IS_ARRAY(value) ||
301
+ prototype === ARRAY_PROTOTYPE ||
302
+ prototype === OBJECT_PROTOTYPE ||
303
+ prototype === null ||
304
+ prototype === DATE_PROTOTYPE ||
305
+ prototype === REGEXP_PROTOTYPE ||
306
+ prototype === MAP_PROTOTYPE ||
307
+ prototype === SET_PROTOTYPE
308
+ )
309
+ }
310
+
159
311
  /**
160
312
  * Reads captured intrinsic view slots without ordinary property access.
161
313
  * @param {unknown} value View candidate.
@@ -175,9 +327,9 @@ export class BinaryDataSnapshot {
175
327
  }
176
328
  try {
177
329
  return {
178
- buffer: bufferGetter.call(value),
179
- byteOffset: offsetGetter.call(value),
180
- byteLength: lengthGetter.call(value),
330
+ buffer: REFLECT_APPLY(bufferGetter, value, []),
331
+ byteOffset: REFLECT_APPLY(offsetGetter, value, []),
332
+ byteLength: REFLECT_APPLY(lengthGetter, value, []),
181
333
  kind
182
334
  }
183
335
  } catch {
@@ -186,30 +338,18 @@ export class BinaryDataSnapshot {
186
338
  }
187
339
 
188
340
  /**
189
- * Maps a genuine typed-array prototype to its platform constructor.
190
- * @param {object | null} prototype Candidate prototype.
341
+ * Maps a genuine typed array's intrinsic tag to a local constructor.
342
+ * @param {unknown} value Genuine typed-array value.
191
343
  * @returns {Function | null} Matching typed-array constructor.
192
344
  */
193
- static #typedArrayConstructor(prototype) {
194
- const constructors = [
195
- Int8Array,
196
- Uint8Array,
197
- Uint8ClampedArray,
198
- Int16Array,
199
- Uint16Array,
200
- Int32Array,
201
- Uint32Array,
202
- Float32Array,
203
- Float64Array,
204
- ...(typeof BigInt64Array === 'function'
205
- ? [BigInt64Array, BigUint64Array]
206
- : [])
207
- ]
208
- return (
209
- constructors.find(
210
- (Constructor) => Constructor.prototype === prototype
211
- ) || null
212
- )
345
+ static #typedArrayConstructor(value) {
346
+ if (typeof TYPED_ARRAY_TAG !== 'function') return null
347
+ try {
348
+ const tag = REFLECT_APPLY(TYPED_ARRAY_TAG, value, [])
349
+ return TYPED_ARRAY_CONSTRUCTORS.get(tag) || null
350
+ } catch {
351
+ return null
352
+ }
213
353
  }
214
354
  }
215
355
 
@@ -74,9 +74,31 @@ export class CircuitJsonDocumentContext {
74
74
  * @returns {CircuitJsonDocumentContext} Prepared request-scoped context.
75
75
  */
76
76
  static prepare(input, options = {}) {
77
+ return CircuitJsonDocumentContext.#prepare(input, options, false)
78
+ }
79
+
80
+ /**
81
+ * Prepares a document whose platform built-ins were normalized by the
82
+ * structured-clone algorithm.
83
+ * @param {unknown} input Structured-cloned document result or existing context.
84
+ * @param {{ indexes?: unknown }} [options] Requested context options.
85
+ * @returns {CircuitJsonDocumentContext} Prepared request-scoped context.
86
+ */
87
+ static prepareStructuredClone(input, options = {}) {
88
+ return CircuitJsonDocumentContext.#prepare(input, options, true)
89
+ }
90
+
91
+ /**
92
+ * Prepares one context with explicit metadata provenance.
93
+ * @param {unknown} input Document result, CircuitJSON model, or context.
94
+ * @param {{ indexes?: unknown }} options Requested context options.
95
+ * @param {boolean} standardBuiltins Whether metadata built-ins have standard local prototypes.
96
+ * @returns {CircuitJsonDocumentContext} Prepared request-scoped context.
97
+ */
98
+ static #prepare(input, options, standardBuiltins) {
77
99
  const context = CircuitJsonDocumentContext.#isContext(input)
78
100
  ? input
79
- : CircuitJsonDocumentContext.#fromInput(input)
101
+ : CircuitJsonDocumentContext.#fromInput(input, standardBuiltins)
80
102
  context.#indexes.ensure(options?.indexes || [])
81
103
  return context
82
104
  }
@@ -181,15 +203,20 @@ export class CircuitJsonDocumentContext {
181
203
  /**
182
204
  * Creates one context and establishes a matching immutable model proof.
183
205
  * @param {unknown} input Document result or CircuitJSON model.
206
+ * @param {boolean} standardBuiltins Whether metadata built-ins have standard local prototypes.
184
207
  * @returns {CircuitJsonDocumentContext} New context.
185
208
  */
186
- static #fromInput(input) {
209
+ static #fromInput(input, standardBuiltins) {
187
210
  const document = CircuitJsonDocumentContext.#normalizeDocument(input)
188
211
  const validationPasses = CircuitJsonValidationProof.has(document)
189
212
  ? 0
190
213
  : 1
191
- const readonlyDocument =
192
- CircuitJsonValidationProof.validateAndAttach(document)
214
+ const readonlyDocument = CircuitJsonValidationProof.validateAndAttach(
215
+ document,
216
+ {
217
+ standardBuiltins
218
+ }
219
+ )
193
220
  const model = CircuitJsonDocumentContext.#ownData(
194
221
  readonlyDocument,
195
222
  'model'
@@ -41,9 +41,10 @@ export class CircuitJsonReadOnlyDocument {
41
41
  * Seals an envelope whose exact model was already deeply frozen by validation.
42
42
  * @param {Record<string, any>} document Canonical document envelope.
43
43
  * @param {object[]} model Exact deeply frozen model owned by the proof.
44
+ * @param {{ standardBuiltins?: boolean }} [options] Proven metadata provenance.
44
45
  * @returns {Record<string, any>} The same read-only envelope.
45
46
  */
46
- static freezeValidated(document, model) {
47
+ static freezeValidated(document, model, options = {}) {
47
48
  if (!CircuitJsonValidationAuthority.permitsSeal(model)) {
48
49
  throw new TypeError(
49
50
  'Validated document sealing requires an unforgeable validation proof.'
@@ -76,7 +77,8 @@ export class CircuitJsonReadOnlyDocument {
76
77
  }
77
78
  return CircuitJsonReadOnlyDocument.#freezeDocument(
78
79
  document,
79
- frozenRoots
80
+ frozenRoots,
81
+ options?.standardBuiltins === true
80
82
  )
81
83
  }
82
84
 
@@ -84,14 +86,18 @@ export class CircuitJsonReadOnlyDocument {
84
86
  * Captures owned boundaries and freezes an envelope with known frozen roots.
85
87
  * @param {Record<string, any>} document Canonical document envelope.
86
88
  * @param {Set<object>} frozenRoots Deeply frozen roots that need no revisit.
89
+ * @param {boolean} [standardBuiltins] Whether extension built-ins have proven standard prototypes.
87
90
  * @returns {Record<string, any>} The same read-only envelope.
88
91
  */
89
- static #freezeDocument(document, frozenRoots) {
92
+ static #freezeDocument(document, frozenRoots, standardBuiltins = false) {
90
93
  const assets = CircuitJsonReadOnlyDocument.#documentAssets(document)
91
94
  const metadataState = StructuredDataSnapshot.createState()
92
95
  CircuitJsonReadOnlyDocument.#captureAssetData(assets, metadataState)
93
96
  const extensions =
94
- CircuitJsonReadOnlyDocument.#captureDocumentExtensions(document)
97
+ CircuitJsonReadOnlyDocument.#captureDocumentExtensions(
98
+ document,
99
+ standardBuiltins
100
+ )
95
101
  if (extensions && typeof extensions === 'object') {
96
102
  frozenRoots.add(extensions)
97
103
  }
@@ -199,9 +205,10 @@ export class CircuitJsonReadOnlyDocument {
199
205
  * transfer-sized item and byte ceilings.
200
206
  * @param {unknown} value Extension candidate.
201
207
  * @param {((snapshot: unknown) => unknown) | null} [normalize] Optional normalization over the owned mutable snapshot.
208
+ * @param {{ standardBuiltins?: boolean }} [options] Proven source-graph provenance.
202
209
  * @returns {unknown} Deeply immutable owned extension metadata.
203
210
  */
204
- static copyReadonlyExtensionValue(value, normalize = null) {
211
+ static copyReadonlyExtensionValue(value, normalize = null, options = {}) {
205
212
  if (
206
213
  value &&
207
214
  typeof value === 'object' &&
@@ -216,7 +223,10 @@ export class CircuitJsonReadOnlyDocument {
216
223
  }
217
224
  const snapshot = StructuredDataSnapshot.capture(
218
225
  value,
219
- StructuredDataSnapshot.createState(EXTENSION_METADATA_LIMITS)
226
+ StructuredDataSnapshot.createState({
227
+ ...EXTENSION_METADATA_LIMITS,
228
+ standardBuiltins: options?.standardBuiltins === true
229
+ })
220
230
  )
221
231
  ProtectedExtensionBinaryBoundary.protect(snapshot)
222
232
  const normalized = normalize ? normalize(snapshot) : snapshot
@@ -327,9 +337,10 @@ export class CircuitJsonReadOnlyDocument {
327
337
  * Captures an unowned worker or caller extension root with extension-sized
328
338
  * bounds before the generic source metadata pass begins.
329
339
  * @param {Record<string, any>} document Canonical document envelope.
340
+ * @param {boolean} standardBuiltins Whether extension built-ins have proven standard prototypes.
330
341
  * @returns {unknown} Owned extension root or undefined.
331
342
  */
332
- static #captureDocumentExtensions(document) {
343
+ static #captureDocumentExtensions(document, standardBuiltins) {
333
344
  let descriptor
334
345
  try {
335
346
  descriptor = Object.getOwnPropertyDescriptor(document, 'extensions')
@@ -345,7 +356,9 @@ export class CircuitJsonReadOnlyDocument {
345
356
  )
346
357
  }
347
358
  const captured = CircuitJsonReadOnlyDocument.copyReadonlyExtensionValue(
348
- descriptor.value
359
+ descriptor.value,
360
+ null,
361
+ { standardBuiltins }
349
362
  )
350
363
  if (captured === descriptor.value) return captured
351
364
  Object.defineProperty(document, 'extensions', {
@@ -53,9 +53,10 @@ export class CircuitJsonValidationProof {
53
53
  /**
54
54
  * Validates, freezes, and proves one canonical document envelope.
55
55
  * @param {Record<string, any>} document Canonical document envelope.
56
+ * @param {{ standardBuiltins?: boolean }} [options] Proven metadata provenance.
56
57
  * @returns {Record<string, any>} The same read-only document envelope.
57
58
  */
58
- static validateAndAttach(document) {
59
+ static validateAndAttach(document, options = {}) {
59
60
  const model = CircuitJsonValidationProof.#requireModelData(document)
60
61
  if (!CircuitJsonValidationProof.#matches(document, model)) {
61
62
  const errors =
@@ -80,7 +81,8 @@ export class CircuitJsonValidationProof {
80
81
  }
81
82
  const readonlyDocument = CircuitJsonReadOnlyDocument.freezeValidated(
82
83
  document,
83
- model
84
+ model,
85
+ options
84
86
  )
85
87
  if (
86
88
  CircuitJsonValidationProof.#requireModelData(readonlyDocument) !==
@@ -12,7 +12,7 @@ export class ProtectedExtensionBinaryBoundary {
12
12
  * @returns {void}
13
13
  */
14
14
  static protect(root) {
15
- if (BinaryDataSnapshot.describe(root)) {
15
+ if (BinaryDataSnapshot.describeStandard(root)) {
16
16
  throw new TypeError(
17
17
  'Canonical extension root must be a plain data container.'
18
18
  )
@@ -51,7 +51,7 @@ export class ProtectedExtensionBinaryBoundary {
51
51
  stack.push(child)
52
52
  continue
53
53
  }
54
- const binary = BinaryDataSnapshot.describe(child)
54
+ const binary = BinaryDataSnapshot.describeStandard(child)
55
55
  if (!binary) continue
56
56
  const ownedBinary = child
57
57
  /** @returns {ArrayBuffer | Uint8Array | DataView} A defensive binary copy. */
@@ -26,8 +26,8 @@ const SET_VALUES = Set.prototype.values
26
26
  export class StructuredDataSnapshot {
27
27
  /**
28
28
  * Creates state shared by multiple metadata roots in one request.
29
- * @param {{ label?: string, maxBytes?: number, maxItems?: number, preserveBinary?: boolean }} [limits] Capture limits.
30
- * @returns {{ seen: Map<object, unknown>, accounted: Set<object>, bytes: number, items: number, label: string, maxBytes: number, maxItems: number, preserveBinary: boolean }} Capture state.
29
+ * @param {{ label?: string, maxBytes?: number, maxItems?: number, preserveBinary?: boolean, standardBuiltins?: boolean }} [limits] Capture limits and proven graph provenance.
30
+ * @returns {{ seen: Map<object, unknown>, accounted: Set<object>, bytes: number, items: number, label: string, maxBytes: number, maxItems: number, preserveBinary: boolean, standardBuiltins: boolean }} Capture state.
31
31
  */
32
32
  static createState(limits = {}) {
33
33
  const maxBytes =
@@ -52,7 +52,8 @@ export class StructuredDataSnapshot {
52
52
  label: String(limits.label || 'Canonical asset source'),
53
53
  maxBytes,
54
54
  maxItems,
55
- preserveBinary: limits.preserveBinary === true
55
+ preserveBinary: limits.preserveBinary === true,
56
+ standardBuiltins: limits.standardBuiltins === true
56
57
  }
57
58
  }
58
59
 
@@ -71,7 +72,8 @@ export class StructuredDataSnapshot {
71
72
  !Number.isSafeInteger(state.maxBytes) ||
72
73
  !Number.isSafeInteger(state.maxItems) ||
73
74
  typeof state.label !== 'string' ||
74
- typeof state.preserveBinary !== 'boolean'
75
+ typeof state.preserveBinary !== 'boolean' ||
76
+ typeof state.standardBuiltins !== 'boolean'
75
77
  ) {
76
78
  throw new TypeError('Invalid source-metadata capture state.')
77
79
  }
@@ -93,7 +95,8 @@ export class StructuredDataSnapshot {
93
95
  !Number.isSafeInteger(state.maxBytes) ||
94
96
  !Number.isSafeInteger(state.maxItems) ||
95
97
  typeof state.label !== 'string' ||
96
- typeof state.preserveBinary !== 'boolean'
98
+ typeof state.preserveBinary !== 'boolean' ||
99
+ typeof state.standardBuiltins !== 'boolean'
97
100
  ) {
98
101
  throw new TypeError('Invalid source-metadata capture state.')
99
102
  }
@@ -224,7 +227,9 @@ export class StructuredDataSnapshot {
224
227
  if (state.seen.has(value)) return state.seen.get(value)
225
228
  StructuredDataSnapshot.#reserve(state, 1)
226
229
 
227
- const binary = BinaryDataSnapshot.describe(value)
230
+ const binary = state.standardBuiltins
231
+ ? BinaryDataSnapshot.describeStandard(value)
232
+ : BinaryDataSnapshot.describe(value)
228
233
  if (binary) {
229
234
  return StructuredDataSnapshot.#binary(value, binary, state)
230
235
  }