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
package/spec/library-scope.md
CHANGED
|
@@ -1,28 +1,35 @@
|
|
|
1
|
-
# Library
|
|
1
|
+
# Library scope
|
|
2
2
|
|
|
3
|
-
`circuitjson-toolkit` owns
|
|
4
|
-
CircuitJSON
|
|
3
|
+
`circuitjson-toolkit` owns browser- and Node-compatible common operations over
|
|
4
|
+
immutable CircuitJSON.
|
|
5
5
|
|
|
6
6
|
## Included
|
|
7
7
|
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
8
|
+
- Canonical parser and project result envelopes
|
|
9
|
+
- CircuitJSON validation proofs and request-scoped prepared contexts
|
|
10
|
+
- Element, relation, connectivity, and PCB spatial indexes
|
|
11
|
+
- Deterministic PCB, schematic, and BOM rendering
|
|
12
|
+
- PCB hit testing, picking, selection, snapping, layers, and diagnostics
|
|
13
|
+
- Repeated component/net queries and connectivity traversal
|
|
14
|
+
- Manufacturing inspection and explicit data exports
|
|
15
|
+
- Simulation definition/export and explicitly injected engine execution
|
|
16
|
+
- Data-only PCB 3D scene building and explicit asset preparation
|
|
17
|
+
- Typed errors, diagnostics, progress, capabilities, and worker protocol
|
|
18
|
+
- Packed downstream conformance fixtures and checks
|
|
19
|
+
- Exactly 37 source-neutral compatibility extensions for previous CircuitJSON
|
|
20
|
+
APIs, classified as shared or derived across the toolkit family
|
|
21
|
+
- Descriptor-safe asset measurement/preparation and project-wide payload limit
|
|
22
|
+
accounting
|
|
18
23
|
|
|
19
24
|
## Excluded
|
|
20
25
|
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
+
- Gerber, Altium, KiCad, or other native ECAD decoders
|
|
27
|
+
- Source-format-specific fidelity predicates and native report generation
|
|
28
|
+
- Three.js or other runtime scene rendering
|
|
29
|
+
- Browser UI state, controls, downloads, or event orchestration
|
|
30
|
+
- Implicit filesystem, process, network, or asset-store access
|
|
31
|
+
- Customer/vendor fixtures or example-specific parsing behavior
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
Native parser behavior belongs in its source toolkit. Runtime visualization
|
|
34
|
+
belongs in `pcb-scene3d-viewer` or another host. Common behavior meaningful for
|
|
35
|
+
CircuitJSON belongs here and is exposed consistently by every source toolkit.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ToolkitCapabilities } from './core/ToolkitCapabilities.mjs'
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { ToolkitError } from './contracts/ToolkitError.mjs'
|
|
2
|
+
|
|
3
|
+
const DRIVE_PATH = /^[A-Za-z]:/u
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Normalizes untrusted archive entry names without touching the host filesystem.
|
|
7
|
+
*/
|
|
8
|
+
export class ArchiveEntryPath {
|
|
9
|
+
/**
|
|
10
|
+
* Normalizes one entry name to a safe relative POSIX path.
|
|
11
|
+
* @param {unknown} name Archive entry name.
|
|
12
|
+
* @returns {string} Safe normalized entry name.
|
|
13
|
+
*/
|
|
14
|
+
static normalize(name) {
|
|
15
|
+
if (typeof name !== 'string' || !name || name.includes('\0')) {
|
|
16
|
+
throw ArchiveEntryPath.#pathError(name)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const candidate = name.replaceAll('\\', '/')
|
|
20
|
+
if (candidate.startsWith('/') || DRIVE_PATH.test(candidate)) {
|
|
21
|
+
throw ArchiveEntryPath.#pathError(name)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const segments = []
|
|
25
|
+
for (const segment of candidate.split('/')) {
|
|
26
|
+
if (!segment || segment === '.') continue
|
|
27
|
+
if (segment === '..') {
|
|
28
|
+
if (!segments.length) {
|
|
29
|
+
throw ArchiveEntryPath.#pathError(name)
|
|
30
|
+
}
|
|
31
|
+
segments.pop()
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
segments.push(segment)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!segments.length) throw ArchiveEntryPath.#pathError(name)
|
|
38
|
+
return segments.join('/')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Normalizes entry names and rejects collisions after normalization.
|
|
43
|
+
* @param {unknown} names Archive entry names.
|
|
44
|
+
* @returns {string[]} Normalized names in caller order.
|
|
45
|
+
*/
|
|
46
|
+
static unique(names) {
|
|
47
|
+
if (!Array.isArray(names)) {
|
|
48
|
+
throw new ToolkitError('Archive entry names must be an array.', {
|
|
49
|
+
code: 'ERR_ARCHIVE_PATH',
|
|
50
|
+
category: 'validation',
|
|
51
|
+
format: 'archive'
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const normalized = []
|
|
56
|
+
const seen = new Set()
|
|
57
|
+
for (const name of names) {
|
|
58
|
+
const entryName = ArchiveEntryPath.normalize(name)
|
|
59
|
+
if (seen.has(entryName)) {
|
|
60
|
+
throw new ToolkitError(
|
|
61
|
+
`Duplicate normalized archive entry: ${entryName}.`,
|
|
62
|
+
{
|
|
63
|
+
code: 'ERR_ARCHIVE_DUPLICATE_ENTRY',
|
|
64
|
+
category: 'validation',
|
|
65
|
+
format: 'archive',
|
|
66
|
+
source: entryName,
|
|
67
|
+
details: { entryName }
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
seen.add(entryName)
|
|
72
|
+
normalized.push(entryName)
|
|
73
|
+
}
|
|
74
|
+
return normalized
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Creates one unsafe-entry error without reflecting NUL data into messages.
|
|
79
|
+
* @param {unknown} name Rejected entry name.
|
|
80
|
+
* @returns {ToolkitError} Typed path error.
|
|
81
|
+
*/
|
|
82
|
+
static #pathError(name) {
|
|
83
|
+
const source =
|
|
84
|
+
typeof name === 'string' && !name.includes('\0') ? name : ''
|
|
85
|
+
return new ToolkitError('Archive entry path is unsafe.', {
|
|
86
|
+
code: 'ERR_ARCHIVE_PATH',
|
|
87
|
+
category: 'validation',
|
|
88
|
+
format: 'archive',
|
|
89
|
+
source,
|
|
90
|
+
details: { reason: 'unsafe-relative-path' }
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ArchiveLimitsValidator } from './ArchiveLimitsValidator.mjs'
|
|
2
|
+
|
|
3
|
+
const DEFAULTS = Object.freeze({
|
|
4
|
+
maxEntries: 4096,
|
|
5
|
+
maxEntryBytes: 536870912,
|
|
6
|
+
maxTotalBytes: 2147483648,
|
|
7
|
+
maxCompressionRatio: 1000,
|
|
8
|
+
maxArchiveDepth: 1
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Publishes the shared non-disableable archive safety ceilings.
|
|
13
|
+
*/
|
|
14
|
+
export class ArchiveLimits {
|
|
15
|
+
/**
|
|
16
|
+
* Returns the immutable default ceilings.
|
|
17
|
+
* @returns {Readonly<Record<string, number>>} Default archive limits.
|
|
18
|
+
*/
|
|
19
|
+
static get defaults() {
|
|
20
|
+
return DEFAULTS
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Applies caller overrides that may only tighten the hard ceilings.
|
|
25
|
+
* @param {unknown} [overrides] Caller limit overrides.
|
|
26
|
+
* @returns {Readonly<Record<string, number>>} Normalized archive limits.
|
|
27
|
+
*/
|
|
28
|
+
static normalize(overrides = {}) {
|
|
29
|
+
return ArchiveLimitsValidator.normalize(overrides, DEFAULTS)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { ToolkitError } from './contracts/ToolkitError.mjs'
|
|
2
|
+
|
|
3
|
+
const INTEGER_LIMITS = new Set([
|
|
4
|
+
'maxEntries',
|
|
5
|
+
'maxEntryBytes',
|
|
6
|
+
'maxTotalBytes',
|
|
7
|
+
'maxArchiveDepth'
|
|
8
|
+
])
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validates caller-controlled archive safety limits against hard maxima.
|
|
12
|
+
*/
|
|
13
|
+
export class ArchiveLimitsValidator {
|
|
14
|
+
/**
|
|
15
|
+
* Normalizes bounded own overrides into a new immutable record.
|
|
16
|
+
* @param {unknown} overrides Limit overrides.
|
|
17
|
+
* @param {Record<string, number>} defaults Hard maximum values.
|
|
18
|
+
* @returns {Readonly<Record<string, number>>} Normalized limits.
|
|
19
|
+
*/
|
|
20
|
+
static normalize(overrides = {}, defaults = {}) {
|
|
21
|
+
if (!ArchiveLimitsValidator.#isPlainRecord(overrides)) {
|
|
22
|
+
throw ArchiveLimitsValidator.#error(
|
|
23
|
+
'',
|
|
24
|
+
'Archive limits must be a plain object.'
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const descriptors = Object.getOwnPropertyDescriptors(overrides)
|
|
29
|
+
const allowed = new Set(Object.keys(defaults))
|
|
30
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
31
|
+
if (
|
|
32
|
+
typeof key !== 'string' ||
|
|
33
|
+
!allowed.has(key) ||
|
|
34
|
+
descriptors[key].enumerable !== true ||
|
|
35
|
+
descriptors[key].get ||
|
|
36
|
+
descriptors[key].set
|
|
37
|
+
) {
|
|
38
|
+
throw ArchiveLimitsValidator.#error(
|
|
39
|
+
typeof key === 'string' ? key : '',
|
|
40
|
+
'Archive limits contain an unknown or accessor-backed key.'
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const normalized = {}
|
|
46
|
+
for (const [key, maximum] of Object.entries(defaults)) {
|
|
47
|
+
const descriptor = descriptors[key]
|
|
48
|
+
const value = descriptor ? descriptor.value : maximum
|
|
49
|
+
ArchiveLimitsValidator.#assertValue(key, value, maximum)
|
|
50
|
+
normalized[key] = value
|
|
51
|
+
}
|
|
52
|
+
return Object.freeze(normalized)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Checks whether a value is an ordinary own-property record.
|
|
57
|
+
* @param {unknown} value Candidate record.
|
|
58
|
+
* @returns {boolean} Whether the record is safe to inspect.
|
|
59
|
+
*/
|
|
60
|
+
static #isPlainRecord(value) {
|
|
61
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
const prototype = Object.getPrototypeOf(value)
|
|
65
|
+
return prototype === Object.prototype || prototype === null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validates one numeric override.
|
|
70
|
+
* @param {string} key Limit name.
|
|
71
|
+
* @param {unknown} value Limit value.
|
|
72
|
+
* @param {number} maximum Hard maximum.
|
|
73
|
+
* @returns {void}
|
|
74
|
+
*/
|
|
75
|
+
static #assertValue(key, value, maximum) {
|
|
76
|
+
const number = typeof value === 'number' ? value : Number.NaN
|
|
77
|
+
const minimum = key === 'maxArchiveDepth' ? 0 : Number.MIN_VALUE
|
|
78
|
+
const invalidInteger =
|
|
79
|
+
INTEGER_LIMITS.has(key) && !Number.isSafeInteger(number)
|
|
80
|
+
if (
|
|
81
|
+
!Number.isFinite(number) ||
|
|
82
|
+
invalidInteger ||
|
|
83
|
+
number < minimum ||
|
|
84
|
+
number > maximum
|
|
85
|
+
) {
|
|
86
|
+
throw ArchiveLimitsValidator.#error(
|
|
87
|
+
key,
|
|
88
|
+
`Archive limit ${key} is outside its safe range.`
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Creates a stable invalid-limit error.
|
|
95
|
+
* @param {string} key Rejected key.
|
|
96
|
+
* @param {string} message Failure message.
|
|
97
|
+
* @returns {ToolkitError} Typed validation error.
|
|
98
|
+
*/
|
|
99
|
+
static #error(key, message) {
|
|
100
|
+
return new ToolkitError(message, {
|
|
101
|
+
code: 'ERR_ARCHIVE_LIMIT_INVALID',
|
|
102
|
+
category: 'validation',
|
|
103
|
+
format: 'archive',
|
|
104
|
+
details: key ? { key } : {}
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const OWNED_PARSER_INPUTS = new WeakSet()
|
|
2
|
+
const OWNED_PROJECT_ENTRIES = new WeakSet()
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Marks structured-cloned worker inputs that already belong to the receiver.
|
|
6
|
+
*/
|
|
7
|
+
export class AsyncInputOwnership {
|
|
8
|
+
/**
|
|
9
|
+
* Marks one worker-received parser input as receiver-owned.
|
|
10
|
+
* @param {object} input Parser input.
|
|
11
|
+
* @returns {object} The same input.
|
|
12
|
+
*/
|
|
13
|
+
static markParser(input) {
|
|
14
|
+
if (input && typeof input === 'object') OWNED_PARSER_INPUTS.add(input)
|
|
15
|
+
return input
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Returns whether one parser input already belongs to this process.
|
|
20
|
+
* @param {unknown} input Parser input candidate.
|
|
21
|
+
* @returns {boolean} Whether the input is receiver-owned.
|
|
22
|
+
*/
|
|
23
|
+
static ownsParser(input) {
|
|
24
|
+
return Boolean(
|
|
25
|
+
input && typeof input === 'object' && OWNED_PARSER_INPUTS.has(input)
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Marks one worker-received project entry array as receiver-owned.
|
|
31
|
+
* @param {object[]} entries Project entries.
|
|
32
|
+
* @returns {object[]} The same entries.
|
|
33
|
+
*/
|
|
34
|
+
static markProject(entries) {
|
|
35
|
+
if (entries && typeof entries === 'object') {
|
|
36
|
+
OWNED_PROJECT_ENTRIES.add(entries)
|
|
37
|
+
}
|
|
38
|
+
return entries
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Returns whether project entries already belong to this process.
|
|
43
|
+
* @param {unknown} entries Project entry candidate.
|
|
44
|
+
* @returns {boolean} Whether the entries are receiver-owned.
|
|
45
|
+
*/
|
|
46
|
+
static ownsProject(entries) {
|
|
47
|
+
return Boolean(
|
|
48
|
+
entries &&
|
|
49
|
+
typeof entries === 'object' &&
|
|
50
|
+
OWNED_PROJECT_ENTRIES.has(entries)
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
Object.freeze(AsyncInputOwnership.prototype)
|
|
56
|
+
Object.freeze(AsyncInputOwnership)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ToolkitError } from './contracts/ToolkitError.mjs'
|
|
2
|
+
|
|
3
|
+
const MAX_ATTACHED_VALUES = 100_000
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Enforces the request-global ceiling for caller-attached asset values.
|
|
7
|
+
*/
|
|
8
|
+
export class AttachedValueLimits {
|
|
9
|
+
/**
|
|
10
|
+
* Returns the hard attached-value ceiling shared by every execution path.
|
|
11
|
+
* @returns {number} Maximum attached values per request.
|
|
12
|
+
*/
|
|
13
|
+
static get maximum() {
|
|
14
|
+
return MAX_ATTACHED_VALUES
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Adds one attached-value array length without visiting its elements.
|
|
19
|
+
* @param {unknown} values Attached-value array candidate.
|
|
20
|
+
* @param {number} [current] Values already counted in this request.
|
|
21
|
+
* @returns {number} Updated request-global count.
|
|
22
|
+
*/
|
|
23
|
+
static add(values, current = 0) {
|
|
24
|
+
if (!Array.isArray(values)) {
|
|
25
|
+
throw new TypeError('Attached values must be an array.')
|
|
26
|
+
}
|
|
27
|
+
let prototype
|
|
28
|
+
let lengthDescriptor
|
|
29
|
+
try {
|
|
30
|
+
prototype = Object.getPrototypeOf(values)
|
|
31
|
+
lengthDescriptor = Object.getOwnPropertyDescriptor(values, 'length')
|
|
32
|
+
} catch {
|
|
33
|
+
throw new TypeError(
|
|
34
|
+
'Attached values could not be inspected safely.'
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
const length = lengthDescriptor?.value
|
|
38
|
+
if (
|
|
39
|
+
prototype !== Array.prototype ||
|
|
40
|
+
!lengthDescriptor ||
|
|
41
|
+
!Object.hasOwn(lengthDescriptor, 'value') ||
|
|
42
|
+
!Number.isSafeInteger(length) ||
|
|
43
|
+
length < 0
|
|
44
|
+
) {
|
|
45
|
+
throw new TypeError('Attached values must be a plain array.')
|
|
46
|
+
}
|
|
47
|
+
const total = current + length
|
|
48
|
+
if (!Number.isSafeInteger(total) || total > MAX_ATTACHED_VALUES) {
|
|
49
|
+
throw new ToolkitError(
|
|
50
|
+
`Attached values exceed the ${MAX_ATTACHED_VALUES} item limit.`,
|
|
51
|
+
{
|
|
52
|
+
code: 'ERR_ATTACHED_VALUE_LIMIT_EXCEEDED',
|
|
53
|
+
category: 'validation',
|
|
54
|
+
format: 'circuitjson',
|
|
55
|
+
details: {
|
|
56
|
+
attachedValues: total,
|
|
57
|
+
maxAttachedValues: MAX_ATTACHED_VALUES
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
return total
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
Object.freeze(AttachedValueLimits.prototype)
|
|
67
|
+
Object.freeze(AttachedValueLimits)
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes CircuitJSON warning and error elements for shared indexes.
|
|
3
|
+
*/
|
|
4
|
+
export class CircuitJsonDiagnosticIndexer {
|
|
5
|
+
/**
|
|
6
|
+
* Returns true when an element is a warning or error row.
|
|
7
|
+
* @param {object} element Element.
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
static isElement(element) {
|
|
11
|
+
return (
|
|
12
|
+
CircuitJsonDiagnosticIndexer.isType(String(element?.type || '')) ||
|
|
13
|
+
Boolean(element?.error_type || element?.warning_type)
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Returns true for upstream warning and error discriminants.
|
|
19
|
+
* @param {string} type Element type.
|
|
20
|
+
* @returns {boolean}
|
|
21
|
+
*/
|
|
22
|
+
static isType(type) {
|
|
23
|
+
return (
|
|
24
|
+
type.includes('error') ||
|
|
25
|
+
type.endsWith('_warning') ||
|
|
26
|
+
type === 'source_ambiguous_port_reference'
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Builds one normalized diagnostic.
|
|
32
|
+
* @param {object} element Diagnostic element.
|
|
33
|
+
* @param {string} elementId Stable element id.
|
|
34
|
+
* @returns {object}
|
|
35
|
+
*/
|
|
36
|
+
static fromElement(element, elementId) {
|
|
37
|
+
const type = String(
|
|
38
|
+
element?.error_type || element?.warning_type || element?.type || ''
|
|
39
|
+
)
|
|
40
|
+
return {
|
|
41
|
+
severity: element?.warning_type ? 'warning' : 'error',
|
|
42
|
+
sourceFormat: 'circuitjson',
|
|
43
|
+
type,
|
|
44
|
+
category: CircuitJsonDiagnosticIndexer.#category(type),
|
|
45
|
+
message: String(element?.message || type || 'CircuitJSON issue'),
|
|
46
|
+
elementId,
|
|
47
|
+
...CircuitJsonDiagnosticIndexer.#relations(element)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Extracts optional relation ids from one diagnostic element.
|
|
53
|
+
* @param {object} element Diagnostic element.
|
|
54
|
+
* @returns {object}
|
|
55
|
+
*/
|
|
56
|
+
static #relations(element) {
|
|
57
|
+
const scalar = Object.fromEntries(
|
|
58
|
+
[
|
|
59
|
+
['sourceComponentId', element?.source_component_id],
|
|
60
|
+
['sourcePortId', element?.source_port_id],
|
|
61
|
+
['sourceNetId', element?.source_net_id],
|
|
62
|
+
['sourceTraceId', element?.source_trace_id],
|
|
63
|
+
['pcbComponentId', element?.pcb_component_id],
|
|
64
|
+
['pcbPortId', element?.pcb_port_id],
|
|
65
|
+
['pcbTraceId', element?.pcb_trace_id],
|
|
66
|
+
['pcbSmtpadId', element?.pcb_smtpad_id],
|
|
67
|
+
['pcbViaId', element?.pcb_via_id],
|
|
68
|
+
['pcbPlatedHoleId', element?.pcb_plated_hole_id],
|
|
69
|
+
['pcbHoleId', element?.pcb_hole_id],
|
|
70
|
+
['schematicComponentId', element?.schematic_component_id],
|
|
71
|
+
['schematicSymbolId', element?.schematic_symbol_id],
|
|
72
|
+
['schematicPortId', element?.schematic_port_id]
|
|
73
|
+
]
|
|
74
|
+
.map(([key, value]) => [key, String(value || '').trim()])
|
|
75
|
+
.filter(([_key, value]) => value)
|
|
76
|
+
)
|
|
77
|
+
const plural = Object.fromEntries(
|
|
78
|
+
[
|
|
79
|
+
[
|
|
80
|
+
'sourceComponentIds',
|
|
81
|
+
element?.source_component_ids,
|
|
82
|
+
element?.source_component_id
|
|
83
|
+
],
|
|
84
|
+
[
|
|
85
|
+
'sourcePortIds',
|
|
86
|
+
element?.source_port_ids,
|
|
87
|
+
element?.source_port_id
|
|
88
|
+
],
|
|
89
|
+
[
|
|
90
|
+
'sourceNetIds',
|
|
91
|
+
element?.source_net_ids,
|
|
92
|
+
element?.source_net_id
|
|
93
|
+
],
|
|
94
|
+
[
|
|
95
|
+
'sourceTraceIds',
|
|
96
|
+
element?.source_trace_ids,
|
|
97
|
+
element?.source_trace_id
|
|
98
|
+
],
|
|
99
|
+
[
|
|
100
|
+
'pcbComponentIds',
|
|
101
|
+
element?.pcb_component_ids,
|
|
102
|
+
element?.pcb_component_id
|
|
103
|
+
],
|
|
104
|
+
['pcbPortIds', element?.pcb_port_ids, element?.pcb_port_id],
|
|
105
|
+
['pcbTraceIds', element?.pcb_trace_ids, element?.pcb_trace_id],
|
|
106
|
+
[
|
|
107
|
+
'pcbPadIds',
|
|
108
|
+
element?.pcb_pad_ids ?? element?.pcb_smtpad_ids,
|
|
109
|
+
element?.pcb_pad_id ?? element?.pcb_smtpad_id
|
|
110
|
+
],
|
|
111
|
+
['pcbViaIds', element?.pcb_via_ids, element?.pcb_via_id],
|
|
112
|
+
[
|
|
113
|
+
'pcbPlatedHoleIds',
|
|
114
|
+
element?.pcb_plated_hole_ids,
|
|
115
|
+
element?.pcb_plated_hole_id
|
|
116
|
+
],
|
|
117
|
+
['pcbHoleIds', element?.pcb_hole_ids, element?.pcb_hole_id]
|
|
118
|
+
]
|
|
119
|
+
.map(([key, values, singular]) => {
|
|
120
|
+
const ids = Array.isArray(values)
|
|
121
|
+
? [...new Set(values.map(String).filter(Boolean))]
|
|
122
|
+
: []
|
|
123
|
+
const singularId = String(singular || '').trim()
|
|
124
|
+
return [
|
|
125
|
+
key,
|
|
126
|
+
ids.length === 1 && ids[0] === singularId ? [] : ids
|
|
127
|
+
]
|
|
128
|
+
})
|
|
129
|
+
.filter(([_key, values]) => values.length)
|
|
130
|
+
)
|
|
131
|
+
return { ...scalar, ...plural }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Resolves a broad diagnostic category.
|
|
136
|
+
* @param {string} type Diagnostic type or code.
|
|
137
|
+
* @returns {string}
|
|
138
|
+
*/
|
|
139
|
+
static #category(type) {
|
|
140
|
+
const text = String(type || '').toLowerCase()
|
|
141
|
+
if (text.includes('clearance')) return 'clearance'
|
|
142
|
+
if (text.includes('autorouting') || text.includes('trace_error')) {
|
|
143
|
+
return 'routing'
|
|
144
|
+
}
|
|
145
|
+
if (text.includes('placement') || text.includes('outside_board')) {
|
|
146
|
+
return 'placement'
|
|
147
|
+
}
|
|
148
|
+
if (
|
|
149
|
+
text.includes('trace_missing') ||
|
|
150
|
+
text.includes('not_connected') ||
|
|
151
|
+
text.includes('missing_trace') ||
|
|
152
|
+
text.includes('pin_missing_trace') ||
|
|
153
|
+
text.includes('pin_must_be_connected')
|
|
154
|
+
) {
|
|
155
|
+
return 'connectivity'
|
|
156
|
+
}
|
|
157
|
+
if (text.includes('layout')) return 'layout'
|
|
158
|
+
if (text.includes('simulation')) return 'simulation'
|
|
159
|
+
if (text.includes('footprint')) return 'footprint'
|
|
160
|
+
if (
|
|
161
|
+
text.includes('pin_defined') ||
|
|
162
|
+
text.includes('pins_underspecified') ||
|
|
163
|
+
text.includes('ground_pin') ||
|
|
164
|
+
text.includes('power_pin')
|
|
165
|
+
) {
|
|
166
|
+
return 'pin-definition'
|
|
167
|
+
}
|
|
168
|
+
if (
|
|
169
|
+
text.includes('manufacturer_part') ||
|
|
170
|
+
text.includes('missing_property') ||
|
|
171
|
+
text.includes('property_ignored')
|
|
172
|
+
) {
|
|
173
|
+
return 'metadata'
|
|
174
|
+
}
|
|
175
|
+
if (text.includes('manual_edit_conflict')) return 'edit-conflict'
|
|
176
|
+
if (text.includes('property') || text.includes('misconfigured')) {
|
|
177
|
+
return 'configuration'
|
|
178
|
+
}
|
|
179
|
+
return 'general'
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
Object.freeze(CircuitJsonDiagnosticIndexer.prototype)
|
|
184
|
+
Object.freeze(CircuitJsonDiagnosticIndexer)
|