@vessel-dsp/core 0.6.3 → 0.6.5
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/README.md +11 -3
- package/dist/editor/commands.d.ts +13 -13
- package/dist/editor/commands.d.ts.map +1 -1
- package/dist/editor/commands.js +44 -29
- package/dist/editor/commands.js.map +1 -1
- package/dist/editor/factory.d.ts +1 -1
- package/dist/editor/factory.d.ts.map +1 -1
- package/dist/editor/factory.js +51 -51
- package/dist/editor/factory.js.map +1 -1
- package/dist/editor/history.d.ts +7 -7
- package/dist/editor/history.d.ts.map +1 -1
- package/dist/editor/history.js +20 -12
- package/dist/editor/history.js.map +1 -1
- package/dist/editor/index.d.ts +8 -8
- package/dist/editor/index.d.ts.map +1 -1
- package/dist/editor/index.js +4 -4
- package/dist/editor/index.js.map +1 -1
- package/dist/editor/layout.d.ts +1 -1
- package/dist/editor/layout.d.ts.map +1 -1
- package/dist/editor/layout.js +11 -6
- package/dist/editor/layout.js.map +1 -1
- package/dist/formats/circuit-json/serializer.d.ts +15 -15
- package/dist/formats/circuit-json/serializer.d.ts.map +1 -1
- package/dist/formats/circuit-json/serializer.js +486 -394
- package/dist/formats/circuit-json/serializer.js.map +1 -1
- package/dist/formats/document.d.ts +6 -6
- package/dist/formats/document.d.ts.map +1 -1
- package/dist/formats/document.js +112 -92
- package/dist/formats/document.js.map +1 -1
- package/dist/formats/interchange/parser.d.ts +1 -1
- package/dist/formats/interchange/parser.d.ts.map +1 -1
- package/dist/formats/interchange/parser.js +483 -286
- package/dist/formats/interchange/parser.js.map +1 -1
- package/dist/formats/interchange/serializer.d.ts +1 -1
- package/dist/formats/interchange/serializer.d.ts.map +1 -1
- package/dist/formats/interchange/serializer.js +59 -28
- package/dist/formats/interchange/serializer.js.map +1 -1
- package/dist/formats/ltspice/catalog.d.ts +1 -1
- package/dist/formats/ltspice/catalog.d.ts.map +1 -1
- package/dist/formats/ltspice/catalog.js +150 -48
- package/dist/formats/ltspice/catalog.js.map +1 -1
- package/dist/formats/ltspice/encoding.js +12 -40
- package/dist/formats/ltspice/encoding.js.map +1 -1
- package/dist/formats/ltspice/parser.d.ts +1 -1
- package/dist/formats/ltspice/parser.d.ts.map +1 -1
- package/dist/formats/ltspice/parser.js +122 -75
- package/dist/formats/ltspice/parser.js.map +1 -1
- package/dist/formats/ltspice/serializer.d.ts +1 -1
- package/dist/formats/ltspice/serializer.d.ts.map +1 -1
- package/dist/formats/ltspice/serializer.js +69 -47
- package/dist/formats/ltspice/serializer.js.map +1 -1
- package/dist/formats/schx/catalog.d.ts +1 -1
- package/dist/formats/schx/catalog.d.ts.map +1 -1
- package/dist/formats/schx/catalog.js +499 -254
- package/dist/formats/schx/catalog.js.map +1 -1
- package/dist/formats/schx/parser.d.ts +1 -1
- package/dist/formats/schx/parser.d.ts.map +1 -1
- package/dist/formats/schx/parser.js +40 -38
- package/dist/formats/schx/parser.js.map +1 -1
- package/dist/formats/schx/runtime-descriptors.d.ts +1 -1
- package/dist/formats/schx/runtime-descriptors.d.ts.map +1 -1
- package/dist/formats/schx/runtime-descriptors.js +239 -201
- package/dist/formats/schx/runtime-descriptors.js.map +1 -1
- package/dist/formats/schx/serializer.d.ts +2 -2
- package/dist/formats/schx/serializer.d.ts.map +1 -1
- package/dist/formats/schx/serializer.js +106 -106
- package/dist/formats/schx/serializer.js.map +1 -1
- package/dist/formats/schx/transforms.d.ts +1 -1
- package/dist/formats/schx/transforms.d.ts.map +1 -1
- package/dist/formats/schx/transforms.js +16 -8
- package/dist/formats/schx/transforms.js.map +1 -1
- package/dist/formats/spice/parser.d.ts +1 -1
- package/dist/formats/spice/parser.d.ts.map +1 -1
- package/dist/formats/spice/parser.js +105 -56
- package/dist/formats/spice/parser.js.map +1 -1
- package/dist/formats/spice/serializer.d.ts +1 -1
- package/dist/formats/spice/serializer.js +14 -12
- package/dist/formats/spice/serializer.js.map +1 -1
- package/dist/index.d.ts +47 -46
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -31
- package/dist/index.js.map +1 -1
- package/dist/model/connectivity.d.ts +1 -1
- package/dist/model/connectivity.d.ts.map +1 -1
- package/dist/model/connectivity.js +22 -7
- package/dist/model/connectivity.js.map +1 -1
- package/dist/model/netlist.d.ts +3 -3
- package/dist/model/netlist.d.ts.map +1 -1
- package/dist/model/netlist.js +117 -100
- package/dist/model/netlist.js.map +1 -1
- package/dist/model/properties.d.ts +1 -1
- package/dist/model/properties.d.ts.map +1 -1
- package/dist/model/properties.js +16 -16
- package/dist/model/properties.js.map +1 -1
- package/dist/model/quantity.d.ts +1 -1
- package/dist/model/quantity.d.ts.map +1 -1
- package/dist/model/quantity.js +35 -35
- package/dist/model/quantity.js.map +1 -1
- package/dist/model/types.d.ts +72 -37
- package/dist/model/types.d.ts.map +1 -1
- package/dist/model/types.js +1 -1
- package/dist/model/types.js.map +1 -1
- package/dist/model/validation.d.ts +5 -5
- package/dist/model/validation.d.ts.map +1 -1
- package/dist/model/validation.js +763 -315
- package/dist/model/validation.js.map +1 -1
- package/dist/model/wires.d.ts +1 -1
- package/dist/model/wires.d.ts.map +1 -1
- package/dist/model/wires.js +4 -1
- package/dist/model/wires.js.map +1 -1
- package/dist/panel/extract.d.ts +2 -2
- package/dist/panel/extract.d.ts.map +1 -1
- package/dist/panel/extract.js +376 -216
- package/dist/panel/extract.js.map +1 -1
- package/dist/panel/index.d.ts +7 -5
- package/dist/panel/index.d.ts.map +1 -1
- package/dist/panel/index.js +5 -4
- package/dist/panel/index.js.map +1 -1
- package/dist/panel/knobs.d.ts +4 -4
- package/dist/panel/knobs.d.ts.map +1 -1
- package/dist/panel/knobs.js +1 -1
- package/dist/panel/knobs.js.map +1 -1
- package/dist/panel/placement.d.ts +27 -0
- package/dist/panel/placement.d.ts.map +1 -0
- package/dist/panel/placement.js +91 -0
- package/dist/panel/placement.js.map +1 -0
- package/dist/panel/protocol.d.ts +1 -1
- package/dist/panel/protocol.d.ts.map +1 -1
- package/dist/panel/protocol.js +32 -23
- package/dist/panel/protocol.js.map +1 -1
- package/dist/panel/types.d.ts +26 -18
- package/dist/panel/types.d.ts.map +1 -1
- package/dist/panel/types.js.map +1 -1
- package/dist/preview/bounds.d.ts +1 -1
- package/dist/preview/bounds.d.ts.map +1 -1
- package/dist/preview/bounds.js +3 -3
- package/dist/preview/bounds.js.map +1 -1
- package/dist/preview/box-layout.d.ts +2 -2
- package/dist/preview/box-layout.js.map +1 -1
- package/dist/preview/colors.d.ts +1 -1
- package/dist/preview/colors.js +35 -35
- package/dist/preview/colors.js.map +1 -1
- package/dist/preview/hanging.d.ts +1 -1
- package/dist/preview/hanging.d.ts.map +1 -1
- package/dist/preview/hanging.js +4 -1
- package/dist/preview/hanging.js.map +1 -1
- package/dist/preview/junctions.d.ts +1 -1
- package/dist/preview/junctions.d.ts.map +1 -1
- package/dist/preview/junctions.js.map +1 -1
- package/dist/preview/label-layout.d.ts.map +1 -1
- package/dist/preview/label-layout.js +4 -4
- package/dist/preview/label-layout.js.map +1 -1
- package/dist/preview/ports.d.ts +1 -1
- package/dist/preview/ports.d.ts.map +1 -1
- package/dist/preview/ports.js +2 -1
- package/dist/preview/ports.js.map +1 -1
- package/dist/preview/renderable-wires.d.ts +1 -1
- package/dist/preview/renderable-wires.d.ts.map +1 -1
- package/dist/preview/renderable-wires.js +3 -1
- package/dist/preview/renderable-wires.js.map +1 -1
- package/dist/preview/routing.d.ts +1 -1
- package/dist/preview/routing.js +1 -1
- package/dist/preview/routing.js.map +1 -1
- package/dist/preview/snap.d.ts +1 -1
- package/dist/preview/snap.d.ts.map +1 -1
- package/dist/preview/snap.js +11 -3
- package/dist/preview/snap.js.map +1 -1
- package/dist/preview/symbols/svg-content.d.ts.map +1 -1
- package/dist/preview/symbols/svg-content.js +200 -50
- package/dist/preview/symbols/svg-content.js.map +1 -1
- package/dist/preview/symbols.d.ts +2 -2
- package/dist/preview/symbols.d.ts.map +1 -1
- package/dist/preview/symbols.js +100 -97
- package/dist/preview/symbols.js.map +1 -1
- package/dist/preview/wire-chains.d.ts +1 -1
- package/dist/preview/wire-chains.d.ts.map +1 -1
- package/dist/preview/wire-chains.js.map +1 -1
- package/dist/profiles.d.ts +600 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +118 -0
- package/dist/profiles.js.map +1 -0
- package/package.json +54 -54
package/dist/model/validation.js
CHANGED
|
@@ -1,93 +1,279 @@
|
|
|
1
|
-
import { propertyQuantityValue, propertyStringValue } from
|
|
2
|
-
import { extractPanel } from
|
|
3
|
-
const MODEL_ALIASES = [
|
|
1
|
+
import { propertyQuantityValue, propertyStringValue } from "./properties.js";
|
|
2
|
+
import { extractPanel } from "../panel/extract.js";
|
|
3
|
+
const MODEL_ALIASES = ["Model", "Type", "partNumber", "PartNumber"];
|
|
4
4
|
// Short source-type names (last dotted segment) that represent an "ideal" component variant —
|
|
5
5
|
// no model name is required because the component is a mathematical abstraction.
|
|
6
|
-
const IDEAL_SOURCE_TYPES = new Set([
|
|
6
|
+
const IDEAL_SOURCE_TYPES = new Set(["IdealOpAmp"]);
|
|
7
7
|
// Per-kind property names that, if present, satisfy the "needs a model" requirement.
|
|
8
8
|
// LiveSPICE stores tube Koren parameters and opamp small-signal parameters inline; when those
|
|
9
9
|
// are present, the parameters ARE the model definition and no separate model name is needed.
|
|
10
10
|
const INLINE_MODEL_PARAMETERS = {
|
|
11
|
-
opamp: [
|
|
12
|
-
triode: [
|
|
13
|
-
pentode: [
|
|
11
|
+
opamp: ["Rin", "Rout", "Aol", "GBP", "SupplyVoltage"],
|
|
12
|
+
triode: ["Mu", "K", "Kp", "Kvb", "Ex", "Kg"],
|
|
13
|
+
pentode: ["Mu", "K", "Kp", "Kvb", "Ex", "Kg", "Kg1", "Kg2"],
|
|
14
14
|
};
|
|
15
15
|
const RUNTIME_DESCRIPTOR_CONTROL_PROPERTIES = [
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
16
|
+
"TimeControl",
|
|
17
|
+
"FeedbackControl",
|
|
18
|
+
"MixControl",
|
|
19
|
+
"LevelControl",
|
|
20
|
+
"ToneControl",
|
|
21
|
+
"ModRateControl",
|
|
22
|
+
"ModDepthControl",
|
|
23
|
+
"ModeControl",
|
|
24
|
+
"TempoTapControl",
|
|
25
|
+
"TapTempoControl",
|
|
26
|
+
"TempoControl",
|
|
27
|
+
"DirectOutputJack",
|
|
28
|
+
"DirectOutJack",
|
|
29
|
+
"DirectOutputControl",
|
|
30
|
+
"DirectOutControl",
|
|
31
31
|
];
|
|
32
32
|
const KIND_RULES = {
|
|
33
|
-
resistor: [
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
min: 1e-9,
|
|
40
|
-
|
|
33
|
+
resistor: [
|
|
34
|
+
{
|
|
35
|
+
kind: "quantity",
|
|
36
|
+
name: "R",
|
|
37
|
+
required: true,
|
|
38
|
+
unit: "Ω",
|
|
39
|
+
min: 1e-9,
|
|
40
|
+
max: 1e9,
|
|
41
|
+
aliases: ["Resistance", "resistance", "r"],
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
"variable-resistor": [
|
|
45
|
+
{
|
|
46
|
+
kind: "quantity",
|
|
47
|
+
name: "R",
|
|
48
|
+
required: true,
|
|
49
|
+
unit: "Ω",
|
|
50
|
+
min: 1e-9,
|
|
51
|
+
max: 1e9,
|
|
52
|
+
aliases: ["Resistance", "resistance", "r"],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
41
55
|
potentiometer: [
|
|
42
56
|
{
|
|
43
|
-
kind:
|
|
44
|
-
|
|
57
|
+
kind: "quantity",
|
|
58
|
+
name: "R",
|
|
59
|
+
required: true,
|
|
60
|
+
unit: "Ω",
|
|
61
|
+
min: 1e-9,
|
|
62
|
+
max: 1e9,
|
|
63
|
+
aliases: ["Resistance", "totalResistance"],
|
|
64
|
+
},
|
|
65
|
+
{ kind: "string", name: "taper", required: false, aliases: ["Taper"] },
|
|
66
|
+
],
|
|
67
|
+
capacitor: [
|
|
68
|
+
{
|
|
69
|
+
kind: "quantity",
|
|
70
|
+
name: "C",
|
|
71
|
+
required: true,
|
|
72
|
+
unit: "F",
|
|
73
|
+
min: 1e-15,
|
|
74
|
+
max: 1,
|
|
75
|
+
aliases: ["Capacitance", "capacitance", "c"],
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
inductor: [
|
|
79
|
+
{
|
|
80
|
+
kind: "quantity",
|
|
81
|
+
name: "L",
|
|
82
|
+
required: true,
|
|
83
|
+
unit: "H",
|
|
84
|
+
min: 1e-12,
|
|
85
|
+
max: 100,
|
|
86
|
+
aliases: ["Inductance", "inductance", "l"],
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
"voltage-source": [
|
|
90
|
+
{
|
|
91
|
+
kind: "quantity",
|
|
92
|
+
name: "V",
|
|
93
|
+
required: true,
|
|
94
|
+
unit: "V",
|
|
95
|
+
aliases: ["Voltage", "voltage", "v"],
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
"current-source": [
|
|
99
|
+
{
|
|
100
|
+
kind: "quantity",
|
|
101
|
+
name: "I",
|
|
102
|
+
required: true,
|
|
103
|
+
unit: "A",
|
|
104
|
+
aliases: ["Current", "current", "i"],
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
battery: [
|
|
108
|
+
{
|
|
109
|
+
kind: "quantity",
|
|
110
|
+
name: "V",
|
|
111
|
+
required: true,
|
|
112
|
+
unit: "V",
|
|
113
|
+
aliases: ["Voltage", "voltage", "v"],
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
rail: [
|
|
117
|
+
{
|
|
118
|
+
kind: "quantity",
|
|
119
|
+
name: "V",
|
|
120
|
+
required: true,
|
|
121
|
+
unit: "V",
|
|
122
|
+
aliases: ["Voltage", "voltage", "v"],
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
diode: [
|
|
126
|
+
{
|
|
127
|
+
kind: "string",
|
|
128
|
+
name: "model",
|
|
129
|
+
required: true,
|
|
130
|
+
aliases: [...MODEL_ALIASES],
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
led: [
|
|
134
|
+
{
|
|
135
|
+
kind: "string",
|
|
136
|
+
name: "model",
|
|
137
|
+
required: true,
|
|
138
|
+
aliases: [...MODEL_ALIASES],
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
bjt: [
|
|
142
|
+
{
|
|
143
|
+
kind: "string",
|
|
144
|
+
name: "model",
|
|
145
|
+
required: true,
|
|
146
|
+
aliases: [...MODEL_ALIASES],
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
jfet: [
|
|
150
|
+
{
|
|
151
|
+
kind: "string",
|
|
152
|
+
name: "model",
|
|
153
|
+
required: true,
|
|
154
|
+
aliases: [...MODEL_ALIASES],
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
mosfet: [
|
|
158
|
+
{
|
|
159
|
+
kind: "string",
|
|
160
|
+
name: "model",
|
|
161
|
+
required: true,
|
|
162
|
+
aliases: [...MODEL_ALIASES],
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
opamp: [
|
|
166
|
+
{
|
|
167
|
+
kind: "string",
|
|
168
|
+
name: "model",
|
|
169
|
+
required: true,
|
|
170
|
+
aliases: [...MODEL_ALIASES],
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
triode: [
|
|
174
|
+
{
|
|
175
|
+
kind: "string",
|
|
176
|
+
name: "model",
|
|
177
|
+
required: true,
|
|
178
|
+
aliases: [...MODEL_ALIASES],
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
pentode: [
|
|
182
|
+
{
|
|
183
|
+
kind: "string",
|
|
184
|
+
name: "model",
|
|
185
|
+
required: true,
|
|
186
|
+
aliases: [...MODEL_ALIASES],
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
"tube-diode": [
|
|
190
|
+
{
|
|
191
|
+
kind: "string",
|
|
192
|
+
name: "model",
|
|
193
|
+
required: true,
|
|
194
|
+
aliases: [...MODEL_ALIASES],
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
optocoupler: [
|
|
198
|
+
{
|
|
199
|
+
kind: "string",
|
|
200
|
+
name: "model",
|
|
201
|
+
required: true,
|
|
202
|
+
aliases: [...MODEL_ALIASES],
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
transformer: [
|
|
206
|
+
{
|
|
207
|
+
kind: "string",
|
|
208
|
+
name: "model",
|
|
209
|
+
required: false,
|
|
210
|
+
aliases: [...MODEL_ALIASES],
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
ota: [
|
|
214
|
+
{
|
|
215
|
+
kind: "string",
|
|
216
|
+
name: "model",
|
|
217
|
+
required: true,
|
|
218
|
+
aliases: [...MODEL_ALIASES],
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
bbd: [
|
|
222
|
+
{
|
|
223
|
+
kind: "string",
|
|
224
|
+
name: "model",
|
|
225
|
+
required: true,
|
|
226
|
+
aliases: [...MODEL_ALIASES],
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
"delay-ic": [
|
|
230
|
+
{
|
|
231
|
+
kind: "string",
|
|
232
|
+
name: "model",
|
|
233
|
+
required: true,
|
|
234
|
+
aliases: [...MODEL_ALIASES],
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
"power-amp": [
|
|
238
|
+
{
|
|
239
|
+
kind: "string",
|
|
240
|
+
name: "model",
|
|
241
|
+
required: true,
|
|
242
|
+
aliases: [...MODEL_ALIASES],
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
regulator: [
|
|
246
|
+
{
|
|
247
|
+
kind: "string",
|
|
248
|
+
name: "model",
|
|
249
|
+
required: true,
|
|
250
|
+
aliases: [...MODEL_ALIASES],
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
"analog-switch": [
|
|
254
|
+
{
|
|
255
|
+
kind: "string",
|
|
256
|
+
name: "model",
|
|
257
|
+
required: true,
|
|
258
|
+
aliases: [...MODEL_ALIASES],
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
flipflop: [
|
|
262
|
+
{
|
|
263
|
+
kind: "string",
|
|
264
|
+
name: "model",
|
|
265
|
+
required: true,
|
|
266
|
+
aliases: [...MODEL_ALIASES],
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
ic: [
|
|
270
|
+
{
|
|
271
|
+
kind: "string",
|
|
272
|
+
name: "model",
|
|
273
|
+
required: true,
|
|
274
|
+
aliases: [...MODEL_ALIASES],
|
|
45
275
|
},
|
|
46
|
-
{ kind: 'string', name: 'taper', required: false, aliases: ['Taper'] },
|
|
47
276
|
],
|
|
48
|
-
capacitor: [{
|
|
49
|
-
kind: 'quantity', name: 'C', required: true, unit: 'F',
|
|
50
|
-
min: 1e-15, max: 1, aliases: ['Capacitance', 'capacitance', 'c'],
|
|
51
|
-
}],
|
|
52
|
-
inductor: [{
|
|
53
|
-
kind: 'quantity', name: 'L', required: true, unit: 'H',
|
|
54
|
-
min: 1e-12, max: 100, aliases: ['Inductance', 'inductance', 'l'],
|
|
55
|
-
}],
|
|
56
|
-
'voltage-source': [{
|
|
57
|
-
kind: 'quantity', name: 'V', required: true, unit: 'V',
|
|
58
|
-
aliases: ['Voltage', 'voltage', 'v'],
|
|
59
|
-
}],
|
|
60
|
-
'current-source': [{
|
|
61
|
-
kind: 'quantity', name: 'I', required: true, unit: 'A',
|
|
62
|
-
aliases: ['Current', 'current', 'i'],
|
|
63
|
-
}],
|
|
64
|
-
battery: [{
|
|
65
|
-
kind: 'quantity', name: 'V', required: true, unit: 'V',
|
|
66
|
-
aliases: ['Voltage', 'voltage', 'v'],
|
|
67
|
-
}],
|
|
68
|
-
rail: [{
|
|
69
|
-
kind: 'quantity', name: 'V', required: true, unit: 'V',
|
|
70
|
-
aliases: ['Voltage', 'voltage', 'v'],
|
|
71
|
-
}],
|
|
72
|
-
diode: [{ kind: 'string', name: 'model', required: true, aliases: [...MODEL_ALIASES] }],
|
|
73
|
-
led: [{ kind: 'string', name: 'model', required: true, aliases: [...MODEL_ALIASES] }],
|
|
74
|
-
bjt: [{ kind: 'string', name: 'model', required: true, aliases: [...MODEL_ALIASES] }],
|
|
75
|
-
jfet: [{ kind: 'string', name: 'model', required: true, aliases: [...MODEL_ALIASES] }],
|
|
76
|
-
mosfet: [{ kind: 'string', name: 'model', required: true, aliases: [...MODEL_ALIASES] }],
|
|
77
|
-
opamp: [{ kind: 'string', name: 'model', required: true, aliases: [...MODEL_ALIASES] }],
|
|
78
|
-
triode: [{ kind: 'string', name: 'model', required: true, aliases: [...MODEL_ALIASES] }],
|
|
79
|
-
pentode: [{ kind: 'string', name: 'model', required: true, aliases: [...MODEL_ALIASES] }],
|
|
80
|
-
'tube-diode': [{ kind: 'string', name: 'model', required: true, aliases: [...MODEL_ALIASES] }],
|
|
81
|
-
optocoupler: [{ kind: 'string', name: 'model', required: true, aliases: [...MODEL_ALIASES] }],
|
|
82
|
-
transformer: [{ kind: 'string', name: 'model', required: false, aliases: [...MODEL_ALIASES] }],
|
|
83
|
-
ota: [{ kind: 'string', name: 'model', required: true, aliases: [...MODEL_ALIASES] }],
|
|
84
|
-
bbd: [{ kind: 'string', name: 'model', required: true, aliases: [...MODEL_ALIASES] }],
|
|
85
|
-
'delay-ic': [{ kind: 'string', name: 'model', required: true, aliases: [...MODEL_ALIASES] }],
|
|
86
|
-
'power-amp': [{ kind: 'string', name: 'model', required: true, aliases: [...MODEL_ALIASES] }],
|
|
87
|
-
regulator: [{ kind: 'string', name: 'model', required: true, aliases: [...MODEL_ALIASES] }],
|
|
88
|
-
'analog-switch': [{ kind: 'string', name: 'model', required: true, aliases: [...MODEL_ALIASES] }],
|
|
89
|
-
flipflop: [{ kind: 'string', name: 'model', required: true, aliases: [...MODEL_ALIASES] }],
|
|
90
|
-
ic: [{ kind: 'string', name: 'model', required: true, aliases: [...MODEL_ALIASES] }],
|
|
91
277
|
};
|
|
92
278
|
export function getRulesForKind(kind) {
|
|
93
279
|
return KIND_RULES[kind] ?? [];
|
|
@@ -102,8 +288,8 @@ export function validateComponent(component, rules = getRulesForKind(component.k
|
|
|
102
288
|
}
|
|
103
289
|
continue;
|
|
104
290
|
}
|
|
105
|
-
if (rule.kind ===
|
|
106
|
-
if (typeof value !==
|
|
291
|
+
if (rule.kind === "string") {
|
|
292
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
107
293
|
if (rule.required && !isRequirementWaived(component, rule)) {
|
|
108
294
|
issues.push(missingPropertyIssue(component, rule));
|
|
109
295
|
}
|
|
@@ -112,22 +298,25 @@ export function validateComponent(component, rules = getRulesForKind(component.k
|
|
|
112
298
|
}
|
|
113
299
|
const quantity = coerceQuantity(value);
|
|
114
300
|
if (quantity === null) {
|
|
115
|
-
if (typeof value ===
|
|
301
|
+
if (typeof value === "string" && isRawQuantityExpression(value)) {
|
|
116
302
|
continue;
|
|
117
303
|
}
|
|
118
304
|
issues.push({
|
|
119
|
-
code:
|
|
120
|
-
severity:
|
|
305
|
+
code: "value-unparseable",
|
|
306
|
+
severity: "error",
|
|
121
307
|
message: `${component.id}: property "${rule.name}" could not be parsed as a quantity`,
|
|
122
308
|
componentId: component.id,
|
|
123
309
|
property: rule.name,
|
|
124
310
|
});
|
|
125
311
|
continue;
|
|
126
312
|
}
|
|
127
|
-
if (rule.unit !== undefined &&
|
|
313
|
+
if (rule.unit !== undefined &&
|
|
314
|
+
rule.unit.length > 0 &&
|
|
315
|
+
quantity.unit.length > 0 &&
|
|
316
|
+
quantity.unit !== rule.unit) {
|
|
128
317
|
issues.push({
|
|
129
|
-
code:
|
|
130
|
-
severity:
|
|
318
|
+
code: "unit-mismatch",
|
|
319
|
+
severity: "warning",
|
|
131
320
|
message: `${component.id}: property "${rule.name}" has unit "${quantity.unit}" but expected "${rule.unit}"`,
|
|
132
321
|
componentId: component.id,
|
|
133
322
|
property: rule.name,
|
|
@@ -135,8 +324,8 @@ export function validateComponent(component, rules = getRulesForKind(component.k
|
|
|
135
324
|
}
|
|
136
325
|
if (rule.min !== undefined && quantity.value < rule.min) {
|
|
137
326
|
issues.push({
|
|
138
|
-
code:
|
|
139
|
-
severity:
|
|
327
|
+
code: "value-out-of-range",
|
|
328
|
+
severity: "error",
|
|
140
329
|
message: `${component.id}: property "${rule.name}" value ${quantity.value} is below minimum ${rule.min}`,
|
|
141
330
|
componentId: component.id,
|
|
142
331
|
property: rule.name,
|
|
@@ -144,8 +333,8 @@ export function validateComponent(component, rules = getRulesForKind(component.k
|
|
|
144
333
|
}
|
|
145
334
|
if (rule.max !== undefined && quantity.value > rule.max) {
|
|
146
335
|
issues.push({
|
|
147
|
-
code:
|
|
148
|
-
severity:
|
|
336
|
+
code: "value-out-of-range",
|
|
337
|
+
severity: "error",
|
|
149
338
|
message: `${component.id}: property "${rule.name}" value ${quantity.value} is above maximum ${rule.max}`,
|
|
150
339
|
componentId: component.id,
|
|
151
340
|
property: rule.name,
|
|
@@ -160,18 +349,18 @@ export function validateDocument(doc) {
|
|
|
160
349
|
for (const component of doc.components) {
|
|
161
350
|
if (seen.has(component.id)) {
|
|
162
351
|
issues.push({
|
|
163
|
-
code:
|
|
164
|
-
severity:
|
|
352
|
+
code: "duplicate-id",
|
|
353
|
+
severity: "error",
|
|
165
354
|
message: `Duplicate component id "${component.id}"`,
|
|
166
355
|
componentId: component.id,
|
|
167
356
|
});
|
|
168
357
|
}
|
|
169
358
|
seen.add(component.id);
|
|
170
|
-
if (component.kind ===
|
|
359
|
+
if (component.kind === "unsupported") {
|
|
171
360
|
issues.push({
|
|
172
|
-
code:
|
|
173
|
-
severity:
|
|
174
|
-
message: `${component.id}: unsupported source type ${component.sourceTypeName ??
|
|
361
|
+
code: "unsupported-component",
|
|
362
|
+
severity: "warning",
|
|
363
|
+
message: `${component.id}: unsupported source type ${component.sourceTypeName ?? "unknown"}`,
|
|
175
364
|
componentId: component.id,
|
|
176
365
|
});
|
|
177
366
|
continue;
|
|
@@ -187,8 +376,8 @@ export function validateDocument(doc) {
|
|
|
187
376
|
const [a, b] = wire.endpoints;
|
|
188
377
|
if (a.x === b.x && a.y === b.y) {
|
|
189
378
|
issues.push({
|
|
190
|
-
code:
|
|
191
|
-
severity:
|
|
379
|
+
code: "degenerate-wire",
|
|
380
|
+
severity: "warning",
|
|
192
381
|
message: `Wire "${wire.id}" has identical endpoints`,
|
|
193
382
|
wireId: wire.id,
|
|
194
383
|
});
|
|
@@ -200,23 +389,27 @@ export function validateDocument(doc) {
|
|
|
200
389
|
for (const issue of validatePanel(doc, seen, new Set(doc.deviceInterface?.controls.map((control) => control.id) ?? []))) {
|
|
201
390
|
issues.push(issue);
|
|
202
391
|
}
|
|
392
|
+
for (const issue of validateMountGroups(doc)) {
|
|
393
|
+
issues.push(issue);
|
|
394
|
+
}
|
|
203
395
|
for (const issue of validateV3BuildMetadata(doc, seen)) {
|
|
204
396
|
issues.push(issue);
|
|
205
397
|
}
|
|
206
398
|
return issues;
|
|
207
399
|
}
|
|
208
400
|
export function hasErrors(issues) {
|
|
209
|
-
return issues.some((issue) => issue.severity ===
|
|
401
|
+
return issues.some((issue) => issue.severity === "error");
|
|
210
402
|
}
|
|
211
403
|
function isRequirementWaived(component, rule) {
|
|
212
404
|
if (isInterfaceOnlyComponent(component)) {
|
|
213
405
|
return true;
|
|
214
406
|
}
|
|
215
407
|
// Only the "model" string requirement has a waiver path today.
|
|
216
|
-
if (rule.kind !==
|
|
408
|
+
if (rule.kind !== "string" || rule.name !== "model") {
|
|
217
409
|
return false;
|
|
218
410
|
}
|
|
219
|
-
if (component.kind ===
|
|
411
|
+
if (component.kind === "ic" &&
|
|
412
|
+
component.properties.RuntimeDescriptor === "true") {
|
|
220
413
|
return true;
|
|
221
414
|
}
|
|
222
415
|
const shortType = shortSourceType(component.sourceTypeName);
|
|
@@ -231,54 +424,60 @@ function isInterfaceOnlyComponent(component) {
|
|
|
231
424
|
if (interfaceOnly === true) {
|
|
232
425
|
return true;
|
|
233
426
|
}
|
|
234
|
-
if (typeof interfaceOnly ===
|
|
427
|
+
if (typeof interfaceOnly === "string" &&
|
|
428
|
+
normalizeToken(interfaceOnly) === "true") {
|
|
235
429
|
return true;
|
|
236
430
|
}
|
|
237
431
|
const support = component.properties.Support;
|
|
238
|
-
return typeof support ===
|
|
432
|
+
return typeof support === "string" && normalizeToken(support) === "view-only";
|
|
239
433
|
}
|
|
240
434
|
function validateSemanticMetadata(component) {
|
|
241
435
|
const issues = [];
|
|
242
|
-
if (component.kind ===
|
|
436
|
+
if (component.kind === "jack") {
|
|
243
437
|
issues.push(...validateJackSemanticMetadata(component));
|
|
244
438
|
}
|
|
245
|
-
if (component.kind ===
|
|
439
|
+
if (component.kind === "ic" &&
|
|
440
|
+
component.properties.RuntimeDescriptor === "true") {
|
|
246
441
|
issues.push(...validateRuntimeDescriptorMetadata(component));
|
|
247
442
|
}
|
|
248
443
|
return issues;
|
|
249
444
|
}
|
|
250
445
|
function validateJackSemanticMetadata(component) {
|
|
251
446
|
const issues = [];
|
|
252
|
-
for (const property of [
|
|
447
|
+
for (const property of ["Role", "ControlRole"]) {
|
|
253
448
|
const value = propertyString(component, property);
|
|
254
|
-
if (value !== null &&
|
|
449
|
+
if (value !== null &&
|
|
450
|
+
value.trim().length > 0 &&
|
|
451
|
+
!isRecognizedJackRole(value)) {
|
|
255
452
|
issues.push({
|
|
256
|
-
code:
|
|
257
|
-
severity:
|
|
453
|
+
code: "invalid-jack-role",
|
|
454
|
+
severity: "warning",
|
|
258
455
|
message: `${component.id}: jack ${property} "${value}" is not a recognized panel role`,
|
|
259
456
|
componentId: component.id,
|
|
260
457
|
property,
|
|
261
458
|
});
|
|
262
459
|
}
|
|
263
460
|
}
|
|
264
|
-
const interfaceName = propertyString(component,
|
|
265
|
-
if (interfaceName !== null &&
|
|
461
|
+
const interfaceName = propertyString(component, "Interface");
|
|
462
|
+
if (interfaceName !== null &&
|
|
463
|
+
interfaceName.trim().length > 0 &&
|
|
464
|
+
!isRecognizedJackInterface(interfaceName)) {
|
|
266
465
|
issues.push({
|
|
267
|
-
code:
|
|
268
|
-
severity:
|
|
466
|
+
code: "invalid-jack-interface",
|
|
467
|
+
severity: "warning",
|
|
269
468
|
message: `${component.id}: jack Interface "${interfaceName}" is not a recognized panel interface`,
|
|
270
469
|
componentId: component.id,
|
|
271
|
-
property:
|
|
470
|
+
property: "Interface",
|
|
272
471
|
});
|
|
273
472
|
}
|
|
274
|
-
const audioRole = propertyString(component,
|
|
473
|
+
const audioRole = propertyString(component, "AudioRole");
|
|
275
474
|
if (audioRole !== null && !isValidJackAudioRole(audioRole)) {
|
|
276
475
|
issues.push({
|
|
277
|
-
code:
|
|
278
|
-
severity:
|
|
476
|
+
code: "invalid-jack-audio-role",
|
|
477
|
+
severity: "warning",
|
|
279
478
|
message: `${component.id}: jack AudioRole "${audioRole}" must be a lower-kebab source subtype slug`,
|
|
280
479
|
componentId: component.id,
|
|
281
|
-
property:
|
|
480
|
+
property: "AudioRole",
|
|
282
481
|
});
|
|
283
482
|
}
|
|
284
483
|
return issues;
|
|
@@ -289,23 +488,25 @@ function validateRuntimeDescriptorMetadata(component) {
|
|
|
289
488
|
const value = propertyString(component, property);
|
|
290
489
|
if (value !== null && value.trim().length === 0) {
|
|
291
490
|
issues.push({
|
|
292
|
-
code:
|
|
293
|
-
severity:
|
|
491
|
+
code: "descriptor-control-empty",
|
|
492
|
+
severity: "warning",
|
|
294
493
|
message: `${component.id}: runtime descriptor property "${property}" must not be empty`,
|
|
295
494
|
componentId: component.id,
|
|
296
495
|
property,
|
|
297
496
|
});
|
|
298
497
|
}
|
|
299
498
|
}
|
|
300
|
-
const labels = parseStringList(propertyStringAny(component, [
|
|
301
|
-
const stepCount = parsePositiveInteger(propertyStringAny(component, [
|
|
302
|
-
if (labels.length > 0 &&
|
|
499
|
+
const labels = parseStringList(propertyStringAny(component, ["ModeLabels", "ModeOptions"]));
|
|
500
|
+
const stepCount = parsePositiveInteger(propertyStringAny(component, ["ModeStepCount", "ModeSteps", "ModeCount"]));
|
|
501
|
+
if (labels.length > 0 &&
|
|
502
|
+
stepCount !== undefined &&
|
|
503
|
+
labels.length !== stepCount) {
|
|
303
504
|
issues.push({
|
|
304
|
-
code:
|
|
305
|
-
severity:
|
|
505
|
+
code: "descriptor-mode-label-mismatch",
|
|
506
|
+
severity: "warning",
|
|
306
507
|
message: `${component.id}: ModeLabels has ${labels.length} labels but ModeStepCount is ${stepCount}`,
|
|
307
508
|
componentId: component.id,
|
|
308
|
-
property:
|
|
509
|
+
property: "ModeLabels",
|
|
309
510
|
});
|
|
310
511
|
}
|
|
311
512
|
return issues;
|
|
@@ -314,11 +515,11 @@ function shortSourceType(sourceTypeName) {
|
|
|
314
515
|
if (sourceTypeName === null) {
|
|
315
516
|
return null;
|
|
316
517
|
}
|
|
317
|
-
const head = sourceTypeName.split(
|
|
518
|
+
const head = sourceTypeName.split(",")[0]?.trim() ?? "";
|
|
318
519
|
if (head.length === 0) {
|
|
319
520
|
return null;
|
|
320
521
|
}
|
|
321
|
-
const lastDot = head.lastIndexOf(
|
|
522
|
+
const lastDot = head.lastIndexOf(".");
|
|
322
523
|
return lastDot >= 0 ? head.slice(lastDot + 1) : head;
|
|
323
524
|
}
|
|
324
525
|
function findProperty(component, rule) {
|
|
@@ -351,58 +552,58 @@ function isRawQuantityExpression(value) {
|
|
|
351
552
|
if (trimmed.length === 0) {
|
|
352
553
|
return false;
|
|
353
554
|
}
|
|
354
|
-
if (trimmed.startsWith(
|
|
555
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
355
556
|
return true;
|
|
356
557
|
}
|
|
357
|
-
return /^(AC|DC)\b/i.test(trimmed) ||
|
|
358
|
-
/^(SINE|PULSE|PWL|EXP|SFFM|AM|WAVEFILE)\s*\(/i.test(trimmed);
|
|
558
|
+
return (/^(AC|DC)\b/i.test(trimmed) ||
|
|
559
|
+
/^(SINE|PULSE|PWL|EXP|SFFM|AM|WAVEFILE)\s*\(/i.test(trimmed));
|
|
359
560
|
}
|
|
360
561
|
function isRecognizedJackRole(value) {
|
|
361
562
|
const normalized = normalizeToken(value);
|
|
362
563
|
return [
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
564
|
+
"input",
|
|
565
|
+
"audio-input",
|
|
566
|
+
"in",
|
|
567
|
+
"direct-output",
|
|
568
|
+
"direct-out",
|
|
569
|
+
"dry-output",
|
|
570
|
+
"dry-out",
|
|
571
|
+
"output",
|
|
572
|
+
"audio-output",
|
|
573
|
+
"out",
|
|
574
|
+
"send",
|
|
575
|
+
"return",
|
|
576
|
+
"expression",
|
|
577
|
+
"exp",
|
|
578
|
+
"expression-pedal",
|
|
579
|
+
"tempo-tap",
|
|
580
|
+
"tap-tempo",
|
|
581
|
+
"tempo-in",
|
|
582
|
+
"tap",
|
|
583
|
+
"tempo",
|
|
584
|
+
"external-control",
|
|
585
|
+
"external-control-input",
|
|
586
|
+
"control-input",
|
|
587
|
+
"remote",
|
|
588
|
+
"footswitch",
|
|
589
|
+
"trigger",
|
|
590
|
+
"reset",
|
|
390
591
|
].includes(normalized);
|
|
391
592
|
}
|
|
392
593
|
function isRecognizedJackInterface(value) {
|
|
393
594
|
const normalized = normalizeToken(value);
|
|
394
|
-
return isRecognizedJackRole(value) ||
|
|
595
|
+
return (isRecognizedJackRole(value) ||
|
|
395
596
|
[
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
].includes(normalized);
|
|
597
|
+
"audio",
|
|
598
|
+
"audio-port",
|
|
599
|
+
"control",
|
|
600
|
+
"control-port",
|
|
601
|
+
"power",
|
|
602
|
+
"power-port",
|
|
603
|
+
"dc-power",
|
|
604
|
+
"dc-power-input",
|
|
605
|
+
"tap-tempo-input",
|
|
606
|
+
].includes(normalized));
|
|
406
607
|
}
|
|
407
608
|
function isValidJackAudioRole(value) {
|
|
408
609
|
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
|
|
@@ -428,51 +629,56 @@ function parsePositiveInteger(value) {
|
|
|
428
629
|
return Number.isInteger(count) && count > 0 ? count : undefined;
|
|
429
630
|
}
|
|
430
631
|
function normalizeToken(value) {
|
|
431
|
-
return value
|
|
632
|
+
return value
|
|
633
|
+
.trim()
|
|
634
|
+
.toLowerCase()
|
|
635
|
+
.replace(/[\s_]+/g, "-");
|
|
432
636
|
}
|
|
433
637
|
function validateDeviceInterface(doc, componentIds) {
|
|
434
638
|
const issues = [];
|
|
435
639
|
const groupIds = new Set(doc.controlGroups?.map((group) => group.id) ?? []);
|
|
436
640
|
const contextIds = new Set(doc.controlContexts?.map((context) => context.id) ?? []);
|
|
641
|
+
const declaredControlIds = new Set(doc.deviceInterface?.controls.map((control) => control.id) ?? []);
|
|
437
642
|
const semanticControlIds = new Set();
|
|
438
643
|
const externalInterfaceIds = new Set(doc.controlInterfaces?.map((controlInterface) => controlInterface.id) ?? []);
|
|
439
644
|
const componentsById = new Map(doc.components.map((component) => [component.id, component]));
|
|
440
645
|
const resolvedPanelElements = resolvePanelElements(doc);
|
|
441
646
|
for (const group of doc.controlGroups ?? []) {
|
|
442
|
-
issues.push(...validateOpenToken(group.role, group.id,
|
|
647
|
+
issues.push(...validateOpenToken(group.role, group.id, "role"));
|
|
443
648
|
for (const contextId of group.contextIds ?? []) {
|
|
444
649
|
if (!contextIds.has(contextId)) {
|
|
445
650
|
issues.push({
|
|
446
|
-
code:
|
|
447
|
-
severity:
|
|
651
|
+
code: "control-group-context-unresolved",
|
|
652
|
+
severity: "warning",
|
|
448
653
|
message: `Control group "${group.id}" references missing context "${contextId}"`,
|
|
449
654
|
componentId: group.id,
|
|
450
|
-
property:
|
|
655
|
+
property: "contextIds",
|
|
451
656
|
});
|
|
452
657
|
}
|
|
453
658
|
}
|
|
659
|
+
issues.push(...validateControlGroupMembers(group, declaredControlIds, contextIds));
|
|
454
660
|
}
|
|
455
661
|
for (const context of doc.controlContexts ?? []) {
|
|
456
|
-
issues.push(...validateOpenToken(context.role, context.id,
|
|
662
|
+
issues.push(...validateOpenToken(context.role, context.id, "role"));
|
|
457
663
|
}
|
|
458
664
|
for (const control of doc.deviceInterface?.controls ?? []) {
|
|
459
665
|
if (semanticControlIds.has(control.id)) {
|
|
460
666
|
issues.push({
|
|
461
|
-
code:
|
|
462
|
-
severity:
|
|
667
|
+
code: "duplicate-device-interface-control-id",
|
|
668
|
+
severity: "error",
|
|
463
669
|
message: `Duplicate device interface control id "${control.id}"`,
|
|
464
670
|
componentId: control.id,
|
|
465
671
|
});
|
|
466
672
|
}
|
|
467
673
|
semanticControlIds.add(control.id);
|
|
468
|
-
issues.push(...validateOpenToken(control.role, control.id,
|
|
674
|
+
issues.push(...validateOpenToken(control.role, control.id, "role"));
|
|
469
675
|
if (control.groupId !== undefined && !groupIds.has(control.groupId)) {
|
|
470
676
|
issues.push({
|
|
471
|
-
code:
|
|
472
|
-
severity:
|
|
677
|
+
code: "device-interface-group-unresolved",
|
|
678
|
+
severity: "warning",
|
|
473
679
|
message: `Device interface control "${control.id}" references missing group "${control.groupId}"`,
|
|
474
680
|
componentId: control.id,
|
|
475
|
-
property:
|
|
681
|
+
property: "groupId",
|
|
476
682
|
});
|
|
477
683
|
}
|
|
478
684
|
issues.push(...validateApplicability(control, contextIds));
|
|
@@ -480,37 +686,129 @@ function validateDeviceInterface(doc, componentIds) {
|
|
|
480
686
|
issues.push(...validateDeviceInterfaceBinding(control, control.binding, componentIds, externalInterfaceIds, componentsById, resolvedPanelElements));
|
|
481
687
|
}
|
|
482
688
|
}
|
|
483
|
-
issues.push(...validateDuplicateDeviceInterfaceRoles(doc.deviceInterface?.controls ?? []));
|
|
689
|
+
issues.push(...validateDuplicateDeviceInterfaceRoles(doc.deviceInterface?.controls ?? [], doc.controlGroups ?? []));
|
|
690
|
+
return issues;
|
|
691
|
+
}
|
|
692
|
+
function validateControlGroupMembers(group, controlIds, contextIds) {
|
|
693
|
+
const issues = [];
|
|
694
|
+
const orderOwners = new Map();
|
|
695
|
+
for (const member of group.members ?? []) {
|
|
696
|
+
if (!controlIds.has(member.controlId)) {
|
|
697
|
+
issues.push({
|
|
698
|
+
code: "control-group-member-unresolved",
|
|
699
|
+
severity: "warning",
|
|
700
|
+
message: `Control group "${group.id}" references missing member control "${member.controlId}"`,
|
|
701
|
+
componentId: group.id,
|
|
702
|
+
property: "members.controlId",
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
if (member.order !== undefined) {
|
|
706
|
+
const existingControlId = orderOwners.get(member.order);
|
|
707
|
+
if (existingControlId !== undefined) {
|
|
708
|
+
issues.push({
|
|
709
|
+
code: "control-group-member-order-duplicate",
|
|
710
|
+
severity: "warning",
|
|
711
|
+
message: `Control group "${group.id}" assigns order ${member.order} to both "${existingControlId}" and "${member.controlId}"`,
|
|
712
|
+
componentId: group.id,
|
|
713
|
+
property: "members.order",
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
orderOwners.set(member.order, member.controlId);
|
|
717
|
+
}
|
|
718
|
+
issues.push(...validateControlGroupMemberApplicability(group.id, member, contextIds));
|
|
719
|
+
}
|
|
720
|
+
return issues;
|
|
721
|
+
}
|
|
722
|
+
function validateControlGroupMemberApplicability(groupId, member, contextIds) {
|
|
723
|
+
const issues = [];
|
|
724
|
+
if (member.appliesWhen === undefined) {
|
|
725
|
+
return issues;
|
|
726
|
+
}
|
|
727
|
+
issues.push(...validateGroupMemberContextList(groupId, member, "members.appliesWhen.allOf", member.appliesWhen.allOf, contextIds));
|
|
728
|
+
issues.push(...validateGroupMemberContextList(groupId, member, "members.appliesWhen.anyOf", member.appliesWhen.anyOf, contextIds));
|
|
729
|
+
if (member.appliesWhen.allOf !== undefined &&
|
|
730
|
+
member.appliesWhen.allOf.length === 0 &&
|
|
731
|
+
member.appliesWhen.anyOf === undefined) {
|
|
732
|
+
issues.push(emptyGroupMemberApplicabilityIssue(groupId, member.controlId, "members.appliesWhen.allOf"));
|
|
733
|
+
}
|
|
734
|
+
if (member.appliesWhen.anyOf !== undefined &&
|
|
735
|
+
member.appliesWhen.anyOf.length === 0 &&
|
|
736
|
+
member.appliesWhen.allOf === undefined) {
|
|
737
|
+
issues.push(emptyGroupMemberApplicabilityIssue(groupId, member.controlId, "members.appliesWhen.anyOf"));
|
|
738
|
+
}
|
|
739
|
+
return issues;
|
|
740
|
+
}
|
|
741
|
+
function validateGroupMemberContextList(groupId, member, property, values, contextIds) {
|
|
742
|
+
if (values === undefined) {
|
|
743
|
+
return [];
|
|
744
|
+
}
|
|
745
|
+
const issues = [];
|
|
746
|
+
const seen = new Set();
|
|
747
|
+
if (values.length === 0) {
|
|
748
|
+
issues.push(emptyGroupMemberApplicabilityIssue(groupId, member.controlId, property));
|
|
749
|
+
}
|
|
750
|
+
for (const contextId of values) {
|
|
751
|
+
if (seen.has(contextId)) {
|
|
752
|
+
issues.push({
|
|
753
|
+
code: "control-group-member-context-unresolved",
|
|
754
|
+
severity: "warning",
|
|
755
|
+
message: `Control group "${groupId}" member "${member.controlId}" repeats context "${contextId}" in ${property}`,
|
|
756
|
+
componentId: groupId,
|
|
757
|
+
property,
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
seen.add(contextId);
|
|
761
|
+
if (!contextIds.has(contextId)) {
|
|
762
|
+
issues.push({
|
|
763
|
+
code: "control-group-member-context-unresolved",
|
|
764
|
+
severity: "warning",
|
|
765
|
+
message: `Control group "${groupId}" member "${member.controlId}" references missing context "${contextId}"`,
|
|
766
|
+
componentId: groupId,
|
|
767
|
+
property,
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
}
|
|
484
771
|
return issues;
|
|
485
772
|
}
|
|
773
|
+
function emptyGroupMemberApplicabilityIssue(groupId, controlId, property) {
|
|
774
|
+
return {
|
|
775
|
+
code: "control-group-member-context-unresolved",
|
|
776
|
+
severity: "warning",
|
|
777
|
+
message: `Control group "${groupId}" member "${controlId}" has empty ${property}; omit the predicate instead`,
|
|
778
|
+
componentId: groupId,
|
|
779
|
+
property,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
486
782
|
function validateOpenToken(value, componentId, property) {
|
|
487
783
|
if (/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(value)) {
|
|
488
784
|
return [];
|
|
489
785
|
}
|
|
490
|
-
return [
|
|
491
|
-
|
|
492
|
-
|
|
786
|
+
return [
|
|
787
|
+
{
|
|
788
|
+
code: "invalid-device-interface-token",
|
|
789
|
+
severity: "warning",
|
|
493
790
|
message: `${componentId}: ${property} "${value}" must be a lower-kebab token`,
|
|
494
791
|
componentId,
|
|
495
792
|
property,
|
|
496
|
-
}
|
|
793
|
+
},
|
|
794
|
+
];
|
|
497
795
|
}
|
|
498
796
|
function validateApplicability(control, contextIds) {
|
|
499
797
|
const issues = [];
|
|
500
798
|
if (control.appliesWhen === undefined) {
|
|
501
799
|
return issues;
|
|
502
800
|
}
|
|
503
|
-
issues.push(...validateContextList(control.id,
|
|
504
|
-
issues.push(...validateContextList(control.id,
|
|
505
|
-
if (control.appliesWhen.allOf !== undefined
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
issues.push(emptyApplicabilityIssue(control.id,
|
|
801
|
+
issues.push(...validateContextList(control.id, "appliesWhen.allOf", control.appliesWhen.allOf, contextIds));
|
|
802
|
+
issues.push(...validateContextList(control.id, "appliesWhen.anyOf", control.appliesWhen.anyOf, contextIds));
|
|
803
|
+
if (control.appliesWhen.allOf !== undefined &&
|
|
804
|
+
control.appliesWhen.allOf.length === 0 &&
|
|
805
|
+
control.appliesWhen.anyOf === undefined) {
|
|
806
|
+
issues.push(emptyApplicabilityIssue(control.id, "appliesWhen.allOf"));
|
|
509
807
|
}
|
|
510
|
-
if (control.appliesWhen.anyOf !== undefined
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
issues.push(emptyApplicabilityIssue(control.id,
|
|
808
|
+
if (control.appliesWhen.anyOf !== undefined &&
|
|
809
|
+
control.appliesWhen.anyOf.length === 0 &&
|
|
810
|
+
control.appliesWhen.allOf === undefined) {
|
|
811
|
+
issues.push(emptyApplicabilityIssue(control.id, "appliesWhen.anyOf"));
|
|
514
812
|
}
|
|
515
813
|
return issues;
|
|
516
814
|
}
|
|
@@ -526,8 +824,8 @@ function validateContextList(controlId, property, values, contextIds) {
|
|
|
526
824
|
for (const contextId of values) {
|
|
527
825
|
if (seen.has(contextId)) {
|
|
528
826
|
issues.push({
|
|
529
|
-
code:
|
|
530
|
-
severity:
|
|
827
|
+
code: "device-interface-context-unresolved",
|
|
828
|
+
severity: "warning",
|
|
531
829
|
message: `Device interface control "${controlId}" repeats context "${contextId}" in ${property}`,
|
|
532
830
|
componentId: controlId,
|
|
533
831
|
property,
|
|
@@ -536,8 +834,8 @@ function validateContextList(controlId, property, values, contextIds) {
|
|
|
536
834
|
seen.add(contextId);
|
|
537
835
|
if (!contextIds.has(contextId)) {
|
|
538
836
|
issues.push({
|
|
539
|
-
code:
|
|
540
|
-
severity:
|
|
837
|
+
code: "device-interface-context-unresolved",
|
|
838
|
+
severity: "warning",
|
|
541
839
|
message: `Device interface control "${controlId}" references missing context "${contextId}"`,
|
|
542
840
|
componentId: controlId,
|
|
543
841
|
property,
|
|
@@ -548,8 +846,8 @@ function validateContextList(controlId, property, values, contextIds) {
|
|
|
548
846
|
}
|
|
549
847
|
function emptyApplicabilityIssue(controlId, property) {
|
|
550
848
|
return {
|
|
551
|
-
code:
|
|
552
|
-
severity:
|
|
849
|
+
code: "device-interface-context-unresolved",
|
|
850
|
+
severity: "warning",
|
|
553
851
|
message: `Device interface control "${controlId}" has empty ${property}; omit the predicate instead`,
|
|
554
852
|
componentId: controlId,
|
|
555
853
|
property,
|
|
@@ -557,79 +855,204 @@ function emptyApplicabilityIssue(controlId, property) {
|
|
|
557
855
|
}
|
|
558
856
|
function validateDeviceInterfaceBinding(control, binding, componentIds, externalInterfaceIds, componentsById, resolvedPanelElements) {
|
|
559
857
|
const issues = [];
|
|
560
|
-
if (binding.externalInterfaceId !== undefined &&
|
|
858
|
+
if (binding.externalInterfaceId !== undefined &&
|
|
859
|
+
!externalInterfaceIds.has(binding.externalInterfaceId)) {
|
|
561
860
|
issues.push({
|
|
562
|
-
code:
|
|
563
|
-
severity:
|
|
861
|
+
code: "device-interface-binding-unresolved",
|
|
862
|
+
severity: "warning",
|
|
564
863
|
message: `Device interface control "${control.id}" references missing external interface "${binding.externalInterfaceId}"`,
|
|
565
864
|
componentId: control.id,
|
|
566
|
-
property:
|
|
865
|
+
property: "binding.externalInterfaceId",
|
|
567
866
|
});
|
|
568
867
|
}
|
|
569
868
|
if (!componentIds.has(binding.componentId)) {
|
|
570
869
|
issues.push({
|
|
571
|
-
code:
|
|
572
|
-
severity:
|
|
870
|
+
code: "device-interface-binding-unresolved",
|
|
871
|
+
severity: "warning",
|
|
573
872
|
message: `Device interface control "${control.id}" references missing component "${binding.componentId}"`,
|
|
574
873
|
componentId: control.id,
|
|
575
|
-
property:
|
|
874
|
+
property: "binding.componentId",
|
|
576
875
|
});
|
|
577
876
|
return issues;
|
|
578
877
|
}
|
|
579
|
-
if (binding.controlId !== undefined
|
|
580
|
-
|
|
878
|
+
if (binding.controlId !== undefined &&
|
|
879
|
+
!resolvedPanelElements.some((resolved) => resolved.componentId === binding.componentId &&
|
|
880
|
+
resolved.id === binding.controlId)) {
|
|
581
881
|
issues.push({
|
|
582
|
-
code:
|
|
583
|
-
severity:
|
|
882
|
+
code: "device-interface-binding-unresolved",
|
|
883
|
+
severity: "warning",
|
|
584
884
|
message: `Device interface control "${control.id}" references missing control "${binding.controlId}"`,
|
|
585
885
|
componentId: control.id,
|
|
586
|
-
property:
|
|
886
|
+
property: "binding.controlId",
|
|
587
887
|
});
|
|
588
888
|
}
|
|
589
889
|
const component = componentsById.get(binding.componentId);
|
|
590
|
-
if (binding.property !== undefined &&
|
|
890
|
+
if (binding.property !== undefined &&
|
|
891
|
+
component?.properties[binding.property] === undefined) {
|
|
591
892
|
issues.push({
|
|
592
|
-
code:
|
|
593
|
-
severity:
|
|
893
|
+
code: "device-interface-binding-unresolved",
|
|
894
|
+
severity: "warning",
|
|
594
895
|
message: `Device interface control "${control.id}" references missing property "${binding.property}"`,
|
|
595
896
|
componentId: control.id,
|
|
596
|
-
property:
|
|
897
|
+
property: "binding.property",
|
|
597
898
|
});
|
|
598
899
|
}
|
|
599
900
|
return issues;
|
|
600
901
|
}
|
|
601
|
-
function validateDuplicateDeviceInterfaceRoles(controls) {
|
|
902
|
+
function validateDuplicateDeviceInterfaceRoles(controls, groups) {
|
|
602
903
|
const issues = [];
|
|
904
|
+
const layoutsByControlId = deviceInterfaceRoleLayoutsByControlId(groups);
|
|
603
905
|
const seen = new Map();
|
|
604
906
|
for (const control of controls) {
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
907
|
+
const layouts = layoutsByControlId.get(control.id) ?? [
|
|
908
|
+
{ groupId: control.groupId ?? "", order: control.order },
|
|
909
|
+
];
|
|
910
|
+
for (const layout of layouts) {
|
|
911
|
+
const key = `${layout.groupId}:${control.role}`;
|
|
912
|
+
const existing = seen.get(key);
|
|
913
|
+
if (existing !== undefined &&
|
|
914
|
+
existing.order === undefined &&
|
|
915
|
+
layout.order === undefined) {
|
|
916
|
+
if (deviceInterfaceBindingSignature(existing.control.binding) ===
|
|
917
|
+
deviceInterfaceBindingSignature(control.binding)) {
|
|
918
|
+
issues.push({
|
|
919
|
+
code: "device-interface-duplicate-role",
|
|
920
|
+
severity: "warning",
|
|
921
|
+
message: `Device interface controls "${existing.control.id}" and "${control.id}" share role "${control.role}" without order or distinct binding`,
|
|
922
|
+
componentId: control.id,
|
|
923
|
+
property: "role",
|
|
924
|
+
});
|
|
925
|
+
}
|
|
616
926
|
}
|
|
927
|
+
seen.set(key, {
|
|
928
|
+
control,
|
|
929
|
+
...(layout.order === undefined ? {} : { order: layout.order }),
|
|
930
|
+
});
|
|
617
931
|
}
|
|
618
|
-
seen.set(key, control);
|
|
619
932
|
}
|
|
620
933
|
return issues;
|
|
621
934
|
}
|
|
935
|
+
function deviceInterfaceRoleLayoutsByControlId(groups) {
|
|
936
|
+
const layoutsByControlId = new Map();
|
|
937
|
+
for (const group of groups) {
|
|
938
|
+
for (const member of group.members ?? []) {
|
|
939
|
+
const layouts = layoutsByControlId.get(member.controlId) ?? [];
|
|
940
|
+
layouts.push({
|
|
941
|
+
groupId: group.id,
|
|
942
|
+
...(member.order === undefined ? {} : { order: member.order }),
|
|
943
|
+
});
|
|
944
|
+
layoutsByControlId.set(member.controlId, layouts);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return layoutsByControlId;
|
|
948
|
+
}
|
|
622
949
|
function deviceInterfaceBindingSignature(binding) {
|
|
623
950
|
if (binding === undefined) {
|
|
624
|
-
return
|
|
951
|
+
return "";
|
|
625
952
|
}
|
|
626
953
|
return [
|
|
627
954
|
binding.componentId,
|
|
628
|
-
binding.controlId ??
|
|
629
|
-
binding.controlName ??
|
|
630
|
-
binding.property ??
|
|
631
|
-
binding.externalInterfaceId ??
|
|
632
|
-
].join(
|
|
955
|
+
binding.controlId ?? "",
|
|
956
|
+
binding.controlName ?? "",
|
|
957
|
+
binding.property ?? "",
|
|
958
|
+
binding.externalInterfaceId ?? "",
|
|
959
|
+
].join(":");
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Structural (catalog-free) validation of multi-surface part mounts, where
|
|
963
|
+
* several placement elements share one physical part in one hole (e.g. a
|
|
964
|
+
* concentric pot). Catches orphan bindings and inconsistent mount groups.
|
|
965
|
+
* Surface-existence and completeness against the part catalog are validated
|
|
966
|
+
* downstream in the stompbox build layer, which owns the part profiles.
|
|
967
|
+
*/
|
|
968
|
+
function validateMountGroups(doc) {
|
|
969
|
+
if (doc.panel === undefined) {
|
|
970
|
+
return [];
|
|
971
|
+
}
|
|
972
|
+
const issues = [];
|
|
973
|
+
const groups = new Map();
|
|
974
|
+
for (const face of doc.panel.faces) {
|
|
975
|
+
for (const element of face.elements) {
|
|
976
|
+
const physical = element.physical;
|
|
977
|
+
if (physical === undefined) {
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
const componentId = element.bind.componentId;
|
|
981
|
+
if (physical.mountId === undefined) {
|
|
982
|
+
if (physical.surface !== undefined) {
|
|
983
|
+
issues.push({
|
|
984
|
+
code: "panel-mount-orphan",
|
|
985
|
+
severity: "warning",
|
|
986
|
+
message: `Panel element for "${componentId}" sets physical.surface "${physical.surface}" without a mountId`,
|
|
987
|
+
componentId,
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
if (physical.surface === undefined) {
|
|
993
|
+
issues.push({
|
|
994
|
+
code: "panel-mount-orphan",
|
|
995
|
+
severity: "warning",
|
|
996
|
+
message: `Panel element for "${componentId}" joins mount "${physical.mountId}" without a surface`,
|
|
997
|
+
componentId,
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
if (physical.partProfileId === undefined) {
|
|
1001
|
+
issues.push({
|
|
1002
|
+
code: "panel-mount-orphan",
|
|
1003
|
+
severity: "warning",
|
|
1004
|
+
message: `Panel element for "${componentId}" joins mount "${physical.mountId}" without a partProfileId`,
|
|
1005
|
+
componentId,
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
const members = groups.get(physical.mountId) ?? [];
|
|
1009
|
+
members.push({ componentId, physical });
|
|
1010
|
+
groups.set(physical.mountId, members);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
for (const [mountId, members] of groups) {
|
|
1014
|
+
const anchorId = members[0]?.componentId;
|
|
1015
|
+
const surfaces = new Set();
|
|
1016
|
+
const partIds = new Set();
|
|
1017
|
+
const centers = new Set();
|
|
1018
|
+
for (const member of members) {
|
|
1019
|
+
const { surface, partProfileId, centerMm } = member.physical;
|
|
1020
|
+
if (surface !== undefined) {
|
|
1021
|
+
if (surfaces.has(surface)) {
|
|
1022
|
+
issues.push({
|
|
1023
|
+
code: "panel-mount-inconsistent",
|
|
1024
|
+
severity: "warning",
|
|
1025
|
+
message: `Mount "${mountId}" has duplicate surface "${surface}"`,
|
|
1026
|
+
componentId: member.componentId,
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
surfaces.add(surface);
|
|
1030
|
+
}
|
|
1031
|
+
if (partProfileId !== undefined) {
|
|
1032
|
+
partIds.add(partProfileId);
|
|
1033
|
+
}
|
|
1034
|
+
if (centerMm !== undefined) {
|
|
1035
|
+
centers.add(`${centerMm.x},${centerMm.y}`);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
if (partIds.size > 1) {
|
|
1039
|
+
issues.push({
|
|
1040
|
+
code: "panel-mount-inconsistent",
|
|
1041
|
+
severity: "warning",
|
|
1042
|
+
message: `Mount "${mountId}" mixes part profiles: ${[...partIds].join(", ")}`,
|
|
1043
|
+
...(anchorId === undefined ? {} : { componentId: anchorId }),
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
if (centers.size > 1) {
|
|
1047
|
+
issues.push({
|
|
1048
|
+
code: "panel-mount-inconsistent",
|
|
1049
|
+
severity: "warning",
|
|
1050
|
+
message: `Mount "${mountId}" members are not at one shared centerMm`,
|
|
1051
|
+
...(anchorId === undefined ? {} : { componentId: anchorId }),
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return issues;
|
|
633
1056
|
}
|
|
634
1057
|
function validatePanel(doc, componentIds, semanticControlIds) {
|
|
635
1058
|
if (doc.panel === undefined) {
|
|
@@ -640,19 +1063,20 @@ function validatePanel(doc, componentIds, semanticControlIds) {
|
|
|
640
1063
|
for (const face of doc.panel.faces) {
|
|
641
1064
|
for (const element of face.elements) {
|
|
642
1065
|
const componentId = element.bind.componentId;
|
|
643
|
-
if (element.interfaceControlId !== undefined &&
|
|
1066
|
+
if (element.interfaceControlId !== undefined &&
|
|
1067
|
+
!semanticControlIds.has(element.interfaceControlId)) {
|
|
644
1068
|
issues.push({
|
|
645
|
-
code:
|
|
646
|
-
severity:
|
|
1069
|
+
code: "panel-interface-control-unresolved",
|
|
1070
|
+
severity: "warning",
|
|
647
1071
|
message: `Panel element on face "${face.id}" references missing interface control "${element.interfaceControlId}"`,
|
|
648
1072
|
componentId: element.interfaceControlId,
|
|
649
|
-
property:
|
|
1073
|
+
property: "interfaceControlId",
|
|
650
1074
|
});
|
|
651
1075
|
}
|
|
652
1076
|
if (!componentIds.has(componentId)) {
|
|
653
1077
|
issues.push({
|
|
654
|
-
code:
|
|
655
|
-
severity:
|
|
1078
|
+
code: "panel-binding-unresolved",
|
|
1079
|
+
severity: "warning",
|
|
656
1080
|
message: `Panel element on face "${face.id}" references missing component "${componentId}"`,
|
|
657
1081
|
componentId,
|
|
658
1082
|
});
|
|
@@ -661,18 +1085,19 @@ function validatePanel(doc, componentIds, semanticControlIds) {
|
|
|
661
1085
|
const resolved = resolvePanelElement(resolvedElements, element);
|
|
662
1086
|
if (element.bind.controlId !== undefined && resolved === undefined) {
|
|
663
1087
|
issues.push({
|
|
664
|
-
code:
|
|
665
|
-
severity:
|
|
1088
|
+
code: "panel-control-unresolved",
|
|
1089
|
+
severity: "warning",
|
|
666
1090
|
message: `Panel element on face "${face.id}" references missing control "${element.bind.controlId}" on component "${componentId}"`,
|
|
667
1091
|
componentId,
|
|
668
1092
|
property: element.bind.controlId,
|
|
669
1093
|
});
|
|
670
1094
|
continue;
|
|
671
1095
|
}
|
|
672
|
-
if (resolved !== undefined &&
|
|
1096
|
+
if (resolved !== undefined &&
|
|
1097
|
+
!panelKindsCompatible(element.kind, resolved.kind)) {
|
|
673
1098
|
issues.push({
|
|
674
|
-
code:
|
|
675
|
-
severity:
|
|
1099
|
+
code: "panel-kind-mismatch",
|
|
1100
|
+
severity: "warning",
|
|
676
1101
|
message: `Panel element on face "${face.id}" binds component "${componentId}" as ${element.kind} but resolved kind is ${resolved.kind}`,
|
|
677
1102
|
componentId,
|
|
678
1103
|
});
|
|
@@ -688,7 +1113,8 @@ function panelKindsCompatible(declared, resolved) {
|
|
|
688
1113
|
if (declared === resolved) {
|
|
689
1114
|
return true;
|
|
690
1115
|
}
|
|
691
|
-
return resolved ===
|
|
1116
|
+
return (resolved === "switch" &&
|
|
1117
|
+
(declared === "selector" || declared === "footswitch"));
|
|
692
1118
|
}
|
|
693
1119
|
function resolvePanelElements(doc) {
|
|
694
1120
|
const panel = extractPanel(doc);
|
|
@@ -697,47 +1123,51 @@ function resolvePanelElements(doc) {
|
|
|
697
1123
|
resolved.push({
|
|
698
1124
|
id: knob.id,
|
|
699
1125
|
componentId: componentIdFromPanelElementId(knob.id),
|
|
700
|
-
kind: knob.id.endsWith(
|
|
1126
|
+
kind: knob.id.endsWith(":mode") && knob.controlMode === "stepped"
|
|
1127
|
+
? "switch"
|
|
1128
|
+
: "knob",
|
|
701
1129
|
});
|
|
702
1130
|
}
|
|
703
1131
|
for (const slider of panel.sliders ?? []) {
|
|
704
1132
|
resolved.push({
|
|
705
1133
|
id: slider.id,
|
|
706
1134
|
componentId: componentIdFromPanelElementId(slider.id),
|
|
707
|
-
kind:
|
|
1135
|
+
kind: "slider",
|
|
708
1136
|
});
|
|
709
1137
|
}
|
|
710
1138
|
for (const switchControl of panel.switches) {
|
|
711
1139
|
resolved.push({
|
|
712
1140
|
id: switchControl.id,
|
|
713
1141
|
componentId: componentIdFromPanelElementId(switchControl.id),
|
|
714
|
-
kind:
|
|
1142
|
+
kind: "switch",
|
|
715
1143
|
});
|
|
716
1144
|
}
|
|
717
1145
|
for (const led of panel.leds) {
|
|
718
1146
|
resolved.push({
|
|
719
1147
|
id: led.id,
|
|
720
1148
|
componentId: componentIdFromPanelElementId(led.id),
|
|
721
|
-
kind:
|
|
1149
|
+
kind: "led",
|
|
722
1150
|
});
|
|
723
1151
|
}
|
|
724
1152
|
for (const jack of panel.jacks) {
|
|
725
1153
|
resolved.push({
|
|
726
1154
|
id: jack.id,
|
|
727
1155
|
componentId: jack.sourceComponentId ?? componentIdFromPanelElementId(jack.id),
|
|
728
|
-
kind:
|
|
1156
|
+
kind: "jack",
|
|
729
1157
|
});
|
|
730
1158
|
}
|
|
731
1159
|
return resolved;
|
|
732
1160
|
}
|
|
733
1161
|
function resolvePanelElement(resolvedElements, element) {
|
|
734
1162
|
if (element.bind.controlId !== undefined) {
|
|
735
|
-
return resolvedElements.find((resolved) => resolved.componentId === element.bind.componentId &&
|
|
1163
|
+
return resolvedElements.find((resolved) => resolved.componentId === element.bind.componentId &&
|
|
1164
|
+
resolved.id === element.bind.controlId);
|
|
736
1165
|
}
|
|
737
|
-
return resolvedElements.find((resolved) => resolved.componentId === element.bind.componentId &&
|
|
1166
|
+
return resolvedElements.find((resolved) => resolved.componentId === element.bind.componentId &&
|
|
1167
|
+
resolved.id === element.bind.componentId);
|
|
738
1168
|
}
|
|
739
1169
|
function componentIdFromPanelElementId(id) {
|
|
740
|
-
const separator = id.indexOf(
|
|
1170
|
+
const separator = id.indexOf(":");
|
|
741
1171
|
return separator <= 0 ? id : id.slice(0, separator);
|
|
742
1172
|
}
|
|
743
1173
|
function validatePanelCellCollisions(face) {
|
|
@@ -753,8 +1183,8 @@ function validatePanelCellCollisions(face) {
|
|
|
753
1183
|
const key = `${row}:${column}`;
|
|
754
1184
|
if (occupied.has(key)) {
|
|
755
1185
|
issues.push({
|
|
756
|
-
code:
|
|
757
|
-
severity:
|
|
1186
|
+
code: "panel-cell-collision",
|
|
1187
|
+
severity: "warning",
|
|
758
1188
|
message: `Panel face "${face.id}" has overlapping elements at row ${row}, column ${column}`,
|
|
759
1189
|
componentId: element.bind.componentId,
|
|
760
1190
|
});
|
|
@@ -786,21 +1216,21 @@ function validateV3BuildMetadata(doc, componentIds) {
|
|
|
786
1216
|
]));
|
|
787
1217
|
const selectedBoardId = doc.build?.selectedBoardId;
|
|
788
1218
|
if (selectedBoardId !== undefined && !boardsById.has(selectedBoardId)) {
|
|
789
|
-
issues.push(unresolvedIssue(
|
|
1219
|
+
issues.push(unresolvedIssue("build-board-unresolved", "error", `Build selectedBoardId references missing board "${selectedBoardId}"`, selectedBoardId, "selectedBoardId"));
|
|
790
1220
|
}
|
|
791
1221
|
for (const boardId of doc.build?.alternateBoardIds ?? []) {
|
|
792
1222
|
if (!boardsById.has(boardId)) {
|
|
793
|
-
issues.push(unresolvedIssue(
|
|
1223
|
+
issues.push(unresolvedIssue("build-board-unresolved", "warning", `Build alternateBoardIds references missing board "${boardId}"`, boardId, "alternateBoardIds"));
|
|
794
1224
|
}
|
|
795
1225
|
}
|
|
796
|
-
const preferredBoardId = dataString(doc.mechanical?.internalBoard,
|
|
1226
|
+
const preferredBoardId = dataString(doc.mechanical?.internalBoard, "preferredBoardId");
|
|
797
1227
|
if (preferredBoardId !== undefined && !boardsById.has(preferredBoardId)) {
|
|
798
|
-
issues.push(unresolvedIssue(
|
|
1228
|
+
issues.push(unresolvedIssue("build-board-unresolved", "warning", `Mechanical internalBoard.preferredBoardId references missing board "${preferredBoardId}"`, preferredBoardId, "mechanical.internalBoard.preferredBoardId"));
|
|
799
1229
|
}
|
|
800
1230
|
const harnessesById = new Map(doc.offBoardWiring?.harnesses.map((harness) => [harness.id, harness]) ?? []);
|
|
801
1231
|
for (const harnessId of doc.build?.selectedOffBoardWiringHarnessIds ?? []) {
|
|
802
1232
|
if (!harnessesById.has(harnessId)) {
|
|
803
|
-
issues.push(unresolvedIssue(
|
|
1233
|
+
issues.push(unresolvedIssue("build-harness-unresolved", "error", `Build selectedOffBoardWiringHarnessIds references missing harness "${harnessId}"`, harnessId, "selectedOffBoardWiringHarnessIds"));
|
|
804
1234
|
}
|
|
805
1235
|
}
|
|
806
1236
|
for (const item of doc.bom?.items ?? []) {
|
|
@@ -817,7 +1247,8 @@ function validateV3BuildMetadata(doc, componentIds) {
|
|
|
817
1247
|
if (doc.offBoardWiring !== undefined) {
|
|
818
1248
|
issues.push(...validateOffBoardWiring(doc, componentsById, panelElementIds, boardTerminalsByBoardId, boardNetsByBoardId));
|
|
819
1249
|
}
|
|
820
|
-
if (doc.build?.completeness ===
|
|
1250
|
+
if (doc.build?.completeness === "complete-selected-build" &&
|
|
1251
|
+
selectedBoardId !== undefined) {
|
|
821
1252
|
const selectedBoard = boardsById.get(selectedBoardId);
|
|
822
1253
|
if (selectedBoard !== undefined) {
|
|
823
1254
|
issues.push(...validateCompleteSelectedBoardRoutes(selectedBoard));
|
|
@@ -826,7 +1257,7 @@ function validateV3BuildMetadata(doc, componentIds) {
|
|
|
826
1257
|
return issues;
|
|
827
1258
|
}
|
|
828
1259
|
function hasV3BuildMetadata(doc) {
|
|
829
|
-
return doc.mechanical !== undefined ||
|
|
1260
|
+
return (doc.mechanical !== undefined ||
|
|
830
1261
|
doc.build !== undefined ||
|
|
831
1262
|
doc.bom !== undefined ||
|
|
832
1263
|
doc.partProfiles !== undefined ||
|
|
@@ -834,71 +1265,78 @@ function hasV3BuildMetadata(doc) {
|
|
|
834
1265
|
doc.offBoardWiring !== undefined ||
|
|
835
1266
|
doc.boards !== undefined ||
|
|
836
1267
|
doc.panel?.faces.some((face) => face.geometry !== undefined ||
|
|
837
|
-
face.elements.some((element) => element.id !== undefined || element.physical !== undefined)) === true;
|
|
1268
|
+
face.elements.some((element) => element.id !== undefined || element.physical !== undefined)) === true);
|
|
838
1269
|
}
|
|
839
1270
|
function validateBomRef(ref, componentIds, controlIds, panelElementIds, boardsById, itemId) {
|
|
840
|
-
if (ref.kind ===
|
|
841
|
-
|
|
1271
|
+
if (ref.kind === "component" &&
|
|
1272
|
+
(ref.componentId === undefined || !componentIds.has(ref.componentId))) {
|
|
1273
|
+
return unresolvedIssue("bom-ref-unresolved", "warning", `BOM item "${itemId}" references missing component "${ref.componentId ?? ""}"`, itemId, "refs.componentId");
|
|
842
1274
|
}
|
|
843
|
-
if (ref.kind ===
|
|
1275
|
+
if (ref.kind === "device-interface-control" &&
|
|
844
1276
|
(ref.controlId === undefined || !controlIds.has(ref.controlId))) {
|
|
845
|
-
return unresolvedIssue(
|
|
1277
|
+
return unresolvedIssue("bom-ref-unresolved", "warning", `BOM item "${itemId}" references missing device interface control "${ref.controlId ?? ""}"`, itemId, "refs.controlId");
|
|
846
1278
|
}
|
|
847
|
-
if (ref.kind ===
|
|
848
|
-
|
|
1279
|
+
if (ref.kind === "panel-element" &&
|
|
1280
|
+
(ref.panelElementId === undefined ||
|
|
1281
|
+
!panelElementIds.has(ref.panelElementId))) {
|
|
1282
|
+
return unresolvedIssue("bom-ref-unresolved", "warning", `BOM item "${itemId}" references missing panel element "${ref.panelElementId ?? ""}"`, itemId, "refs.panelElementId");
|
|
849
1283
|
}
|
|
850
|
-
if (ref.kind ===
|
|
851
|
-
|
|
1284
|
+
if (ref.kind === "board" &&
|
|
1285
|
+
(ref.boardId === undefined || !boardsById.has(ref.boardId))) {
|
|
1286
|
+
return unresolvedIssue("bom-ref-unresolved", "warning", `BOM item "${itemId}" references missing board "${ref.boardId ?? ""}"`, itemId, "refs.boardId");
|
|
852
1287
|
}
|
|
853
1288
|
return undefined;
|
|
854
1289
|
}
|
|
855
1290
|
function validateBoardRealization(board, componentsById, boardNetsByBoardId) {
|
|
856
1291
|
const issues = [];
|
|
857
|
-
if (board.sourceCircuit !== undefined &&
|
|
1292
|
+
if (board.sourceCircuit !== undefined &&
|
|
1293
|
+
!isDigestShapedSourceHash(board.sourceCircuit.hash)) {
|
|
858
1294
|
issues.push({
|
|
859
|
-
code:
|
|
860
|
-
severity:
|
|
1295
|
+
code: "board-source-hash-invalid",
|
|
1296
|
+
severity: "error",
|
|
861
1297
|
message: `Board "${board.id}" sourceCircuit.hash must be sha256:<64 hex chars>`,
|
|
862
1298
|
componentId: board.id,
|
|
863
|
-
property:
|
|
1299
|
+
property: "sourceCircuit.hash",
|
|
864
1300
|
});
|
|
865
1301
|
}
|
|
866
1302
|
for (const terminal of board.edgeTerminals) {
|
|
867
|
-
if (terminal.terminalRef !== undefined &&
|
|
868
|
-
|
|
1303
|
+
if (terminal.terminalRef !== undefined &&
|
|
1304
|
+
!componentTerminalExists(componentsById, terminal.terminalRef)) {
|
|
1305
|
+
issues.push(unresolvedIssue("board-terminal-unresolved", "warning", `Board "${board.id}" edge terminal "${terminal.id}" references missing component terminal`, board.id, terminal.id));
|
|
869
1306
|
}
|
|
870
1307
|
}
|
|
871
1308
|
for (const placement of board.footprintPlacements) {
|
|
872
1309
|
if (!componentsById.has(placement.componentId)) {
|
|
873
|
-
issues.push(unresolvedIssue(
|
|
1310
|
+
issues.push(unresolvedIssue("board-terminal-unresolved", "warning", `Board "${board.id}" places missing component "${placement.componentId}"`, board.id, placement.componentId));
|
|
874
1311
|
continue;
|
|
875
1312
|
}
|
|
876
1313
|
for (const pad of placement.pads) {
|
|
877
1314
|
if (pad.terminalName !== undefined &&
|
|
878
1315
|
!componentHasTerminal(componentsById, placement.componentId, pad.terminalName)) {
|
|
879
|
-
issues.push(unresolvedIssue(
|
|
1316
|
+
issues.push(unresolvedIssue("board-terminal-unresolved", "warning", `Board "${board.id}" pad "${pad.padId}" references missing terminal "${pad.terminalName}"`, board.id, pad.padId));
|
|
880
1317
|
}
|
|
881
1318
|
}
|
|
882
1319
|
}
|
|
883
1320
|
for (const net of board.netlist?.nets ?? []) {
|
|
884
1321
|
for (const member of net.members) {
|
|
885
1322
|
if (!componentTerminalExists(componentsById, member)) {
|
|
886
|
-
issues.push(unresolvedIssue(
|
|
1323
|
+
issues.push(unresolvedIssue("board-terminal-unresolved", "warning", `Board "${board.id}" net "${net.id}" references missing component terminal`, board.id, net.id));
|
|
887
1324
|
}
|
|
888
1325
|
}
|
|
889
1326
|
}
|
|
890
1327
|
for (const route of board.routes) {
|
|
891
1328
|
if (route.zones !== undefined || route.drills !== undefined) {
|
|
892
1329
|
issues.push({
|
|
893
|
-
code:
|
|
894
|
-
severity:
|
|
1330
|
+
code: "board-route-feature-invalid",
|
|
1331
|
+
severity: "error",
|
|
895
1332
|
message: `Board "${board.id}" route "${route.id}" contains board-level zones or drills`,
|
|
896
1333
|
componentId: board.id,
|
|
897
1334
|
property: route.id,
|
|
898
1335
|
});
|
|
899
1336
|
}
|
|
900
|
-
if (isBoardNetlistRef(route.netRef) &&
|
|
901
|
-
|
|
1337
|
+
if (isBoardNetlistRef(route.netRef) &&
|
|
1338
|
+
!boardNetRefExists(route.netRef, board.id, boardNetsByBoardId)) {
|
|
1339
|
+
issues.push(unresolvedIssue("offboard-signal-unresolved", "warning", `Board "${board.id}" route "${route.id}" references missing board net "${route.netRef.netId}"`, board.id, route.id));
|
|
902
1340
|
}
|
|
903
1341
|
}
|
|
904
1342
|
return issues;
|
|
@@ -918,10 +1356,10 @@ function validateOffBoardWiring(doc, componentsById, panelElementIds, boardTermi
|
|
|
918
1356
|
}
|
|
919
1357
|
for (const connection of harness.connections) {
|
|
920
1358
|
if (!localEndpointIds.has(connection.fromEndpointId)) {
|
|
921
|
-
issues.push(unresolvedIssue(
|
|
1359
|
+
issues.push(unresolvedIssue("offboard-endpoint-unresolved", "error", `Harness "${harness.id}" connection "${connection.id}" references missing endpoint "${connection.fromEndpointId}"`, harness.id, connection.id));
|
|
922
1360
|
}
|
|
923
1361
|
if (!localEndpointIds.has(connection.toEndpointId)) {
|
|
924
|
-
issues.push(unresolvedIssue(
|
|
1362
|
+
issues.push(unresolvedIssue("offboard-endpoint-unresolved", "error", `Harness "${harness.id}" connection "${connection.id}" references missing endpoint "${connection.toEndpointId}"`, harness.id, connection.id));
|
|
925
1363
|
}
|
|
926
1364
|
if (connection.signalRef !== undefined) {
|
|
927
1365
|
const issue = validateOffBoardSignalRef(connection.signalRef, componentsById, boardNetsByBoardId, harness.id);
|
|
@@ -937,33 +1375,38 @@ function validateOffBoardWiring(doc, componentsById, panelElementIds, boardTermi
|
|
|
937
1375
|
continue;
|
|
938
1376
|
}
|
|
939
1377
|
for (const connection of harness.connections) {
|
|
940
|
-
if (!endpointIds.has(connection.fromEndpointId) ||
|
|
941
|
-
|
|
1378
|
+
if (!endpointIds.has(connection.fromEndpointId) ||
|
|
1379
|
+
!endpointIds.has(connection.toEndpointId)) {
|
|
1380
|
+
issues.push(unresolvedIssue("offboard-endpoint-unresolved", "error", `Selected harness "${harnessId}" contains an unresolved connection endpoint`, harnessId, connection.id));
|
|
942
1381
|
}
|
|
943
1382
|
}
|
|
944
1383
|
}
|
|
945
1384
|
return issues;
|
|
946
1385
|
}
|
|
947
1386
|
function validateOffBoardEndpoint(endpoint, componentsById, panelElementIds, boardTerminalsByBoardId) {
|
|
948
|
-
if (endpoint.kind ===
|
|
949
|
-
const terminalIds = endpoint.boardId === undefined
|
|
950
|
-
|
|
951
|
-
|
|
1387
|
+
if (endpoint.kind === "board-terminal") {
|
|
1388
|
+
const terminalIds = endpoint.boardId === undefined
|
|
1389
|
+
? undefined
|
|
1390
|
+
: boardTerminalsByBoardId.get(endpoint.boardId);
|
|
1391
|
+
if (terminalIds === undefined ||
|
|
1392
|
+
endpoint.terminalId === undefined ||
|
|
1393
|
+
!terminalIds.has(endpoint.terminalId)) {
|
|
1394
|
+
return unresolvedIssue("offboard-endpoint-unresolved", "error", `Off-board endpoint "${endpoint.id}" references missing board terminal`, endpoint.id, "terminalId");
|
|
952
1395
|
}
|
|
953
1396
|
return undefined;
|
|
954
1397
|
}
|
|
955
|
-
if (endpoint.kind ===
|
|
956
|
-
endpoint.kind ===
|
|
957
|
-
endpoint.kind ===
|
|
1398
|
+
if (endpoint.kind === "panel-component-terminal" ||
|
|
1399
|
+
endpoint.kind === "power-terminal" ||
|
|
1400
|
+
endpoint.kind === "footswitch-terminal") {
|
|
958
1401
|
if (endpoint.componentId === undefined ||
|
|
959
1402
|
endpoint.terminalName === undefined ||
|
|
960
1403
|
!componentHasTerminal(componentsById, endpoint.componentId, endpoint.terminalName)) {
|
|
961
|
-
return unresolvedIssue(
|
|
1404
|
+
return unresolvedIssue("offboard-endpoint-unresolved", "error", `Off-board endpoint "${endpoint.id}" references missing component terminal`, endpoint.id, "componentId");
|
|
962
1405
|
}
|
|
963
1406
|
if (endpoint.panelElementId !== undefined &&
|
|
964
|
-
endpoint.kind !==
|
|
1407
|
+
endpoint.kind !== "power-terminal" &&
|
|
965
1408
|
!panelElementIds.has(endpoint.panelElementId)) {
|
|
966
|
-
return unresolvedIssue(
|
|
1409
|
+
return unresolvedIssue("offboard-endpoint-unresolved", "warning", `Off-board endpoint "${endpoint.id}" references missing panel element "${endpoint.panelElementId}"`, endpoint.id, "panelElementId");
|
|
967
1410
|
}
|
|
968
1411
|
}
|
|
969
1412
|
return undefined;
|
|
@@ -971,16 +1414,18 @@ function validateOffBoardEndpoint(endpoint, componentsById, panelElementIds, boa
|
|
|
971
1414
|
function validateOffBoardSignalRef(signalRef, componentsById, boardNetsByBoardId, harnessId) {
|
|
972
1415
|
if (isBoardNetlistRef(signalRef)) {
|
|
973
1416
|
if (!boardNetRefExists(signalRef, signalRef.boardId, boardNetsByBoardId)) {
|
|
974
|
-
return unresolvedIssue(
|
|
1417
|
+
return unresolvedIssue("offboard-signal-unresolved", "error", `Harness "${harnessId}" references missing board net "${signalRef.netId}"`, harnessId, "signalRef");
|
|
975
1418
|
}
|
|
976
1419
|
return undefined;
|
|
977
1420
|
}
|
|
978
|
-
const member = dataObject(signalRef,
|
|
979
|
-
const componentId = dataString(member,
|
|
980
|
-
const terminalName = dataString(member,
|
|
981
|
-
if (dataString(signalRef,
|
|
1421
|
+
const member = dataObject(signalRef, "member");
|
|
1422
|
+
const componentId = dataString(member, "componentId");
|
|
1423
|
+
const terminalName = dataString(member, "terminalName");
|
|
1424
|
+
if (dataString(signalRef, "source") === "canonical-circuit" &&
|
|
1425
|
+
componentId !== undefined &&
|
|
1426
|
+
terminalName !== undefined) {
|
|
982
1427
|
if (!componentHasTerminal(componentsById, componentId, terminalName)) {
|
|
983
|
-
return unresolvedIssue(
|
|
1428
|
+
return unresolvedIssue("offboard-signal-unresolved", "error", `Harness "${harnessId}" references missing canonical component terminal`, harnessId, "signalRef");
|
|
984
1429
|
}
|
|
985
1430
|
}
|
|
986
1431
|
return undefined;
|
|
@@ -989,7 +1434,7 @@ function validateCompleteSelectedBoardRoutes(board) {
|
|
|
989
1434
|
const issues = [];
|
|
990
1435
|
const routedNetIds = new Set(board.routes
|
|
991
1436
|
.filter((route) => isRouteForBoardNet(route, board.id))
|
|
992
|
-
.map((route) => dataString(route.netRef,
|
|
1437
|
+
.map((route) => dataString(route.netRef, "netId"))
|
|
993
1438
|
.filter((netId) => netId !== undefined));
|
|
994
1439
|
for (const net of board.netlist?.nets ?? []) {
|
|
995
1440
|
if (isSingleTerminalEdgeNet(net)) {
|
|
@@ -997,8 +1442,8 @@ function validateCompleteSelectedBoardRoutes(board) {
|
|
|
997
1442
|
}
|
|
998
1443
|
if (!routedNetIds.has(net.id)) {
|
|
999
1444
|
issues.push({
|
|
1000
|
-
code:
|
|
1001
|
-
severity:
|
|
1445
|
+
code: "board-net-unrouted",
|
|
1446
|
+
severity: "error",
|
|
1002
1447
|
message: `Selected board "${board.id}" net "${net.id}" has multiple members but no route`,
|
|
1003
1448
|
componentId: board.id,
|
|
1004
1449
|
property: net.id,
|
|
@@ -1017,7 +1462,8 @@ function isRouteForBoardNet(route, boardId) {
|
|
|
1017
1462
|
return route.netRef.boardId === undefined || route.netRef.boardId === boardId;
|
|
1018
1463
|
}
|
|
1019
1464
|
function isBoardNetlistRef(value) {
|
|
1020
|
-
return dataString(value,
|
|
1465
|
+
return (dataString(value, "source") === "board-netlist" &&
|
|
1466
|
+
dataString(value, "netId") !== undefined);
|
|
1021
1467
|
}
|
|
1022
1468
|
function boardNetRefExists(ref, fallbackBoardId, boardNetsByBoardId) {
|
|
1023
1469
|
const boardId = ref.boardId ?? fallbackBoardId;
|
|
@@ -1045,21 +1491,23 @@ function componentTerminalExists(componentsById, ref) {
|
|
|
1045
1491
|
return componentHasTerminal(componentsById, ref.componentId, ref.terminalName);
|
|
1046
1492
|
}
|
|
1047
1493
|
function componentHasTerminal(componentsById, componentId, terminalName) {
|
|
1048
|
-
return componentsById
|
|
1494
|
+
return (componentsById
|
|
1495
|
+
.get(componentId)
|
|
1496
|
+
?.terminals.some((terminal) => terminal.name === terminalName) === true);
|
|
1049
1497
|
}
|
|
1050
1498
|
function isDigestShapedSourceHash(hash) {
|
|
1051
1499
|
return /^sha256:[0-9a-f]{64}$/i.test(hash);
|
|
1052
1500
|
}
|
|
1053
1501
|
function dataString(object, key) {
|
|
1054
1502
|
const value = object?.[key];
|
|
1055
|
-
return typeof value ===
|
|
1503
|
+
return typeof value === "string" ? value : undefined;
|
|
1056
1504
|
}
|
|
1057
1505
|
function dataObject(object, key) {
|
|
1058
1506
|
const value = object?.[key];
|
|
1059
1507
|
return isBuildDataObject(value) ? value : undefined;
|
|
1060
1508
|
}
|
|
1061
1509
|
function isBuildDataObject(value) {
|
|
1062
|
-
return typeof value ===
|
|
1510
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1063
1511
|
}
|
|
1064
1512
|
function unresolvedIssue(code, severity, message, componentId, property) {
|
|
1065
1513
|
return {
|
|
@@ -1072,8 +1520,8 @@ function unresolvedIssue(code, severity, message, componentId, property) {
|
|
|
1072
1520
|
}
|
|
1073
1521
|
function missingPropertyIssue(component, rule) {
|
|
1074
1522
|
return {
|
|
1075
|
-
code: rule.kind ===
|
|
1076
|
-
severity:
|
|
1523
|
+
code: rule.kind === "string" ? "model-required" : "value-required",
|
|
1524
|
+
severity: "error",
|
|
1077
1525
|
message: `${component.id} (${component.kind}): missing required property "${rule.name}"`,
|
|
1078
1526
|
componentId: component.id,
|
|
1079
1527
|
property: rule.name,
|