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
@@ -1,28 +1,35 @@
1
- # Library Scope
1
+ # Library scope
2
2
 
3
- `circuitjson-toolkit` owns reusable browser and Node utilities for serialized
4
- CircuitJSON element arrays.
3
+ `circuitjson-toolkit` owns browser- and Node-compatible common operations over
4
+ immutable CircuitJSON.
5
5
 
6
6
  ## Included
7
7
 
8
- - CircuitJSON element-array validation
9
- - Standalone JSON text and byte parsing
10
- - Parser-style metadata attachment
11
- - Lookup indexes by element type and stable IDs
12
- - Source and PCB component lookup maps
13
- - Millimeter-to-mil conversion helpers
14
- - Local SPICE transient graph helpers that produce complete CircuitJSON
15
- simulation experiment element sets
16
- - Deterministic SPICE graph summaries and non-fatal local syntax diagnostics
17
- - Small documentation and tests for toolkit behavior
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
- - Three.js or browser DOM rendering
22
- - PCB 3D runtime behavior
23
- - ECAD source parser logic
24
- - Native ECAD source-format compatibility adapters
25
- - Network fetching or remote asset loading
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
- Renderer fixes belong in renderer packages. Source-format fixes belong in the
28
- source toolkit that parses that format.
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)