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,705 @@
1
+ import { ToolkitContractFixtures } from './ToolkitContractFixtures.mjs'
2
+ import { ToolkitLoopbackWorker } from './ToolkitLoopbackWorker.mjs'
3
+
4
+ const CAPABILITY_STATUSES = new Set([
5
+ 'native',
6
+ 'shared',
7
+ 'derived',
8
+ 'unavailable'
9
+ ])
10
+ const REQUIRED_CAPABILITY_IDS = [
11
+ 'bom.build',
12
+ 'interaction.pcb',
13
+ 'manufacturing.export',
14
+ 'parse.document',
15
+ 'project.load',
16
+ 'query.document',
17
+ 'render.pcb',
18
+ 'render.schematic',
19
+ 'scene3d.build',
20
+ 'scene3d.prepare',
21
+ 'simulation.spice',
22
+ 'validation.document',
23
+ 'worker.load-project',
24
+ 'worker.parse'
25
+ ]
26
+ const WORKER_OPERATIONS = { parse: 0, loadProject: 0 }
27
+
28
+ /**
29
+ * Executes the shared high-level toolkit contract without private imports.
30
+ * @param {Record<string, any>} toolkit Package-root namespace.
31
+ * @param {{ fixtures?: Record<string, any> }} [options] Contract options.
32
+ * @returns {Promise<{ schema: string, format: string, checks: object[], failures: object[] }>} Contract report.
33
+ */
34
+ export async function runToolkitContract(toolkit, options = {}) {
35
+ const fixtures = options.fixtures || ToolkitContractFixtures.circuitJson()
36
+ const state = {
37
+ checks: [],
38
+ failures: [],
39
+ toolkit,
40
+ fixtures,
41
+ document: null,
42
+ project: null,
43
+ context: null,
44
+ capabilities: new Map(),
45
+ workerOperations: WORKER_OPERATIONS
46
+ }
47
+
48
+ for (const name of ToolkitContractFixtures.canonicalClassNames) {
49
+ await check(state, `export.${name}`, () => {
50
+ requireCondition(
51
+ typeof toolkit?.[name] === 'function',
52
+ `Missing canonical class: ${name}.`
53
+ )
54
+ })
55
+ }
56
+ if (
57
+ state.failures.some((failure) => failure.code === 'ERR_CONTRACT_EXPORT')
58
+ ) {
59
+ return report(state)
60
+ }
61
+
62
+ await withLoopbackWorker(state, async () => {
63
+ await parserChecks(state)
64
+ await projectChecks(state)
65
+ })
66
+ await capabilityChecks(state)
67
+ await documentServiceChecks(state)
68
+ await errorChecks(state)
69
+ return report(state)
70
+ }
71
+
72
+ /** @param {Record<string, any>} state Contract state. @returns {Promise<void>} */
73
+ async function parserChecks(state) {
74
+ const { Parser } = state.toolkit
75
+ const input = state.fixtures.parserInput
76
+ await check(state, 'parser.supports', () => {
77
+ requireCondition(
78
+ Parser.supports(input) === true,
79
+ 'Parser did not support the fixture.'
80
+ )
81
+ })
82
+ await check(state, 'parser.parse', () => {
83
+ state.document = Parser.parse(input)
84
+ requireSchema(state.document, 'ecad-toolkit.document.v1')
85
+ requireExactFields(state.document, [
86
+ 'schema',
87
+ 'id',
88
+ 'modelSchema',
89
+ 'model',
90
+ 'source',
91
+ 'extensions',
92
+ 'assets',
93
+ 'diagnostics',
94
+ 'statistics'
95
+ ])
96
+ requireCondition(
97
+ state.document.source?.format === state.fixtures.format,
98
+ 'Document source format differs from the fixture.'
99
+ )
100
+ })
101
+ await check(state, 'parser.tryParse', () => {
102
+ const result = Parser.tryParse(input)
103
+ requireCondition(
104
+ result?.ok === true,
105
+ 'tryParse did not return success.'
106
+ )
107
+ requireSchema(result.value, 'ecad-toolkit.document.v1')
108
+ })
109
+ await check(state, 'parser.failure', () => {
110
+ const result = Parser.tryParse(state.fixtures.unsupportedInput)
111
+ requireCondition(
112
+ result?.ok === false,
113
+ 'Unsupported input did not fail.'
114
+ )
115
+ requireCondition(
116
+ result.error?.name === 'ToolkitError',
117
+ 'Parser failure is not a ToolkitError.'
118
+ )
119
+ requireCondition(
120
+ Array.isArray(result.diagnostics) && result.diagnostics.length > 0,
121
+ 'Parser failure did not retain a diagnostic.'
122
+ )
123
+ })
124
+ await check(state, 'parser.async-progress', async () => {
125
+ const progress = []
126
+ const result = await Parser.parseAsync(input, {
127
+ worker: false,
128
+ onProgress: (row) => progress.push(row.stage)
129
+ })
130
+ requireSchema(result, 'ecad-toolkit.document.v1')
131
+ requireEquivalentResult(
132
+ result,
133
+ state.document,
134
+ 'Direct and async parser results differ.'
135
+ )
136
+ requireCondition(
137
+ progress.at(-1) === 'complete',
138
+ 'Async parser progress did not complete.'
139
+ )
140
+ })
141
+ await check(state, 'parser.async-worker', async () => {
142
+ const before = state.workerOperations.parse
143
+ const result = await Parser.parseAsync(input, { worker: true })
144
+ requireSchema(result, 'ecad-toolkit.document.v1')
145
+ requireEquivalentResult(
146
+ result,
147
+ state.document,
148
+ 'Worker and direct parser results differ.'
149
+ )
150
+ requireCondition(
151
+ state.workerOperations.parse === before + 1,
152
+ 'Parser worker request did not cross the protocol transport.'
153
+ )
154
+ })
155
+ }
156
+
157
+ /** @param {Record<string, any>} state Contract state. @returns {Promise<void>} */
158
+ async function projectChecks(state) {
159
+ const { ProjectLoader } = state.toolkit
160
+ const entries = state.fixtures.projectEntries
161
+ await check(state, 'project.supports', () => {
162
+ requireCondition(
163
+ ProjectLoader.supports(entries) === true,
164
+ 'ProjectLoader did not support the fixture.'
165
+ )
166
+ })
167
+ await check(state, 'project.load', () => {
168
+ state.project = ProjectLoader.load(entries)
169
+ requireSchema(state.project, 'ecad-toolkit.project.v1')
170
+ requireExactFields(state.project, [
171
+ 'schema',
172
+ 'id',
173
+ 'source',
174
+ 'documents',
175
+ 'project',
176
+ 'extensions',
177
+ 'assets',
178
+ 'diagnostics',
179
+ 'statistics'
180
+ ])
181
+ for (const field of [
182
+ 'entryCount',
183
+ 'candidateCount',
184
+ 'documentCount',
185
+ 'failureCount',
186
+ 'totalBytes'
187
+ ]) {
188
+ requireCondition(
189
+ Number.isFinite(state.project.statistics?.[field]),
190
+ `Project statistics omit common field: ${field}.`
191
+ )
192
+ }
193
+ })
194
+ await check(state, 'project.tryLoad-failure', () => {
195
+ const result = ProjectLoader.tryLoad([])
196
+ requireCondition(result?.ok === false, 'Empty project did not fail.')
197
+ requireCondition(
198
+ result.error?.name === 'ToolkitError',
199
+ 'Project failure is not a ToolkitError.'
200
+ )
201
+ requireCondition(
202
+ Array.isArray(result.diagnostics) && result.diagnostics.length > 0,
203
+ 'Project failure did not retain a diagnostic.'
204
+ )
205
+ })
206
+ await check(state, 'project.loadAsync', async () => {
207
+ const project = await ProjectLoader.loadAsync(entries, {
208
+ worker: false
209
+ })
210
+ requireSchema(project, 'ecad-toolkit.project.v1')
211
+ requireEquivalentResult(
212
+ project,
213
+ state.project,
214
+ 'Direct and async project results differ.'
215
+ )
216
+ })
217
+ await check(state, 'project.loadAsync-worker', async () => {
218
+ const before = state.workerOperations.loadProject
219
+ const project = await ProjectLoader.loadAsync(entries, {
220
+ worker: true
221
+ })
222
+ requireSchema(project, 'ecad-toolkit.project.v1')
223
+ requireEquivalentResult(
224
+ project,
225
+ state.project,
226
+ 'Worker and direct project results differ.'
227
+ )
228
+ requireCondition(
229
+ state.workerOperations.loadProject === before + 1,
230
+ 'Project worker request did not cross the protocol transport.'
231
+ )
232
+ })
233
+ }
234
+
235
+ /**
236
+ * Runs parser and project checks with an actual cloned protocol transport.
237
+ * @param {Record<string, any>} state Contract state.
238
+ * @param {() => Promise<void>} operation Contract checks.
239
+ * @returns {Promise<void>}
240
+ */
241
+ async function withLoopbackWorker(state, operation) {
242
+ const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'Worker')
243
+ const Worker = ToolkitLoopbackWorker.constructorFor(
244
+ state.toolkit,
245
+ state.workerOperations
246
+ )
247
+ Object.defineProperty(globalThis, 'Worker', {
248
+ configurable: true,
249
+ value: Worker,
250
+ writable: true
251
+ })
252
+ try {
253
+ await operation()
254
+ } finally {
255
+ if (descriptor) {
256
+ Object.defineProperty(globalThis, 'Worker', descriptor)
257
+ } else {
258
+ delete globalThis.Worker
259
+ }
260
+ }
261
+ }
262
+
263
+ /** @param {Record<string, any>} state Contract state. @returns {Promise<void>} */
264
+ async function documentServiceChecks(state) {
265
+ if (!state.document) {
266
+ addFailure(
267
+ state,
268
+ 'services.document',
269
+ 'ERR_CONTRACT_DEPENDENCY',
270
+ 'Document services require a successful parser result.'
271
+ )
272
+ return
273
+ }
274
+ const toolkit = state.toolkit
275
+ await check(state, 'context.reuse', () => {
276
+ state.context = toolkit.CircuitJsonDocumentContext.prepare(
277
+ state.document
278
+ )
279
+ requireCondition(
280
+ toolkit.CircuitJsonDocumentContext.prepare(state.context) ===
281
+ state.context,
282
+ 'Prepared context was not reused.'
283
+ )
284
+ })
285
+ await check(state, 'renderer.pcb', () => {
286
+ return capabilityOperation(
287
+ state,
288
+ 'render.pcb',
289
+ () =>
290
+ toolkit.PcbSvgRenderer.render(
291
+ state.context || state.document,
292
+ state.fixtures.renderOptions.pcb
293
+ ),
294
+ requireSvg
295
+ )
296
+ })
297
+ await check(state, 'renderer.pcb-bottom', () => {
298
+ return capabilityOperation(
299
+ state,
300
+ 'render.pcb',
301
+ () =>
302
+ toolkit.PcbSvgRenderer.render(
303
+ state.context || state.document,
304
+ state.fixtures.renderOptions.pcbBottom
305
+ ),
306
+ requireSvg
307
+ )
308
+ })
309
+ await check(state, 'renderer.schematic', () => {
310
+ return capabilityOperation(
311
+ state,
312
+ 'render.schematic',
313
+ () =>
314
+ toolkit.SchematicSvgRenderer.render(
315
+ state.context || state.document,
316
+ state.fixtures.renderOptions.schematic
317
+ ),
318
+ requireSvg
319
+ )
320
+ })
321
+ await check(state, 'renderer.bom', () => {
322
+ return capabilityOperation(
323
+ state,
324
+ 'bom.build',
325
+ () =>
326
+ toolkit.BomTableRenderer.render(
327
+ state.context || state.document
328
+ ),
329
+ (html) =>
330
+ requireCondition(
331
+ typeof html === 'string' &&
332
+ (html.includes('<table') || html.includes('bom-empty')),
333
+ 'BOM renderer did not return canonical HTML.'
334
+ )
335
+ )
336
+ })
337
+ await check(state, 'interaction.pcb', () => {
338
+ return capabilityOperation(
339
+ state,
340
+ 'interaction.pcb',
341
+ () =>
342
+ toolkit.PcbInteractionIndex.create(
343
+ state.context || state.document
344
+ ).hitTest({ x: 0, y: 0 }),
345
+ (hits) =>
346
+ requireCondition(
347
+ Array.isArray(hits),
348
+ 'PCB hit testing did not return an array.'
349
+ )
350
+ )
351
+ })
352
+ await check(state, 'query.document', () => {
353
+ return capabilityOperation(
354
+ state,
355
+ 'query.document',
356
+ () =>
357
+ toolkit.QueryService.create(
358
+ state.context || state.document
359
+ ).query({ select: 'components' }),
360
+ (query) => {
361
+ requireSchema(query, 'ecad-toolkit.query.v1')
362
+ requireExactFields(query, [
363
+ 'schema',
364
+ 'items',
365
+ 'diagnostics',
366
+ 'statistics'
367
+ ])
368
+ }
369
+ )
370
+ })
371
+ await check(state, 'manufacturing.inspect', () => {
372
+ const inspection = toolkit.ManufacturingService.inspect(
373
+ state.context || state.document
374
+ )
375
+ requireSchema(inspection, 'ecad-toolkit.manufacturing.v1')
376
+ requireExactFields(inspection, [
377
+ 'schema',
378
+ 'placements',
379
+ 'fabricationNotes',
380
+ 'exports',
381
+ 'diagnostics',
382
+ 'statistics'
383
+ ])
384
+ })
385
+ await check(state, 'manufacturing.unavailable', () => {
386
+ requireUnavailable(() =>
387
+ toolkit.ManufacturingService.export(
388
+ state.context || state.document,
389
+ { id: 'contract-unavailable-export' }
390
+ )
391
+ )
392
+ })
393
+ await check(state, 'simulation.build', () => {
394
+ return capabilityOperation(
395
+ state,
396
+ 'simulation.spice',
397
+ () =>
398
+ toolkit.SimulationService.build(
399
+ state.context || state.document
400
+ ),
401
+ (simulation) => {
402
+ requireSchema(simulation, 'ecad-toolkit.simulation.v1')
403
+ requireExactFields(simulation, [
404
+ 'schema',
405
+ 'circuits',
406
+ 'analyses',
407
+ 'models',
408
+ 'diagnostics',
409
+ 'statistics'
410
+ ])
411
+ }
412
+ )
413
+ })
414
+ await check(state, 'scene3d.build', () => {
415
+ return capabilityOperation(
416
+ state,
417
+ 'scene3d.build',
418
+ () =>
419
+ toolkit.PcbScene3dBuilder.build(
420
+ state.context || state.document,
421
+ { fidelity: 'canonical' }
422
+ ),
423
+ (scene) => requireSchema(scene, 'ecad-toolkit.scene3d.v1')
424
+ )
425
+ })
426
+ await check(state, 'scene3d.prepare', async () => {
427
+ await capabilityOperation(
428
+ state,
429
+ 'scene3d.prepare',
430
+ () =>
431
+ toolkit.PcbScene3dPreparator.prepare(
432
+ state.context || state.document,
433
+ { fidelity: 'canonical' }
434
+ ),
435
+ (scene) => requireSchema(scene, 'ecad-toolkit.scene3d.v1')
436
+ )
437
+ })
438
+ }
439
+
440
+ /** @param {Record<string, any>} state Contract state. @returns {Promise<void>} */
441
+ async function capabilityChecks(state) {
442
+ await check(state, 'capabilities.inventory', () => {
443
+ const rows = state.toolkit.ToolkitCapabilities.inventory()
444
+ requireCondition(
445
+ Array.isArray(rows) && rows.length > 0,
446
+ 'Capability inventory is empty.'
447
+ )
448
+ for (const row of rows) {
449
+ requireExactFields(row, [
450
+ 'id',
451
+ 'category',
452
+ 'operation',
453
+ 'status',
454
+ 'entrypoint',
455
+ 'summary',
456
+ 'reason',
457
+ 'tested',
458
+ 'documented'
459
+ ])
460
+ requireCondition(
461
+ typeof row.id === 'string' &&
462
+ typeof row.category === 'string' &&
463
+ typeof row.operation === 'string' &&
464
+ row.id === `${row.category}.${row.operation}` &&
465
+ CAPABILITY_STATUSES.has(row.status) &&
466
+ typeof row.entrypoint === 'string' &&
467
+ typeof row.summary === 'string' &&
468
+ typeof row.reason === 'string' &&
469
+ typeof row.tested === 'boolean' &&
470
+ typeof row.documented === 'boolean' &&
471
+ row.tested === true &&
472
+ row.documented === true &&
473
+ (row.status !== 'unavailable' ||
474
+ row.reason.trim().length > 0),
475
+ 'Capability rows are malformed.'
476
+ )
477
+ }
478
+ const ids = new Set(rows.map((row) => row.id))
479
+ requireCondition(ids.size === rows.length, 'Capability ids repeat.')
480
+ requireCondition(
481
+ rows
482
+ .map((row) => row.id)
483
+ .every((id, index, all) => index === 0 || all[index - 1] < id),
484
+ 'Capability ids are not in stable sorted order.'
485
+ )
486
+ requireCondition(
487
+ REQUIRED_CAPABILITY_IDS.every((id) => ids.has(id)),
488
+ 'Capability inventory omits a common behavior family.'
489
+ )
490
+ state.capabilities = new Map(rows.map((row) => [row.id, row]))
491
+ for (const id of ['parse.document', 'project.load']) {
492
+ requireCondition(
493
+ state.capabilities.get(id).status !== 'unavailable',
494
+ `${id} is unavailable for its own conformance fixture.`
495
+ )
496
+ }
497
+ })
498
+ }
499
+
500
+ /**
501
+ * Executes an available operation or verifies its typed unavailable failure.
502
+ * @param {Record<string, any>} state Contract state.
503
+ * @param {string} capabilityId Required capability id.
504
+ * @param {() => any | Promise<any>} operation Operation under test.
505
+ * @param {(value: any) => void | Promise<void>} validate Available-result validator.
506
+ * @returns {Promise<void>}
507
+ */
508
+ async function capabilityOperation(state, capabilityId, operation, validate) {
509
+ const capability = state.capabilities.get(capabilityId)
510
+ requireCondition(
511
+ Boolean(capability),
512
+ `Capability inventory omits ${capabilityId}.`
513
+ )
514
+ if (capability.status === 'unavailable') {
515
+ await requireUnavailableAsync(operation)
516
+ return
517
+ }
518
+ await validate(await operation())
519
+ }
520
+
521
+ /** @param {Record<string, any>} state Contract state. @returns {Promise<void>} */
522
+ async function errorChecks(state) {
523
+ await check(state, 'error.serialization', () => {
524
+ const error = new state.toolkit.ToolkitError('Contract failure.', {
525
+ code: 'ERR_CONTRACT_PROBE',
526
+ category: 'validation',
527
+ format: state.fixtures.format
528
+ })
529
+ const serialized = error.toJSON()
530
+ requireCondition(
531
+ error.name === 'ToolkitError' &&
532
+ serialized.code === 'ERR_CONTRACT_PROBE' &&
533
+ serialized.category === 'validation',
534
+ 'ToolkitError did not preserve canonical fields.'
535
+ )
536
+ })
537
+ }
538
+
539
+ /**
540
+ * Runs one check and appends a clone-safe failure instead of throwing.
541
+ * @param {Record<string, any>} state Contract state.
542
+ * @param {string} id Check id.
543
+ * @param {() => any | Promise<any>} operation Check body.
544
+ * @returns {Promise<void>}
545
+ */
546
+ async function check(state, id, operation) {
547
+ try {
548
+ await operation()
549
+ state.checks.push({ id, status: 'passed' })
550
+ } catch (error) {
551
+ const exportFailure = id.startsWith('export.')
552
+ addFailure(
553
+ state,
554
+ id,
555
+ exportFailure ? 'ERR_CONTRACT_EXPORT' : 'ERR_CONTRACT_EXECUTION',
556
+ safeErrorMessage(error)
557
+ )
558
+ }
559
+ }
560
+
561
+ /**
562
+ * Adds one deterministic failure and matching failed check.
563
+ * @param {Record<string, any>} state Contract state.
564
+ * @param {string} id Check id.
565
+ * @param {string} code Failure code.
566
+ * @param {string} message Failure message.
567
+ * @returns {void}
568
+ */
569
+ function addFailure(state, id, code, message) {
570
+ state.checks.push({ id, status: 'failed' })
571
+ state.failures.push({ code, check: id, message })
572
+ }
573
+
574
+ /** @param {unknown} value Value. @param {string} message Message. @returns {void} */
575
+ function requireCondition(value, message) {
576
+ if (!value) throw new Error(message)
577
+ }
578
+
579
+ /** @param {unknown} value Value. @param {string} schema Schema. @returns {void} */
580
+ function requireSchema(value, schema) {
581
+ requireCondition(value?.schema === schema, `Expected schema ${schema}.`)
582
+ }
583
+
584
+ /**
585
+ * Requires the exact enumerable top-level result shape.
586
+ * @param {unknown} value Result candidate.
587
+ * @param {string[]} fields Expected own enumerable fields.
588
+ * @returns {void}
589
+ */
590
+ function requireExactFields(value, fields) {
591
+ requireCondition(
592
+ value !== null && typeof value === 'object',
593
+ 'Expected a result object.'
594
+ )
595
+ const actual = Object.keys(value).sort()
596
+ const expected = [...fields].sort()
597
+ requireCondition(
598
+ actual.length === expected.length &&
599
+ actual.every((field, index) => field === expected[index]),
600
+ `Expected result fields: ${expected.join(', ')}.`
601
+ )
602
+ }
603
+
604
+ /**
605
+ * Compares canonical clone-safe results by their serialized value.
606
+ * @param {unknown} actual Actual result.
607
+ * @param {unknown} expected Expected result.
608
+ * @param {string} message Failure message.
609
+ * @returns {void}
610
+ */
611
+ function requireEquivalentResult(actual, expected, message) {
612
+ requireCondition(
613
+ JSON.stringify(actual) === JSON.stringify(expected),
614
+ message
615
+ )
616
+ }
617
+
618
+ /** @param {unknown} value SVG candidate. @returns {void} */
619
+ function requireSvg(value) {
620
+ requireCondition(
621
+ typeof value === 'string' && value.includes('<svg'),
622
+ 'Renderer did not return SVG text.'
623
+ )
624
+ }
625
+
626
+ /** @param {() => unknown} operation Unavailable operation. @returns {void} */
627
+ function requireUnavailable(operation) {
628
+ try {
629
+ operation()
630
+ } catch (error) {
631
+ requireCondition(
632
+ safeErrorCode(error) === 'ERR_CAPABILITY_UNAVAILABLE',
633
+ 'Unavailable operation used the wrong error code.'
634
+ )
635
+ return
636
+ }
637
+ throw new Error('Unavailable operation returned successfully.')
638
+ }
639
+
640
+ /**
641
+ * Requires one synchronous or asynchronous unavailable operation failure.
642
+ * @param {() => unknown | Promise<unknown>} operation Unavailable operation.
643
+ * @returns {Promise<void>}
644
+ */
645
+ async function requireUnavailableAsync(operation) {
646
+ try {
647
+ await operation()
648
+ } catch (error) {
649
+ requireCondition(
650
+ safeErrorCode(error) === 'ERR_CAPABILITY_UNAVAILABLE',
651
+ 'Unavailable operation used the wrong error code.'
652
+ )
653
+ return
654
+ }
655
+ throw new Error('Unavailable operation returned successfully.')
656
+ }
657
+
658
+ /** @param {unknown} error Error candidate. @returns {string} Safe code. */
659
+ function safeErrorCode(error) {
660
+ return safeDataField(error, 'code', '')
661
+ }
662
+
663
+ /** @param {unknown} error Error candidate. @returns {string} Safe message. */
664
+ function safeErrorMessage(error) {
665
+ return safeDataField(error, 'message', 'Toolkit contract check failed.')
666
+ }
667
+
668
+ /**
669
+ * Reads one bounded primitive error field without invoking accessors.
670
+ * @param {unknown} value Owner candidate.
671
+ * @param {string} key Field name.
672
+ * @param {string} fallback Fallback text.
673
+ * @returns {string} Safe field text.
674
+ */
675
+ function safeDataField(value, key, fallback) {
676
+ if (!value || !['object', 'function'].includes(typeof value)) {
677
+ return typeof value === 'string' ? value.slice(0, 1000) : fallback
678
+ }
679
+ let owner = value
680
+ for (let depth = 0; owner && depth < 16; depth += 1) {
681
+ let descriptor
682
+ try {
683
+ descriptor = Object.getOwnPropertyDescriptor(owner, key)
684
+ owner = Object.getPrototypeOf(owner)
685
+ } catch {
686
+ return fallback
687
+ }
688
+ if (!descriptor) continue
689
+ const field = Object.hasOwn(descriptor, 'value')
690
+ ? descriptor.value
691
+ : undefined
692
+ return typeof field === 'string' ? field.slice(0, 1000) : fallback
693
+ }
694
+ return fallback
695
+ }
696
+
697
+ /** @param {Record<string, any>} state Contract state. @returns {Record<string, any>} Report. */
698
+ function report(state) {
699
+ return {
700
+ schema: 'ecad-toolkit.contract-report.v1',
701
+ format: String(state.fixtures?.format || ''),
702
+ checks: state.checks,
703
+ failures: state.failures
704
+ }
705
+ }
@@ -0,0 +1,3 @@
1
+ export { ToolkitContractFixtures } from './testing/ToolkitContractFixtures.mjs'
2
+ export { ToolkitLoopbackWorker } from './testing/ToolkitLoopbackWorker.mjs'
3
+ export { runToolkitContract } from './testing/runToolkitContract.mjs'