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,314 @@
|
|
|
1
|
+
import { CircuitJsonUnits } from '../CircuitJsonUnits.mjs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Projects grid-addressed table cells onto upstream CircuitJSON geometry.
|
|
5
|
+
*/
|
|
6
|
+
export class CircuitJsonSchematicTableNormalizer {
|
|
7
|
+
/**
|
|
8
|
+
* Normalizes every table cell in one model.
|
|
9
|
+
* @param {object[]} rows Model rows.
|
|
10
|
+
* @param {boolean} owned Whether rows may be mutated in place.
|
|
11
|
+
* @returns {boolean} Whether any cell changed.
|
|
12
|
+
*/
|
|
13
|
+
static normalize(rows, owned) {
|
|
14
|
+
const tables = new Map()
|
|
15
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
16
|
+
const table = CircuitJsonSchematicTableNormalizer.#table(
|
|
17
|
+
rows[index]
|
|
18
|
+
)
|
|
19
|
+
if (table) tables.set(table.id, table)
|
|
20
|
+
}
|
|
21
|
+
if (!tables.size) return false
|
|
22
|
+
|
|
23
|
+
let changed = false
|
|
24
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
25
|
+
const descriptors = CircuitJsonSchematicTableNormalizer.#record(
|
|
26
|
+
rows[index]
|
|
27
|
+
)
|
|
28
|
+
if (descriptors?.type?.value !== 'schematic_table_cell') continue
|
|
29
|
+
const fields =
|
|
30
|
+
CircuitJsonSchematicTableNormalizer.#fields(descriptors)
|
|
31
|
+
const table = tables.get(String(fields.schematic_table_id || ''))
|
|
32
|
+
const normalized = CircuitJsonSchematicTableNormalizer.#cell(
|
|
33
|
+
fields,
|
|
34
|
+
table
|
|
35
|
+
)
|
|
36
|
+
if (
|
|
37
|
+
!normalized ||
|
|
38
|
+
!CircuitJsonSchematicTableNormalizer.#changed(
|
|
39
|
+
normalized,
|
|
40
|
+
descriptors
|
|
41
|
+
)
|
|
42
|
+
) {
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
changed = true
|
|
46
|
+
if (owned) {
|
|
47
|
+
for (const key of Object.keys(rows[index])) {
|
|
48
|
+
delete rows[index][key]
|
|
49
|
+
}
|
|
50
|
+
Object.assign(rows[index], normalized)
|
|
51
|
+
} else {
|
|
52
|
+
rows[index] = normalized
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return changed
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Builds table grid offsets from one table row.
|
|
60
|
+
* @param {unknown} row Table row candidate.
|
|
61
|
+
* @returns {Record<string, any> | null} Table geometry.
|
|
62
|
+
*/
|
|
63
|
+
static #table(row) {
|
|
64
|
+
const descriptors = CircuitJsonSchematicTableNormalizer.#record(row)
|
|
65
|
+
if (descriptors?.type?.value !== 'schematic_table') return null
|
|
66
|
+
const fields = CircuitJsonSchematicTableNormalizer.#fields(descriptors)
|
|
67
|
+
const id = String(fields.schematic_table_id || '').trim()
|
|
68
|
+
const columns = CircuitJsonSchematicTableNormalizer.#lengths(
|
|
69
|
+
fields.column_widths
|
|
70
|
+
)
|
|
71
|
+
const rows = CircuitJsonSchematicTableNormalizer.#lengths(
|
|
72
|
+
fields.row_heights
|
|
73
|
+
)
|
|
74
|
+
const anchor = CircuitJsonUnits.optionalPoint(fields.anchor_position)
|
|
75
|
+
if (!id || !columns.length || !rows.length || !anchor) return null
|
|
76
|
+
const width = columns.reduce((total, value) => total + value, 0)
|
|
77
|
+
const height = rows.reduce((total, value) => total + value, 0)
|
|
78
|
+
const name = String(fields.anchor || 'top_left').toLowerCase()
|
|
79
|
+
const [vertical, horizontal] =
|
|
80
|
+
name === 'center' || name === 'middle'
|
|
81
|
+
? ['center', 'center']
|
|
82
|
+
: name.split('_')
|
|
83
|
+
return {
|
|
84
|
+
id,
|
|
85
|
+
x: CircuitJsonSchematicTableNormalizer.#anchorCoordinate(
|
|
86
|
+
anchor.x,
|
|
87
|
+
width,
|
|
88
|
+
horizontal
|
|
89
|
+
),
|
|
90
|
+
y: CircuitJsonSchematicTableNormalizer.#anchorCoordinate(
|
|
91
|
+
anchor.y,
|
|
92
|
+
height,
|
|
93
|
+
vertical
|
|
94
|
+
),
|
|
95
|
+
columns,
|
|
96
|
+
rows,
|
|
97
|
+
columnOffsets:
|
|
98
|
+
CircuitJsonSchematicTableNormalizer.#offsets(columns),
|
|
99
|
+
rowOffsets: CircuitJsonSchematicTableNormalizer.#offsets(rows)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Converts one grid-addressed cell to canonical geometry and indexes.
|
|
105
|
+
* @param {Record<string, any>} fields Cell fields.
|
|
106
|
+
* @param {Record<string, any> | undefined} table Parent table geometry.
|
|
107
|
+
* @returns {Record<string, any> | null} Canonical cell fields.
|
|
108
|
+
*/
|
|
109
|
+
static #cell(fields, table) {
|
|
110
|
+
if (!table) return null
|
|
111
|
+
const startRow = CircuitJsonSchematicTableNormalizer.#index(
|
|
112
|
+
fields.start_row_index ??
|
|
113
|
+
fields.row ??
|
|
114
|
+
fields.row_index ??
|
|
115
|
+
fields.rowIndex
|
|
116
|
+
)
|
|
117
|
+
const startColumn = CircuitJsonSchematicTableNormalizer.#index(
|
|
118
|
+
fields.start_column_index ??
|
|
119
|
+
fields.column ??
|
|
120
|
+
fields.col ??
|
|
121
|
+
fields.column_index ??
|
|
122
|
+
fields.columnIndex
|
|
123
|
+
)
|
|
124
|
+
if (
|
|
125
|
+
startRow === null ||
|
|
126
|
+
startColumn === null ||
|
|
127
|
+
startRow >= table.rows.length ||
|
|
128
|
+
startColumn >= table.columns.length
|
|
129
|
+
) {
|
|
130
|
+
return null
|
|
131
|
+
}
|
|
132
|
+
const endRow = CircuitJsonSchematicTableNormalizer.#endIndex(
|
|
133
|
+
fields.end_row_index,
|
|
134
|
+
startRow,
|
|
135
|
+
fields.row_span ?? fields.rowSpan,
|
|
136
|
+
table.rows.length
|
|
137
|
+
)
|
|
138
|
+
const endColumn = CircuitJsonSchematicTableNormalizer.#endIndex(
|
|
139
|
+
fields.end_column_index,
|
|
140
|
+
startColumn,
|
|
141
|
+
fields.col_span ?? fields.column_span ?? fields.colSpan,
|
|
142
|
+
table.columns.length
|
|
143
|
+
)
|
|
144
|
+
if (endRow < startRow || endColumn < startColumn) return null
|
|
145
|
+
const x = table.x + table.columnOffsets[startColumn]
|
|
146
|
+
const y = table.y + table.rowOffsets[startRow]
|
|
147
|
+
const width =
|
|
148
|
+
table.columnOffsets[endColumn + 1] -
|
|
149
|
+
table.columnOffsets[startColumn]
|
|
150
|
+
const height = table.rowOffsets[endRow + 1] - table.rowOffsets[startRow]
|
|
151
|
+
const normalized = {
|
|
152
|
+
...fields,
|
|
153
|
+
start_row_index: startRow,
|
|
154
|
+
end_row_index: endRow,
|
|
155
|
+
start_column_index: startColumn,
|
|
156
|
+
end_column_index: endColumn,
|
|
157
|
+
center: { x: x + width / 2, y: y + height / 2 },
|
|
158
|
+
width,
|
|
159
|
+
height
|
|
160
|
+
}
|
|
161
|
+
for (const key of [
|
|
162
|
+
'row',
|
|
163
|
+
'row_index',
|
|
164
|
+
'rowIndex',
|
|
165
|
+
'row_span',
|
|
166
|
+
'rowSpan',
|
|
167
|
+
'column',
|
|
168
|
+
'col',
|
|
169
|
+
'column_index',
|
|
170
|
+
'columnIndex',
|
|
171
|
+
'col_span',
|
|
172
|
+
'column_span',
|
|
173
|
+
'colSpan'
|
|
174
|
+
]) {
|
|
175
|
+
delete normalized[key]
|
|
176
|
+
}
|
|
177
|
+
return normalized
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Resolves an anchored axis coordinate.
|
|
182
|
+
* @param {number} coordinate Anchor coordinate.
|
|
183
|
+
* @param {number} length Axis length.
|
|
184
|
+
* @param {string} alignment Axis alignment.
|
|
185
|
+
* @returns {number} Leading coordinate.
|
|
186
|
+
*/
|
|
187
|
+
static #anchorCoordinate(coordinate, length, alignment) {
|
|
188
|
+
if (alignment === 'right' || alignment === 'bottom') {
|
|
189
|
+
return coordinate - length
|
|
190
|
+
}
|
|
191
|
+
return alignment === 'center' || alignment === 'middle'
|
|
192
|
+
? coordinate - length / 2
|
|
193
|
+
: coordinate
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Resolves a bounded inclusive ending index.
|
|
198
|
+
* @param {unknown} explicit Explicit ending index.
|
|
199
|
+
* @param {number} start Starting index.
|
|
200
|
+
* @param {unknown} span Span candidate.
|
|
201
|
+
* @param {number} length Axis length.
|
|
202
|
+
* @returns {number} Inclusive ending index.
|
|
203
|
+
*/
|
|
204
|
+
static #endIndex(explicit, start, span, length) {
|
|
205
|
+
return Math.min(
|
|
206
|
+
CircuitJsonSchematicTableNormalizer.#index(explicit) ??
|
|
207
|
+
start + CircuitJsonSchematicTableNormalizer.#span(span) - 1,
|
|
208
|
+
length - 1
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Normalizes a table dimension array.
|
|
214
|
+
* @param {unknown} values Dimension candidates.
|
|
215
|
+
* @returns {number[]} Positive lengths.
|
|
216
|
+
*/
|
|
217
|
+
static #lengths(values) {
|
|
218
|
+
return (Array.isArray(values) ? values : [])
|
|
219
|
+
.map((value) => CircuitJsonUnits.optionalLength(value))
|
|
220
|
+
.filter((value) => value !== null && value > 0)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Builds cumulative table grid offsets.
|
|
225
|
+
* @param {number[]} values Lengths.
|
|
226
|
+
* @returns {number[]} Boundary offsets.
|
|
227
|
+
*/
|
|
228
|
+
static #offsets(values) {
|
|
229
|
+
const offsets = [0]
|
|
230
|
+
for (const value of values) {
|
|
231
|
+
offsets.push(offsets[offsets.length - 1] + value)
|
|
232
|
+
}
|
|
233
|
+
return offsets
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Normalizes a zero-based table index.
|
|
238
|
+
* @param {unknown} value Index candidate.
|
|
239
|
+
* @returns {number | null} Normalized index.
|
|
240
|
+
*/
|
|
241
|
+
static #index(value) {
|
|
242
|
+
const number = Number(value)
|
|
243
|
+
return Number.isInteger(number) && number >= 0 ? number : null
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Normalizes a positive table span.
|
|
248
|
+
* @param {unknown} value Span candidate.
|
|
249
|
+
* @returns {number} Normalized span.
|
|
250
|
+
*/
|
|
251
|
+
static #span(value) {
|
|
252
|
+
const number = Number(value)
|
|
253
|
+
return Number.isInteger(number) && number > 0 ? number : 1
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Copies own data descriptors into a plain record.
|
|
258
|
+
* @param {Record<string, PropertyDescriptor>} descriptors Descriptors.
|
|
259
|
+
* @returns {Record<string, any>} Plain fields.
|
|
260
|
+
*/
|
|
261
|
+
static #fields(descriptors) {
|
|
262
|
+
return Object.fromEntries(
|
|
263
|
+
Object.entries(descriptors).map(([key, descriptor]) => [
|
|
264
|
+
key,
|
|
265
|
+
descriptor.value
|
|
266
|
+
])
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Returns whether normalized fields differ from original descriptors.
|
|
272
|
+
* @param {Record<string, any>} fields Normalized fields.
|
|
273
|
+
* @param {Record<string, PropertyDescriptor>} descriptors Original fields.
|
|
274
|
+
* @returns {boolean} Whether fields changed.
|
|
275
|
+
*/
|
|
276
|
+
static #changed(fields, descriptors) {
|
|
277
|
+
const keys = Object.keys(fields)
|
|
278
|
+
return (
|
|
279
|
+
keys.length !== Object.keys(descriptors).length ||
|
|
280
|
+
keys.some(
|
|
281
|
+
(key) =>
|
|
282
|
+
!descriptors[key] || fields[key] !== descriptors[key].value
|
|
283
|
+
)
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Reads a safe plain record through data descriptors only.
|
|
289
|
+
* @param {unknown} value Record candidate.
|
|
290
|
+
* @returns {Record<string, PropertyDescriptor> | null} Safe descriptors.
|
|
291
|
+
*/
|
|
292
|
+
static #record(value) {
|
|
293
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
294
|
+
return null
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
const prototype = Object.getPrototypeOf(value)
|
|
298
|
+
const descriptors = Object.getOwnPropertyDescriptors(value)
|
|
299
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
300
|
+
return null
|
|
301
|
+
}
|
|
302
|
+
return Object.values(descriptors).some(
|
|
303
|
+
(descriptor) => !Object.hasOwn(descriptor, 'value')
|
|
304
|
+
)
|
|
305
|
+
? null
|
|
306
|
+
: descriptors
|
|
307
|
+
} catch {
|
|
308
|
+
return null
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
Object.freeze(CircuitJsonSchematicTableNormalizer.prototype)
|
|
314
|
+
Object.freeze(CircuitJsonSchematicTableNormalizer)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { CircuitJsonElementValidator } from '../CircuitJsonElementValidator.mjs'
|
|
2
|
+
|
|
3
|
+
const PROVEN_MODELS = new WeakSet()
|
|
4
|
+
const validateCircuitJsonModel = CircuitJsonElementValidator.validateModel.bind(
|
|
5
|
+
CircuitJsonElementValidator
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Owns the unforgeable deep-validation authority used by document sealing.
|
|
10
|
+
*/
|
|
11
|
+
export class CircuitJsonValidationAuthority {
|
|
12
|
+
/**
|
|
13
|
+
* Validates and freezes one model before granting private sealing authority.
|
|
14
|
+
* @param {unknown} model CircuitJSON model candidate.
|
|
15
|
+
* @returns {string[]} Validation errors.
|
|
16
|
+
*/
|
|
17
|
+
static validateAndFreeze(model) {
|
|
18
|
+
const errors = validateCircuitJsonModel(model, { freeze: true })
|
|
19
|
+
if (errors.length === 0) PROVEN_MODELS.add(model)
|
|
20
|
+
return errors
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Tests the module-private authority for one exact deeply frozen model.
|
|
25
|
+
* @param {unknown} model Model candidate.
|
|
26
|
+
* @returns {boolean} Whether validation granted sealing authority.
|
|
27
|
+
*/
|
|
28
|
+
static permitsSeal(model) {
|
|
29
|
+
return Boolean(
|
|
30
|
+
model &&
|
|
31
|
+
typeof model === 'object' &&
|
|
32
|
+
PROVEN_MODELS.has(model) &&
|
|
33
|
+
Object.isFrozen(model)
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Object.freeze(CircuitJsonValidationAuthority.prototype)
|
|
39
|
+
Object.freeze(CircuitJsonValidationAuthority)
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { CircuitJsonReadOnlyDocument } from './CircuitJsonReadOnlyDocument.mjs'
|
|
2
|
+
import { CircuitJsonValidationAuthority } from './CircuitJsonValidationAuthority.mjs'
|
|
3
|
+
|
|
4
|
+
const VALIDATION_PROOF = Symbol('CircuitJsonValidationProof')
|
|
5
|
+
const VALIDATED_INDEX_ACCESS = Symbol('CircuitJsonValidatedIndexAccess')
|
|
6
|
+
const VALIDATION_TOKEN_SECRET = Object.freeze({})
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Holds an unforgeable runtime binding to one validated model.
|
|
10
|
+
*/
|
|
11
|
+
class CircuitJsonValidationToken {
|
|
12
|
+
#model
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates a token for one already validated immutable model.
|
|
16
|
+
* @param {object[]} model Proven CircuitJSON model.
|
|
17
|
+
* @param {object} secret Module-private construction authority.
|
|
18
|
+
*/
|
|
19
|
+
constructor(model, secret) {
|
|
20
|
+
if (secret !== VALIDATION_TOKEN_SECRET) {
|
|
21
|
+
throw new TypeError('CircuitJSON validation proofs are internal.')
|
|
22
|
+
}
|
|
23
|
+
this.#model = model
|
|
24
|
+
Object.freeze(this)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Tests whether a candidate token is bound to the supplied model.
|
|
29
|
+
* @param {unknown} candidate Token candidate.
|
|
30
|
+
* @param {unknown} model Model candidate.
|
|
31
|
+
* @returns {boolean} Whether the private model slot matches.
|
|
32
|
+
*/
|
|
33
|
+
static matches(candidate, model) {
|
|
34
|
+
try {
|
|
35
|
+
return (
|
|
36
|
+
candidate.#model === model &&
|
|
37
|
+
Array.isArray(model) &&
|
|
38
|
+
Object.isFrozen(model)
|
|
39
|
+
)
|
|
40
|
+
} catch {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Object.freeze(CircuitJsonValidationToken.prototype)
|
|
47
|
+
Object.freeze(CircuitJsonValidationToken)
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Owns runtime-only proof metadata for immutable CircuitJSON documents.
|
|
51
|
+
*/
|
|
52
|
+
export class CircuitJsonValidationProof {
|
|
53
|
+
/**
|
|
54
|
+
* Validates, freezes, and proves one canonical document envelope.
|
|
55
|
+
* @param {Record<string, any>} document Canonical document envelope.
|
|
56
|
+
* @returns {Record<string, any>} The same read-only document envelope.
|
|
57
|
+
*/
|
|
58
|
+
static validateAndAttach(document) {
|
|
59
|
+
const model = CircuitJsonValidationProof.#requireModelData(document)
|
|
60
|
+
if (!CircuitJsonValidationProof.#matches(document, model)) {
|
|
61
|
+
const errors =
|
|
62
|
+
CircuitJsonValidationAuthority.validateAndFreeze(model)
|
|
63
|
+
if (errors.length) throw new TypeError(errors[0])
|
|
64
|
+
if (
|
|
65
|
+
CircuitJsonValidationProof.#requireModelData(document) !== model
|
|
66
|
+
) {
|
|
67
|
+
throw new TypeError(
|
|
68
|
+
'CircuitJSON document model changed during validation.'
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
Object.defineProperty(document, VALIDATION_PROOF, {
|
|
72
|
+
configurable: false,
|
|
73
|
+
enumerable: false,
|
|
74
|
+
value: new CircuitJsonValidationToken(
|
|
75
|
+
model,
|
|
76
|
+
VALIDATION_TOKEN_SECRET
|
|
77
|
+
),
|
|
78
|
+
writable: false
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
const readonlyDocument = CircuitJsonReadOnlyDocument.freezeValidated(
|
|
82
|
+
document,
|
|
83
|
+
model
|
|
84
|
+
)
|
|
85
|
+
if (
|
|
86
|
+
CircuitJsonValidationProof.#requireModelData(readonlyDocument) !==
|
|
87
|
+
model ||
|
|
88
|
+
!CircuitJsonValidationProof.#matches(readonlyDocument, model)
|
|
89
|
+
) {
|
|
90
|
+
throw new TypeError(
|
|
91
|
+
'CircuitJSON document model changed while sealing its validation proof.'
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
return readonlyDocument
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Returns true when an envelope proof matches its current model reference.
|
|
99
|
+
* @param {unknown} document Document candidate.
|
|
100
|
+
* @returns {boolean} Whether the envelope carries a reusable proof.
|
|
101
|
+
*/
|
|
102
|
+
static has(document) {
|
|
103
|
+
if (!document || typeof document !== 'object') return false
|
|
104
|
+
const descriptor = Object.getOwnPropertyDescriptor(document, 'model')
|
|
105
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')) return false
|
|
106
|
+
const model = descriptor.value
|
|
107
|
+
return CircuitJsonValidationProof.#matches(document, model)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Creates context-owned indexer options branded by a matching proof.
|
|
112
|
+
* @param {Record<string, any>} document Proven document envelope.
|
|
113
|
+
* @param {string[] | null} [families] Requested index work families.
|
|
114
|
+
* @returns {{ validated: true, families: string[] | null }} Trusted indexer options.
|
|
115
|
+
*/
|
|
116
|
+
static indexOptions(document, families = null) {
|
|
117
|
+
if (!CircuitJsonValidationProof.has(document)) {
|
|
118
|
+
throw new TypeError(
|
|
119
|
+
'Validated index access requires a matching document proof.'
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const normalizedFamilies = Array.isArray(families)
|
|
124
|
+
? Object.freeze([...new Set(families.map(String))])
|
|
125
|
+
: null
|
|
126
|
+
const options = { validated: true, families: normalizedFamilies }
|
|
127
|
+
Object.defineProperty(options, VALIDATED_INDEX_ACCESS, {
|
|
128
|
+
enumerable: false,
|
|
129
|
+
value: CircuitJsonValidationProof.#proof(document)
|
|
130
|
+
})
|
|
131
|
+
return Object.freeze(options)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Returns true only for an index request branded by a matching proof.
|
|
136
|
+
* @param {unknown} model CircuitJSON model candidate.
|
|
137
|
+
* @param {unknown} options Indexer options candidate.
|
|
138
|
+
* @returns {boolean} Whether public validation may be skipped.
|
|
139
|
+
*/
|
|
140
|
+
static permitsIndex(model, options) {
|
|
141
|
+
const proof = options?.[VALIDATED_INDEX_ACCESS]
|
|
142
|
+
return Boolean(
|
|
143
|
+
options?.validated === true &&
|
|
144
|
+
CircuitJsonValidationToken.matches(proof, model)
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Returns trusted requested index families or null for the legacy full index.
|
|
150
|
+
* @param {unknown} model CircuitJSON model candidate.
|
|
151
|
+
* @param {unknown} options Indexer options candidate.
|
|
152
|
+
* @returns {string[] | null} Requested families, or null for full work.
|
|
153
|
+
*/
|
|
154
|
+
static indexFamilies(model, options) {
|
|
155
|
+
if (!CircuitJsonValidationProof.permitsIndex(model, options)) {
|
|
156
|
+
return null
|
|
157
|
+
}
|
|
158
|
+
return Array.isArray(options.families) ? options.families : null
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Tests one envelope proof against an already captured model reference.
|
|
163
|
+
* @param {unknown} document Document candidate.
|
|
164
|
+
* @param {unknown} model Captured model candidate.
|
|
165
|
+
* @returns {boolean} Whether the proof is valid for that exact model.
|
|
166
|
+
*/
|
|
167
|
+
static #matches(document, model) {
|
|
168
|
+
return CircuitJsonValidationToken.matches(
|
|
169
|
+
CircuitJsonValidationProof.#proof(document),
|
|
170
|
+
model
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Reads one validation proof through its own data descriptor.
|
|
176
|
+
* @param {unknown} document Document candidate.
|
|
177
|
+
* @returns {unknown} Captured proof value.
|
|
178
|
+
*/
|
|
179
|
+
static #proof(document) {
|
|
180
|
+
if (!document || typeof document !== 'object') return undefined
|
|
181
|
+
let descriptor
|
|
182
|
+
try {
|
|
183
|
+
descriptor = Object.getOwnPropertyDescriptor(
|
|
184
|
+
document,
|
|
185
|
+
VALIDATION_PROOF
|
|
186
|
+
)
|
|
187
|
+
} catch {
|
|
188
|
+
return undefined
|
|
189
|
+
}
|
|
190
|
+
return descriptor && Object.hasOwn(descriptor, 'value')
|
|
191
|
+
? descriptor.value
|
|
192
|
+
: undefined
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Returns an envelope's stable own model data property.
|
|
197
|
+
* @param {unknown} document Document candidate.
|
|
198
|
+
* @returns {unknown} Captured model value.
|
|
199
|
+
*/
|
|
200
|
+
static #requireModelData(document) {
|
|
201
|
+
if (!document || typeof document !== 'object') {
|
|
202
|
+
throw new TypeError(
|
|
203
|
+
'CircuitJSON document model must be an own data property.'
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
const descriptor = Object.getOwnPropertyDescriptor(document, 'model')
|
|
207
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')) {
|
|
208
|
+
throw new TypeError(
|
|
209
|
+
'CircuitJSON document model must be an own data property.'
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
return descriptor.value
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
Object.freeze(CircuitJsonValidationProof.prototype)
|
|
217
|
+
Object.freeze(CircuitJsonValidationProof)
|