circuitjson-toolkit 1.0.16 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/AGENTS.md +15 -0
  2. package/NOTICE.md +31 -0
  3. package/README.md +267 -107
  4. package/docs/api.md +501 -96
  5. package/docs/capabilities.md +70 -0
  6. package/docs/migration/behaviors.md +45 -0
  7. package/docs/migration/parser.md +60 -0
  8. package/docs/migration/renderers.md +515 -0
  9. package/docs/migration/root.md +740 -0
  10. package/docs/migration.md +120 -0
  11. package/docs/model-format.md +175 -57
  12. package/docs/provenance.md +206 -0
  13. package/docs/release-notes-v1.1.0.md +154 -0
  14. package/docs/testing.md +117 -7
  15. package/package.json +31 -5
  16. package/spec/api-baseline-v1.0.17.json +1 -0
  17. package/spec/baseline-provenance-v1.0.17.json +7 -0
  18. package/spec/circuitjson-schema-snapshot.json +321 -0
  19. package/spec/circuitjson-schema-source.json +28 -0
  20. package/spec/feature-preservation.json +1 -0
  21. package/spec/library-scope.md +27 -20
  22. package/src/capabilities.mjs +1 -0
  23. package/src/core/ArchiveEntryPath.mjs +93 -0
  24. package/src/core/ArchiveLimits.mjs +31 -0
  25. package/src/core/ArchiveLimitsValidator.mjs +107 -0
  26. package/src/core/AsyncInputOwnership.mjs +56 -0
  27. package/src/core/AttachedValueLimits.mjs +67 -0
  28. package/src/core/CircuitJsonDiagnosticIndexer.mjs +184 -0
  29. package/src/core/CircuitJsonDocument.mjs +19 -61
  30. package/src/core/CircuitJsonElementTypes.mjs +10 -0
  31. package/src/core/CircuitJsonElementValidator.mjs +98 -847
  32. package/src/core/CircuitJsonIndexer.mjs +274 -194
  33. package/src/core/CircuitJsonManufacturingBuilder.mjs +167 -164
  34. package/src/core/CircuitJsonParser.mjs +75 -13
  35. package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +12 -6
  36. package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +108 -10
  37. package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +1 -1
  38. package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +44 -38
  39. package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +146 -28
  40. package/src/core/CircuitJsonPcbPrimitiveFields.mjs +70 -4
  41. package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +18 -2
  42. package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +26 -9
  43. package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +7 -6
  44. package/src/core/CircuitJsonSerializedInputAudit.mjs +87 -0
  45. package/src/core/CircuitJsonSourceMetadata.mjs +5 -1
  46. package/src/core/CircuitJsonSupportMatrixBuilder.mjs +3 -1
  47. package/src/core/CircuitJsonToolkitElementSchema.mjs +218 -0
  48. package/src/core/CircuitJsonUnitParsers.mjs +101 -0
  49. package/src/core/CircuitJsonUnits.mjs +13 -87
  50. package/src/core/CircuitJsonUpstreamSchema.mjs +9 -0
  51. package/src/core/CircuitJsonUpstreamValidator.mjs +418 -0
  52. package/src/core/CircuitJsonValidationUnits.mjs +6 -0
  53. package/src/core/ManufacturingService.mjs +323 -0
  54. package/src/core/Parser.mjs +343 -0
  55. package/src/core/ParserOptions.mjs +333 -0
  56. package/src/core/PcbBoundsSelectionModel.mjs +55 -19
  57. package/src/core/PcbDiagnosticFocusModel.mjs +42 -11
  58. package/src/core/PcbInteractionIndex.mjs +368 -0
  59. package/src/core/PcbInteractionPrimitiveModel.mjs +393 -62
  60. package/src/core/ProjectAsyncInputOwner.mjs +70 -0
  61. package/src/core/ProjectLoader.mjs +975 -0
  62. package/src/core/SimulationService.mjs +790 -0
  63. package/src/core/ToolkitCapabilities.mjs +130 -0
  64. package/src/core/ZipArchiveInspector.mjs +649 -0
  65. package/src/core/context/BinaryDataSnapshot.mjs +217 -0
  66. package/src/core/context/CircuitJsonContextIndexes.mjs +96 -0
  67. package/src/core/context/CircuitJsonDerivedCache.mjs +114 -0
  68. package/src/core/context/CircuitJsonDocumentContext.mjs +353 -0
  69. package/src/core/context/CircuitJsonLegacyModel.mjs +147 -0
  70. package/src/core/context/CircuitJsonLegacyNormalizer.mjs +847 -0
  71. package/src/core/context/CircuitJsonMetadataBoundary.mjs +76 -0
  72. package/src/core/context/CircuitJsonModelFreezeTraversal.mjs +179 -0
  73. package/src/core/context/CircuitJsonReadOnlyDocument.mjs +920 -0
  74. package/src/core/context/CircuitJsonSchematicTableNormalizer.mjs +314 -0
  75. package/src/core/context/CircuitJsonValidationAuthority.mjs +39 -0
  76. package/src/core/context/CircuitJsonValidationProof.mjs +217 -0
  77. package/src/core/context/PcbPrimitivePreparation.mjs +198 -0
  78. package/src/core/context/PcbSpatialIndex.mjs +701 -0
  79. package/src/core/context/ProtectedExtensionBinaryBoundary.mjs +128 -0
  80. package/src/core/context/StructuredDataSnapshot.mjs +683 -0
  81. package/src/core/contracts/DocumentResult.mjs +198 -0
  82. package/src/core/contracts/ProjectResult.mjs +96 -0
  83. package/src/core/contracts/RuntimeProxyBoundary.mjs +48 -0
  84. package/src/core/contracts/ToolkitAsset.mjs +493 -0
  85. package/src/core/contracts/ToolkitDiagnostic.mjs +38 -0
  86. package/src/core/contracts/ToolkitError.mjs +176 -0
  87. package/src/core/contracts/ToolkitProgress.mjs +89 -0
  88. package/src/core/interaction/CanonicalInteractionOptions.mjs +246 -0
  89. package/src/core/interaction/PcbInteractionBounds.mjs +167 -0
  90. package/src/core/query/CircuitTraversal.mjs +343 -0
  91. package/src/core/query/ComponentGrouping.mjs +275 -0
  92. package/src/core/query/QueryNetlistBuilder.mjs +306 -0
  93. package/src/core/query/QueryService.mjs +435 -0
  94. package/src/core/query/RegexPattern.mjs +75 -0
  95. package/src/core/rendering/CanonicalBomOrder.mjs +81 -0
  96. package/src/core/rendering/CanonicalBomRows.mjs +92 -0
  97. package/src/core/rendering/CanonicalRenderOptions.mjs +498 -0
  98. package/src/core/rendering/CanonicalSvgDocument.mjs +102 -0
  99. package/src/core/rendering/PcbRenderPlan.mjs +429 -0
  100. package/src/core/rendering/SchematicSheetSelector.mjs +335 -0
  101. package/src/core/scene3d/PcbScene3dBuilder.mjs +906 -0
  102. package/src/core/scene3d/PcbScene3dPreparator.mjs +47 -0
  103. package/src/core/scene3d/Scene3dAssetIndex.mjs +284 -0
  104. package/src/core/scene3d/Scene3dBoardModel.mjs +596 -0
  105. package/src/core/scene3d/Scene3dDocumentMetadata.mjs +167 -0
  106. package/src/core/scene3d/Scene3dFreeze.mjs +37 -0
  107. package/src/core/scene3d/Scene3dIdRegistry.mjs +34 -0
  108. package/src/core/scene3d/Scene3dInputPreflight.mjs +193 -0
  109. package/src/core/scene3d/Scene3dMaterials.mjs +58 -0
  110. package/src/core/scene3d/Scene3dModelReference.mjs +134 -0
  111. package/src/core/scene3d/Scene3dOptions.mjs +363 -0
  112. package/src/core/scene3d/SceneAssetResolver.mjs +441 -0
  113. package/src/core/simulation/SimulationParameterCloner.mjs +543 -0
  114. package/src/core/worker/ParserWorkerClient.mjs +997 -0
  115. package/src/core/worker/ToolkitWorkerProtocol.mjs +412 -0
  116. package/src/core/worker/WorkerRequestData.mjs +623 -0
  117. package/src/core/worker/WorkerResponseData.mjs +905 -0
  118. package/src/extensions.mjs +37 -0
  119. package/src/index.mjs +14 -9
  120. package/src/interaction.mjs +2 -0
  121. package/src/manufacturing.mjs +1 -0
  122. package/src/parser.mjs +12 -2
  123. package/src/project.mjs +5 -0
  124. package/src/query.mjs +1 -0
  125. package/src/renderers.mjs +3 -29
  126. package/src/scene3d.mjs +3 -0
  127. package/src/simulation.mjs +1 -0
  128. package/src/styles/renderers.css +24 -0
  129. package/src/testing/ToolkitContractFixtures.mjs +124 -0
  130. package/src/testing/ToolkitLoopbackWorker.mjs +174 -0
  131. package/src/testing/runToolkitContract.mjs +705 -0
  132. package/src/testing.mjs +3 -0
  133. package/src/ui/BomTableRenderer.mjs +304 -0
  134. package/src/ui/CircuitJsonPcbBoardSvgRenderer.mjs +80 -0
  135. package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +3 -5
  136. package/src/ui/CircuitJsonPcbSvgRenderer.mjs +63 -43
  137. package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +3 -5
  138. package/src/ui/CircuitJsonSchematicDebugRenderer.mjs +164 -0
  139. package/src/ui/CircuitJsonSchematicImageSvgRenderer.mjs +210 -0
  140. package/src/ui/CircuitJsonSchematicLineRenderer.mjs +86 -0
  141. package/src/ui/CircuitJsonSchematicSheetSymbolSvgRenderer.mjs +98 -0
  142. package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +117 -17
  143. package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +67 -20
  144. package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +45 -9
  145. package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +151 -148
  146. package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +4 -292
  147. package/src/ui/PcbSvgRenderer.mjs +41 -0
  148. package/src/ui/SafeSvgPaint.mjs +26 -0
  149. package/src/ui/SafeXmlText.mjs +60 -0
  150. package/src/ui/SchematicGeometryBounds.mjs +540 -0
  151. package/src/ui/SchematicSvgRenderer.mjs +110 -0
  152. package/src/ui/SchematicTableGeometry.mjs +319 -0
  153. package/src/ui/SchematicTextAnchor.mjs +55 -0
  154. package/src/ui/SchematicTextBounds.mjs +98 -0
  155. package/src/workers/parser.worker.mjs +59 -0
@@ -0,0 +1,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)