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,905 @@
1
+ import { ToolkitError } from '../contracts/ToolkitError.mjs'
2
+ import { ToolkitProgress } from '../contracts/ToolkitProgress.mjs'
3
+ import { RuntimeProxyBoundary } from '../contracts/RuntimeProxyBoundary.mjs'
4
+ import { CircuitJsonValidationProof } from '../context/CircuitJsonValidationProof.mjs'
5
+ import { TOOLKIT_WORKER_PROTOCOL } from './ToolkitWorkerProtocol.mjs'
6
+ import { WorkerRequestData } from './WorkerRequestData.mjs'
7
+
8
+ const ARRAY_BUFFER_BYTE_LENGTH_GETTER = Object.getOwnPropertyDescriptor(
9
+ ArrayBuffer.prototype,
10
+ 'byteLength'
11
+ )?.get
12
+ const TYPED_ARRAY_PROTOTYPE = Object.getPrototypeOf(Uint8Array.prototype)
13
+ const TYPED_ARRAY_BYTE_LENGTH_GETTER = Object.getOwnPropertyDescriptor(
14
+ TYPED_ARRAY_PROTOTYPE,
15
+ 'byteLength'
16
+ )?.get
17
+ const DATA_VIEW_BYTE_LENGTH_GETTER = Object.getOwnPropertyDescriptor(
18
+ DataView.prototype,
19
+ 'byteLength'
20
+ )?.get
21
+
22
+ const ERROR_CATEGORIES = new Set([
23
+ 'parse',
24
+ 'validation',
25
+ 'unsupported',
26
+ 'cancelled',
27
+ 'runtime'
28
+ ])
29
+ const ERROR_FIELDS = [
30
+ 'name',
31
+ 'message',
32
+ 'code',
33
+ 'category',
34
+ 'format',
35
+ 'source',
36
+ 'location',
37
+ 'details',
38
+ 'cause'
39
+ ]
40
+ const ERROR_CAUSE_FIELDS = ['name', 'message', 'code']
41
+ const ASSET_FIELDS = [
42
+ 'id',
43
+ 'kind',
44
+ 'name',
45
+ 'mediaType',
46
+ 'byteLength',
47
+ 'data',
48
+ 'source'
49
+ ]
50
+ const DIAGNOSTIC_FIELDS = [
51
+ 'code',
52
+ 'severity',
53
+ 'message',
54
+ 'source',
55
+ 'location',
56
+ 'details'
57
+ ]
58
+ const DIAGNOSTIC_SEVERITIES = new Set(['info', 'warning', 'error'])
59
+ const DOCUMENT_RESULT_FIELDS = [
60
+ 'schema',
61
+ 'id',
62
+ 'modelSchema',
63
+ 'model',
64
+ 'source',
65
+ 'extensions',
66
+ 'assets',
67
+ 'diagnostics',
68
+ 'statistics'
69
+ ]
70
+ const PROJECT_RESULT_FIELDS = [
71
+ 'schema',
72
+ 'id',
73
+ 'source',
74
+ 'documents',
75
+ 'project',
76
+ 'extensions',
77
+ 'assets',
78
+ 'diagnostics',
79
+ 'statistics'
80
+ ]
81
+ const PROJECT_DESCRIPTOR_FIELDS = [
82
+ 'id',
83
+ 'name',
84
+ 'format',
85
+ 'documentIds',
86
+ 'relationships'
87
+ ]
88
+ const PROGRESS_FIELDS = new Set([
89
+ 'stage',
90
+ 'detail',
91
+ 'completed',
92
+ 'total',
93
+ 'message'
94
+ ])
95
+ const PROGRESS_STAGES = new Set([
96
+ 'detect',
97
+ 'decode',
98
+ 'project',
99
+ 'validate',
100
+ 'complete'
101
+ ])
102
+ const RESPONSE_FIELDS = {
103
+ progress: ['protocol', 'type', 'requestId', 'progress'],
104
+ result: ['protocol', 'type', 'requestId', 'value'],
105
+ error: ['protocol', 'type', 'requestId', 'error', 'diagnostics']
106
+ }
107
+
108
+ /**
109
+ * Validates the strict data-only responses accepted from parser workers.
110
+ */
111
+ export class WorkerResponseData {
112
+ /**
113
+ * Validates one response header and its exact type-specific field set.
114
+ * @param {unknown} value Response candidate.
115
+ * @returns {Record<string, any>} Accessor-free response fields.
116
+ */
117
+ static message(value) {
118
+ const fields = WorkerResponseData.#record(
119
+ value,
120
+ 'Toolkit worker response'
121
+ )
122
+ if (fields.protocol !== TOOLKIT_WORKER_PROTOCOL) {
123
+ throw WorkerResponseData.#error(
124
+ 'Toolkit worker response used an unknown protocol.'
125
+ )
126
+ }
127
+ const expected = RESPONSE_FIELDS[fields.type]
128
+ if (!expected) {
129
+ throw WorkerResponseData.#error(
130
+ 'Toolkit worker response type is invalid.'
131
+ )
132
+ }
133
+ WorkerResponseData.#exactKeys(
134
+ fields,
135
+ expected,
136
+ 'Toolkit worker response'
137
+ )
138
+ if (
139
+ typeof fields.requestId !== 'string' ||
140
+ !fields.requestId ||
141
+ fields.requestId.length > 256
142
+ ) {
143
+ throw WorkerResponseData.#error(
144
+ 'Toolkit worker response request id is invalid.'
145
+ )
146
+ }
147
+ return fields
148
+ }
149
+
150
+ /**
151
+ * Validates an operation-specific canonical result envelope.
152
+ * @param {'parse' | 'loadProject' | undefined} operation Pending operation.
153
+ * @param {unknown} value Result candidate.
154
+ * @returns {object} Original validated result.
155
+ */
156
+ static result(operation, value) {
157
+ const prepared = WorkerRequestData.prepareResponse(value)
158
+ const fields = WorkerResponseData.#record(
159
+ prepared,
160
+ 'Toolkit worker result'
161
+ )
162
+ if (operation === 'parse') {
163
+ WorkerResponseData.#documentResult(fields, prepared)
164
+ return prepared
165
+ }
166
+ if (operation === 'loadProject') {
167
+ WorkerResponseData.#projectResult(fields, prepared)
168
+ return prepared
169
+ }
170
+ throw WorkerResponseData.#error(
171
+ 'Toolkit worker result has no active operation.'
172
+ )
173
+ }
174
+
175
+ /**
176
+ * Validates and normalizes one progress response without coercion.
177
+ * @param {unknown} value Progress candidate.
178
+ * @param {Record<string, any> | null} previous Previous progress row.
179
+ * @returns {Record<string, any>} Canonical progress row.
180
+ */
181
+ static progress(value, previous) {
182
+ const fields = WorkerResponseData.#record(
183
+ value,
184
+ 'Toolkit worker progress'
185
+ )
186
+ WorkerResponseData.#allowedKeys(
187
+ fields,
188
+ PROGRESS_FIELDS,
189
+ new Set(['stage']),
190
+ 'Toolkit worker progress'
191
+ )
192
+ if (
193
+ typeof fields.stage !== 'string' ||
194
+ !PROGRESS_STAGES.has(fields.stage)
195
+ ) {
196
+ throw WorkerResponseData.#error(
197
+ 'Toolkit worker progress stage is invalid.'
198
+ )
199
+ }
200
+ for (const key of ['detail', 'message']) {
201
+ if (
202
+ fields[key] !== undefined &&
203
+ (typeof fields[key] !== 'string' ||
204
+ !fields[key] ||
205
+ fields[key].length > 4096)
206
+ ) {
207
+ throw WorkerResponseData.#error(
208
+ `Toolkit worker progress ${key} is invalid.`
209
+ )
210
+ }
211
+ }
212
+ for (const key of ['completed', 'total']) {
213
+ if (
214
+ fields[key] !== undefined &&
215
+ (typeof fields[key] !== 'number' ||
216
+ !Number.isFinite(fields[key]) ||
217
+ fields[key] < 0)
218
+ ) {
219
+ throw WorkerResponseData.#error(
220
+ `Toolkit worker progress ${key} is invalid.`
221
+ )
222
+ }
223
+ }
224
+ try {
225
+ return ToolkitProgress.create(fields, previous)
226
+ } catch (error) {
227
+ throw WorkerResponseData.#error(
228
+ 'Toolkit worker progress ordering is invalid.',
229
+ error
230
+ )
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Validates diagnostics and reconstructs one exact remote ToolkitError.
236
+ * @param {unknown} errorValue Remote error candidate.
237
+ * @param {unknown} diagnosticsValue Diagnostics candidate.
238
+ * @returns {ToolkitError} Local typed error.
239
+ */
240
+ static remoteError(errorValue, diagnosticsValue) {
241
+ WorkerResponseData.#diagnostics(diagnosticsValue)
242
+ const fields = WorkerResponseData.#record(
243
+ errorValue,
244
+ 'Toolkit worker error'
245
+ )
246
+ WorkerResponseData.#exactKeys(
247
+ fields,
248
+ ERROR_FIELDS,
249
+ 'Toolkit worker error'
250
+ )
251
+ if (
252
+ fields.name !== 'ToolkitError' ||
253
+ !ERROR_CATEGORIES.has(fields.category)
254
+ ) {
255
+ throw WorkerResponseData.#error(
256
+ 'Toolkit worker error fields are invalid.'
257
+ )
258
+ }
259
+ WorkerResponseData.#boundedString(
260
+ fields.message,
261
+ 4096,
262
+ 'message',
263
+ false
264
+ )
265
+ WorkerResponseData.#boundedString(fields.format, 4096, 'format', false)
266
+ WorkerResponseData.#boundedString(fields.source, 4096, 'source', true)
267
+ WorkerResponseData.#boundedString(fields.code, 256, 'code', false)
268
+ WorkerResponseData.#cause(fields.cause)
269
+ WorkerRequestData.assertCloneSafe({
270
+ location: fields.location,
271
+ details: fields.details,
272
+ cause: fields.cause
273
+ })
274
+ return new ToolkitError(fields.message, {
275
+ code: fields.code,
276
+ category: fields.category,
277
+ format: fields.format,
278
+ source: fields.source,
279
+ location: fields.location,
280
+ details: fields.details,
281
+ cause: fields.cause
282
+ })
283
+ }
284
+
285
+ /**
286
+ * Validates and proves one locally owned received document.
287
+ * @param {Record<string, any>} fields Document fields.
288
+ * @param {Record<string, any>} document Owned document result.
289
+ * @returns {void}
290
+ */
291
+ static #documentResult(fields, document) {
292
+ WorkerResponseData.#exactKeys(
293
+ fields,
294
+ DOCUMENT_RESULT_FIELDS,
295
+ 'Toolkit worker document result'
296
+ )
297
+ if (fields.schema !== 'ecad-toolkit.document.v1') {
298
+ throw WorkerResponseData.#error(
299
+ 'Toolkit worker document schema is invalid.'
300
+ )
301
+ }
302
+ WorkerResponseData.#boundedString(fields.id, 4096, 'id', false)
303
+ const modelSchema = WorkerResponseData.#record(
304
+ fields.modelSchema,
305
+ 'Toolkit worker model schema'
306
+ )
307
+ WorkerResponseData.#exactKeys(
308
+ modelSchema,
309
+ ['name', 'version'],
310
+ 'Toolkit worker model schema'
311
+ )
312
+ if (
313
+ modelSchema.name !== 'circuit-json' ||
314
+ modelSchema.version !== '0.0.446'
315
+ ) {
316
+ throw WorkerResponseData.#error(
317
+ 'Toolkit worker model schema is invalid.'
318
+ )
319
+ }
320
+ WorkerResponseData.#array(
321
+ fields.model,
322
+ 'Toolkit worker document model',
323
+ 2_000_000
324
+ )
325
+ WorkerResponseData.#assets(fields.assets)
326
+ WorkerResponseData.#diagnosticRows(fields.diagnostics, 100_000)
327
+ WorkerResponseData.#documentSource(fields.source)
328
+ WorkerResponseData.#plainResultRecords(fields, [
329
+ 'extensions',
330
+ 'statistics'
331
+ ])
332
+ try {
333
+ CircuitJsonValidationProof.validateAndAttach(document)
334
+ } catch (error) {
335
+ throw WorkerResponseData.#error(
336
+ 'Toolkit worker document model is invalid.',
337
+ error
338
+ )
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Validates one locally owned received project and all nested documents.
344
+ * @param {Record<string, any>} fields Project fields.
345
+ * @param {Record<string, any>} projectResult Owned project result.
346
+ * @returns {void}
347
+ */
348
+ static #projectResult(fields, projectResult) {
349
+ WorkerResponseData.#exactKeys(
350
+ fields,
351
+ PROJECT_RESULT_FIELDS,
352
+ 'Toolkit worker project result'
353
+ )
354
+ if (fields.schema !== 'ecad-toolkit.project.v1') {
355
+ throw WorkerResponseData.#error(
356
+ 'Toolkit worker project schema is invalid.'
357
+ )
358
+ }
359
+ WorkerResponseData.#boundedString(fields.id, 4096, 'id', false)
360
+ WorkerResponseData.#resultCollections(fields, ['documents'])
361
+ const documents = WorkerResponseData.#array(
362
+ fields.documents,
363
+ 'Toolkit worker project documents',
364
+ 4096
365
+ )
366
+ for (let index = 0; index < documents.length; index += 1) {
367
+ const document = documents[index]
368
+ WorkerResponseData.#documentResult(
369
+ WorkerResponseData.#record(
370
+ document,
371
+ 'Toolkit worker project document'
372
+ ),
373
+ projectResult.documents[index]
374
+ )
375
+ }
376
+ WorkerResponseData.#assets(fields.assets)
377
+ WorkerResponseData.#diagnosticRows(fields.diagnostics, 100_000)
378
+ WorkerResponseData.#projectSource(fields.source)
379
+ WorkerResponseData.#plainResultRecords(fields, [
380
+ 'extensions',
381
+ 'statistics'
382
+ ])
383
+ if (fields.project !== null) {
384
+ const project = WorkerResponseData.#record(
385
+ fields.project,
386
+ 'Toolkit worker project descriptor'
387
+ )
388
+ WorkerResponseData.#exactKeys(
389
+ project,
390
+ PROJECT_DESCRIPTOR_FIELDS,
391
+ 'Toolkit worker project descriptor'
392
+ )
393
+ for (const key of ['id', 'name', 'format']) {
394
+ WorkerResponseData.#boundedString(
395
+ project[key],
396
+ 4096,
397
+ `project ${key}`,
398
+ key === 'name'
399
+ )
400
+ }
401
+ const documentIds = WorkerResponseData.#array(
402
+ project.documentIds,
403
+ 'Toolkit worker project document ids',
404
+ 4096
405
+ )
406
+ for (const documentId of documentIds) {
407
+ WorkerResponseData.#boundedString(
408
+ documentId,
409
+ 4096,
410
+ 'project document id',
411
+ false
412
+ )
413
+ }
414
+ if (
415
+ documentIds.length !== documents.length ||
416
+ documentIds.some(
417
+ (documentId, index) => documentId !== documents[index].id
418
+ )
419
+ ) {
420
+ throw WorkerResponseData.#error(
421
+ 'Toolkit worker project document ids are inconsistent.'
422
+ )
423
+ }
424
+ const relationships = WorkerResponseData.#array(
425
+ project.relationships,
426
+ 'Toolkit worker project relationships',
427
+ 100_000
428
+ )
429
+ for (const relationship of relationships) {
430
+ WorkerResponseData.#record(
431
+ relationship,
432
+ 'Toolkit worker project relationship'
433
+ )
434
+ }
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Requires common result collections plus operation-specific collections.
440
+ * @param {Record<string, any>} fields Result fields.
441
+ * @param {string[]} specific Operation-specific collection names.
442
+ * @returns {void}
443
+ */
444
+ static #resultCollections(fields, specific) {
445
+ for (const key of [...specific, 'assets', 'diagnostics']) {
446
+ if (!Array.isArray(fields[key])) {
447
+ throw WorkerResponseData.#error(
448
+ `Toolkit worker result ${key} is invalid.`
449
+ )
450
+ }
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Requires common result map fields to be plain accessor-free records.
456
+ * @param {Record<string, any>} fields Result fields.
457
+ * @param {string[]} names Field names.
458
+ * @returns {void}
459
+ */
460
+ static #plainResultRecords(fields, names) {
461
+ for (const name of names) {
462
+ WorkerResponseData.#record(
463
+ fields[name],
464
+ `Toolkit worker result ${name}`
465
+ )
466
+ }
467
+ }
468
+
469
+ /** @param {unknown} value Diagnostics candidate. @returns {void} */
470
+ static #diagnostics(value) {
471
+ WorkerRequestData.assertCloneSafe(value)
472
+ WorkerResponseData.#diagnosticRows(value, 1000)
473
+ }
474
+
475
+ /**
476
+ * Validates canonical asset records without coercion.
477
+ * @param {unknown} value Asset array.
478
+ * @returns {void}
479
+ */
480
+ static #assets(value) {
481
+ const assets = WorkerResponseData.#array(
482
+ value,
483
+ 'Toolkit worker assets',
484
+ 100_000
485
+ )
486
+ for (const asset of assets) {
487
+ const fields = WorkerResponseData.#record(
488
+ asset,
489
+ 'Toolkit worker asset'
490
+ )
491
+ WorkerResponseData.#exactKeys(
492
+ fields,
493
+ ASSET_FIELDS,
494
+ 'Toolkit worker asset'
495
+ )
496
+ for (const key of ['id', 'kind', 'name', 'mediaType']) {
497
+ WorkerResponseData.#boundedString(
498
+ fields[key],
499
+ 4096,
500
+ `asset ${key}`,
501
+ key === 'name'
502
+ )
503
+ }
504
+ if (
505
+ !Number.isSafeInteger(fields.byteLength) ||
506
+ fields.byteLength < 0
507
+ ) {
508
+ throw WorkerResponseData.#error(
509
+ 'Toolkit worker asset byteLength is invalid.'
510
+ )
511
+ }
512
+ if (fields.source !== null) {
513
+ WorkerResponseData.#record(
514
+ fields.source,
515
+ 'Toolkit worker asset source'
516
+ )
517
+ }
518
+ if (
519
+ fields.data !== null &&
520
+ typeof fields.data !== 'string' &&
521
+ WorkerResponseData.#binaryByteLength(fields.data) === null
522
+ ) {
523
+ throw WorkerResponseData.#error(
524
+ 'Toolkit worker asset data is invalid.'
525
+ )
526
+ }
527
+ const actualByteLength = WorkerResponseData.#assetByteLength(
528
+ fields.data
529
+ )
530
+ if (
531
+ actualByteLength !== null &&
532
+ fields.byteLength !== actualByteLength
533
+ ) {
534
+ throw WorkerResponseData.#error(
535
+ 'Toolkit worker asset byteLength is inconsistent.'
536
+ )
537
+ }
538
+ }
539
+ }
540
+
541
+ /**
542
+ * Validates the required canonical document-source primitives.
543
+ * @param {unknown} value Source candidate.
544
+ * @returns {void}
545
+ */
546
+ static #documentSource(value) {
547
+ const fields = WorkerResponseData.#record(
548
+ value,
549
+ 'Toolkit worker document source'
550
+ )
551
+ WorkerResponseData.#boundedString(
552
+ fields.format,
553
+ 256,
554
+ 'document source format',
555
+ false
556
+ )
557
+ WorkerResponseData.#boundedString(
558
+ fields.fileName,
559
+ 4096,
560
+ 'document source fileName',
561
+ true
562
+ )
563
+ WorkerResponseData.#boundedString(
564
+ fields.fileType,
565
+ 256,
566
+ 'document source fileType',
567
+ false
568
+ )
569
+ }
570
+
571
+ /**
572
+ * Validates the required canonical project-source primitives.
573
+ * @param {unknown} value Source candidate.
574
+ * @returns {void}
575
+ */
576
+ static #projectSource(value) {
577
+ const fields = WorkerResponseData.#record(
578
+ value,
579
+ 'Toolkit worker project source'
580
+ )
581
+ WorkerResponseData.#boundedString(
582
+ fields.format,
583
+ 256,
584
+ 'project source format',
585
+ false
586
+ )
587
+ const entryNames = WorkerResponseData.#array(
588
+ fields.entryNames,
589
+ 'Toolkit worker project source entryNames',
590
+ 4096
591
+ )
592
+ for (const entryName of entryNames) {
593
+ WorkerResponseData.#boundedString(
594
+ entryName,
595
+ 4096,
596
+ 'project source entryName',
597
+ false
598
+ )
599
+ }
600
+ }
601
+
602
+ /**
603
+ * Measures present canonical asset data exactly as UTF-8 or binary bytes.
604
+ * @param {unknown} value Asset data.
605
+ * @returns {number | null} Actual bytes, or null for absent/invalid data.
606
+ */
607
+ static #assetByteLength(value) {
608
+ if (value === null) return null
609
+ if (typeof value === 'string') {
610
+ return WorkerResponseData.#utf8ByteLength(value)
611
+ }
612
+ return WorkerResponseData.#binaryByteLength(value)
613
+ }
614
+
615
+ /**
616
+ * Reads genuine binary internal slots without trusting surface properties.
617
+ * @param {unknown} value Binary candidate.
618
+ * @returns {number | null} Intrinsic byte length.
619
+ */
620
+ static #binaryByteLength(value) {
621
+ for (const getter of [
622
+ ARRAY_BUFFER_BYTE_LENGTH_GETTER,
623
+ DATA_VIEW_BYTE_LENGTH_GETTER,
624
+ TYPED_ARRAY_BYTE_LENGTH_GETTER
625
+ ]) {
626
+ if (!getter) continue
627
+ try {
628
+ return getter.call(value)
629
+ } catch {
630
+ // Each intrinsic independently proves its corresponding brand.
631
+ }
632
+ }
633
+ return null
634
+ }
635
+
636
+ /**
637
+ * Measures a string with the replacement behavior used by UTF-8 encoders.
638
+ * @param {string} value Text value.
639
+ * @returns {number} UTF-8 byte length.
640
+ */
641
+ static #utf8ByteLength(value) {
642
+ let bytes = 0
643
+ for (let index = 0; index < value.length; index += 1) {
644
+ const first = value.charCodeAt(index)
645
+ let codePoint = first
646
+ if (first >= 0xd800 && first <= 0xdbff) {
647
+ const second = value.charCodeAt(index + 1)
648
+ if (second >= 0xdc00 && second <= 0xdfff) {
649
+ codePoint =
650
+ 0x10000 + ((first - 0xd800) << 10) + (second - 0xdc00)
651
+ index += 1
652
+ } else {
653
+ codePoint = 0xfffd
654
+ }
655
+ } else if (first >= 0xdc00 && first <= 0xdfff) {
656
+ codePoint = 0xfffd
657
+ }
658
+ if (codePoint <= 0x7f) bytes += 1
659
+ else if (codePoint <= 0x7ff) bytes += 2
660
+ else if (codePoint <= 0xffff) bytes += 3
661
+ else bytes += 4
662
+ }
663
+ return bytes
664
+ }
665
+
666
+ /**
667
+ * Validates canonical diagnostic rows.
668
+ * @param {unknown} value Diagnostics candidate.
669
+ * @param {number} maximum Maximum row count.
670
+ * @returns {void}
671
+ */
672
+ static #diagnosticRows(value, maximum) {
673
+ const diagnostics = WorkerResponseData.#array(
674
+ value,
675
+ 'Toolkit worker diagnostics',
676
+ maximum
677
+ )
678
+ for (const diagnostic of diagnostics) {
679
+ const fields = WorkerResponseData.#record(
680
+ diagnostic,
681
+ 'Toolkit worker diagnostic'
682
+ )
683
+ WorkerResponseData.#exactKeys(
684
+ fields,
685
+ DIAGNOSTIC_FIELDS,
686
+ 'Toolkit worker diagnostic'
687
+ )
688
+ WorkerResponseData.#boundedString(
689
+ fields.code,
690
+ 256,
691
+ 'diagnostic code',
692
+ false
693
+ )
694
+ WorkerResponseData.#boundedString(
695
+ fields.message,
696
+ 4096,
697
+ 'diagnostic message',
698
+ true
699
+ )
700
+ WorkerResponseData.#boundedString(
701
+ fields.source,
702
+ 4096,
703
+ 'diagnostic source',
704
+ true
705
+ )
706
+ if (!DIAGNOSTIC_SEVERITIES.has(fields.severity)) {
707
+ throw WorkerResponseData.#error(
708
+ 'Toolkit worker diagnostic severity is invalid.'
709
+ )
710
+ }
711
+ }
712
+ }
713
+
714
+ /** @param {unknown} value Cause candidate. @returns {void} */
715
+ static #cause(value) {
716
+ if (value === null) return
717
+ const fields = WorkerResponseData.#record(
718
+ value,
719
+ 'Toolkit worker error cause'
720
+ )
721
+ WorkerResponseData.#exactKeys(
722
+ fields,
723
+ ERROR_CAUSE_FIELDS,
724
+ 'Toolkit worker error cause'
725
+ )
726
+ WorkerResponseData.#boundedString(
727
+ fields.name,
728
+ 4096,
729
+ 'cause name',
730
+ false
731
+ )
732
+ WorkerResponseData.#boundedString(
733
+ fields.message,
734
+ 4096,
735
+ 'cause message',
736
+ true
737
+ )
738
+ if (fields.code !== null) {
739
+ WorkerResponseData.#boundedString(
740
+ fields.code,
741
+ 256,
742
+ 'cause code',
743
+ true
744
+ )
745
+ }
746
+ }
747
+
748
+ /**
749
+ * Requires one bounded string without coercion.
750
+ * @param {unknown} value String candidate.
751
+ * @param {number} maximum Maximum length.
752
+ * @param {string} label Field label.
753
+ * @param {boolean} allowEmpty Whether an empty string is valid.
754
+ * @returns {void}
755
+ */
756
+ static #boundedString(value, maximum, label, allowEmpty) {
757
+ if (
758
+ typeof value !== 'string' ||
759
+ value.length > maximum ||
760
+ (!allowEmpty && !value)
761
+ ) {
762
+ throw WorkerResponseData.#error(
763
+ `Toolkit worker ${label} is invalid.`
764
+ )
765
+ }
766
+ }
767
+
768
+ /**
769
+ * Requires an exact enumerable field set.
770
+ * @param {Record<string, any>} fields Record fields.
771
+ * @param {string[]} expected Expected keys.
772
+ * @param {string} label Record label.
773
+ * @returns {void}
774
+ */
775
+ static #exactKeys(fields, expected, label) {
776
+ const keys = Object.keys(fields)
777
+ if (
778
+ keys.length !== expected.length ||
779
+ expected.some((key) => !Object.hasOwn(fields, key))
780
+ ) {
781
+ throw WorkerResponseData.#error(`${label} fields are invalid.`)
782
+ }
783
+ }
784
+
785
+ /**
786
+ * Requires an allowed key set and all required keys.
787
+ * @param {Record<string, any>} fields Record fields.
788
+ * @param {Set<string>} allowed Allowed keys.
789
+ * @param {Set<string>} required Required keys.
790
+ * @param {string} label Record label.
791
+ * @returns {void}
792
+ */
793
+ static #allowedKeys(fields, allowed, required, label) {
794
+ const keys = Object.keys(fields)
795
+ if (
796
+ keys.some((key) => !allowed.has(key)) ||
797
+ [...required].some((key) => !Object.hasOwn(fields, key))
798
+ ) {
799
+ throw WorkerResponseData.#error(`${label} fields are invalid.`)
800
+ }
801
+ }
802
+
803
+ /**
804
+ * Reads one accessor-free plain record.
805
+ * @param {unknown} value Record candidate.
806
+ * @param {string} label Record label.
807
+ * @returns {Record<string, any>} Null-prototype data map.
808
+ */
809
+ static #record(value, label) {
810
+ RuntimeProxyBoundary.assert(value, label)
811
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
812
+ throw WorkerResponseData.#error(`${label} must be a plain object.`)
813
+ }
814
+ let prototype
815
+ let descriptors
816
+ try {
817
+ prototype = Object.getPrototypeOf(value)
818
+ descriptors = Object.getOwnPropertyDescriptors(value)
819
+ } catch (error) {
820
+ throw WorkerResponseData.#error(
821
+ `${label} could not be inspected safely.`,
822
+ error
823
+ )
824
+ }
825
+ if (prototype !== Object.prototype && prototype !== null) {
826
+ throw WorkerResponseData.#error(`${label} must be a plain object.`)
827
+ }
828
+ const result = Object.create(null)
829
+ for (const key of Reflect.ownKeys(descriptors)) {
830
+ const descriptor = descriptors[key]
831
+ if (
832
+ typeof key !== 'string' ||
833
+ descriptor.get ||
834
+ descriptor.set ||
835
+ descriptor.enumerable !== true
836
+ ) {
837
+ throw WorkerResponseData.#error(
838
+ `${label} may contain only enumerable data properties.`
839
+ )
840
+ }
841
+ Object.defineProperty(result, key, {
842
+ configurable: true,
843
+ enumerable: true,
844
+ value: descriptor.value,
845
+ writable: true
846
+ })
847
+ }
848
+ return result
849
+ }
850
+
851
+ /**
852
+ * Reads one bounded dense plain array.
853
+ * @param {unknown} value Array candidate.
854
+ * @param {string} label Array label.
855
+ * @param {number} maximum Maximum length.
856
+ * @returns {any[]} Array values.
857
+ */
858
+ static #array(value, label, maximum) {
859
+ RuntimeProxyBoundary.assert(value, label)
860
+ if (!Array.isArray(value)) {
861
+ throw WorkerResponseData.#error(`${label} must be an array.`)
862
+ }
863
+ const length = Object.getOwnPropertyDescriptor(value, 'length')?.value
864
+ const keys = Reflect.ownKeys(value)
865
+ if (
866
+ Object.getPrototypeOf(value) !== Array.prototype ||
867
+ !Number.isSafeInteger(length) ||
868
+ length < 0 ||
869
+ length > maximum ||
870
+ keys.length !== length + 1
871
+ ) {
872
+ throw WorkerResponseData.#error(
873
+ `${label} must be a bounded dense plain array.`
874
+ )
875
+ }
876
+ const result = new Array(length)
877
+ for (let index = 0; index < length; index += 1) {
878
+ const descriptor = Object.getOwnPropertyDescriptor(
879
+ value,
880
+ String(index)
881
+ )
882
+ if (!descriptor || !Object.hasOwn(descriptor, 'value')) {
883
+ throw WorkerResponseData.#error(
884
+ `${label} must contain data properties.`
885
+ )
886
+ }
887
+ result[index] = descriptor.value
888
+ }
889
+ return result
890
+ }
891
+
892
+ /**
893
+ * Creates one typed protocol error.
894
+ * @param {string} message Failure message.
895
+ * @param {unknown} [cause] Failure cause.
896
+ * @returns {ToolkitError} Protocol error.
897
+ */
898
+ static #error(message, cause = null) {
899
+ return new ToolkitError(message, {
900
+ code: 'ERR_WORKER_MESSAGE',
901
+ category: 'runtime',
902
+ cause
903
+ })
904
+ }
905
+ }