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,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
|
+
}
|
package/src/testing.mjs
ADDED