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