@vessel-dsp/core 0.6.4 → 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 +10 -2
- 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 +463 -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 +41 -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 +61 -33
- 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 +668 -331
- 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 +327 -225
- package/dist/panel/extract.js.map +1 -1
- package/dist/panel/index.d.ts +7 -7
- package/dist/panel/index.d.ts.map +1 -1
- package/dist/panel/index.js +5 -5
- 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 +1 -1
- package/dist/panel/placement.d.ts.map +1 -1
- package/dist/panel/placement.js +11 -9
- package/dist/panel/placement.js.map +1 -1
- 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 +18 -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,7 +629,10 @@ 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 = [];
|
|
@@ -440,41 +644,41 @@ function validateDeviceInterface(doc, componentIds) {
|
|
|
440
644
|
const componentsById = new Map(doc.components.map((component) => [component.id, component]));
|
|
441
645
|
const resolvedPanelElements = resolvePanelElements(doc);
|
|
442
646
|
for (const group of doc.controlGroups ?? []) {
|
|
443
|
-
issues.push(...validateOpenToken(group.role, group.id,
|
|
647
|
+
issues.push(...validateOpenToken(group.role, group.id, "role"));
|
|
444
648
|
for (const contextId of group.contextIds ?? []) {
|
|
445
649
|
if (!contextIds.has(contextId)) {
|
|
446
650
|
issues.push({
|
|
447
|
-
code:
|
|
448
|
-
severity:
|
|
651
|
+
code: "control-group-context-unresolved",
|
|
652
|
+
severity: "warning",
|
|
449
653
|
message: `Control group "${group.id}" references missing context "${contextId}"`,
|
|
450
654
|
componentId: group.id,
|
|
451
|
-
property:
|
|
655
|
+
property: "contextIds",
|
|
452
656
|
});
|
|
453
657
|
}
|
|
454
658
|
}
|
|
455
659
|
issues.push(...validateControlGroupMembers(group, declaredControlIds, contextIds));
|
|
456
660
|
}
|
|
457
661
|
for (const context of doc.controlContexts ?? []) {
|
|
458
|
-
issues.push(...validateOpenToken(context.role, context.id,
|
|
662
|
+
issues.push(...validateOpenToken(context.role, context.id, "role"));
|
|
459
663
|
}
|
|
460
664
|
for (const control of doc.deviceInterface?.controls ?? []) {
|
|
461
665
|
if (semanticControlIds.has(control.id)) {
|
|
462
666
|
issues.push({
|
|
463
|
-
code:
|
|
464
|
-
severity:
|
|
667
|
+
code: "duplicate-device-interface-control-id",
|
|
668
|
+
severity: "error",
|
|
465
669
|
message: `Duplicate device interface control id "${control.id}"`,
|
|
466
670
|
componentId: control.id,
|
|
467
671
|
});
|
|
468
672
|
}
|
|
469
673
|
semanticControlIds.add(control.id);
|
|
470
|
-
issues.push(...validateOpenToken(control.role, control.id,
|
|
674
|
+
issues.push(...validateOpenToken(control.role, control.id, "role"));
|
|
471
675
|
if (control.groupId !== undefined && !groupIds.has(control.groupId)) {
|
|
472
676
|
issues.push({
|
|
473
|
-
code:
|
|
474
|
-
severity:
|
|
677
|
+
code: "device-interface-group-unresolved",
|
|
678
|
+
severity: "warning",
|
|
475
679
|
message: `Device interface control "${control.id}" references missing group "${control.groupId}"`,
|
|
476
680
|
componentId: control.id,
|
|
477
|
-
property:
|
|
681
|
+
property: "groupId",
|
|
478
682
|
});
|
|
479
683
|
}
|
|
480
684
|
issues.push(...validateApplicability(control, contextIds));
|
|
@@ -491,22 +695,22 @@ function validateControlGroupMembers(group, controlIds, contextIds) {
|
|
|
491
695
|
for (const member of group.members ?? []) {
|
|
492
696
|
if (!controlIds.has(member.controlId)) {
|
|
493
697
|
issues.push({
|
|
494
|
-
code:
|
|
495
|
-
severity:
|
|
698
|
+
code: "control-group-member-unresolved",
|
|
699
|
+
severity: "warning",
|
|
496
700
|
message: `Control group "${group.id}" references missing member control "${member.controlId}"`,
|
|
497
701
|
componentId: group.id,
|
|
498
|
-
property:
|
|
702
|
+
property: "members.controlId",
|
|
499
703
|
});
|
|
500
704
|
}
|
|
501
705
|
if (member.order !== undefined) {
|
|
502
706
|
const existingControlId = orderOwners.get(member.order);
|
|
503
707
|
if (existingControlId !== undefined) {
|
|
504
708
|
issues.push({
|
|
505
|
-
code:
|
|
506
|
-
severity:
|
|
709
|
+
code: "control-group-member-order-duplicate",
|
|
710
|
+
severity: "warning",
|
|
507
711
|
message: `Control group "${group.id}" assigns order ${member.order} to both "${existingControlId}" and "${member.controlId}"`,
|
|
508
712
|
componentId: group.id,
|
|
509
|
-
property:
|
|
713
|
+
property: "members.order",
|
|
510
714
|
});
|
|
511
715
|
}
|
|
512
716
|
orderOwners.set(member.order, member.controlId);
|
|
@@ -520,17 +724,17 @@ function validateControlGroupMemberApplicability(groupId, member, contextIds) {
|
|
|
520
724
|
if (member.appliesWhen === undefined) {
|
|
521
725
|
return issues;
|
|
522
726
|
}
|
|
523
|
-
issues.push(...validateGroupMemberContextList(groupId, member,
|
|
524
|
-
issues.push(...validateGroupMemberContextList(groupId, member,
|
|
525
|
-
if (member.appliesWhen.allOf !== undefined
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
issues.push(emptyGroupMemberApplicabilityIssue(groupId, member.controlId,
|
|
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"));
|
|
529
733
|
}
|
|
530
|
-
if (member.appliesWhen.anyOf !== undefined
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
issues.push(emptyGroupMemberApplicabilityIssue(groupId, member.controlId,
|
|
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"));
|
|
534
738
|
}
|
|
535
739
|
return issues;
|
|
536
740
|
}
|
|
@@ -546,8 +750,8 @@ function validateGroupMemberContextList(groupId, member, property, values, conte
|
|
|
546
750
|
for (const contextId of values) {
|
|
547
751
|
if (seen.has(contextId)) {
|
|
548
752
|
issues.push({
|
|
549
|
-
code:
|
|
550
|
-
severity:
|
|
753
|
+
code: "control-group-member-context-unresolved",
|
|
754
|
+
severity: "warning",
|
|
551
755
|
message: `Control group "${groupId}" member "${member.controlId}" repeats context "${contextId}" in ${property}`,
|
|
552
756
|
componentId: groupId,
|
|
553
757
|
property,
|
|
@@ -556,8 +760,8 @@ function validateGroupMemberContextList(groupId, member, property, values, conte
|
|
|
556
760
|
seen.add(contextId);
|
|
557
761
|
if (!contextIds.has(contextId)) {
|
|
558
762
|
issues.push({
|
|
559
|
-
code:
|
|
560
|
-
severity:
|
|
763
|
+
code: "control-group-member-context-unresolved",
|
|
764
|
+
severity: "warning",
|
|
561
765
|
message: `Control group "${groupId}" member "${member.controlId}" references missing context "${contextId}"`,
|
|
562
766
|
componentId: groupId,
|
|
563
767
|
property,
|
|
@@ -568,8 +772,8 @@ function validateGroupMemberContextList(groupId, member, property, values, conte
|
|
|
568
772
|
}
|
|
569
773
|
function emptyGroupMemberApplicabilityIssue(groupId, controlId, property) {
|
|
570
774
|
return {
|
|
571
|
-
code:
|
|
572
|
-
severity:
|
|
775
|
+
code: "control-group-member-context-unresolved",
|
|
776
|
+
severity: "warning",
|
|
573
777
|
message: `Control group "${groupId}" member "${controlId}" has empty ${property}; omit the predicate instead`,
|
|
574
778
|
componentId: groupId,
|
|
575
779
|
property,
|
|
@@ -579,30 +783,32 @@ function validateOpenToken(value, componentId, property) {
|
|
|
579
783
|
if (/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(value)) {
|
|
580
784
|
return [];
|
|
581
785
|
}
|
|
582
|
-
return [
|
|
583
|
-
|
|
584
|
-
|
|
786
|
+
return [
|
|
787
|
+
{
|
|
788
|
+
code: "invalid-device-interface-token",
|
|
789
|
+
severity: "warning",
|
|
585
790
|
message: `${componentId}: ${property} "${value}" must be a lower-kebab token`,
|
|
586
791
|
componentId,
|
|
587
792
|
property,
|
|
588
|
-
}
|
|
793
|
+
},
|
|
794
|
+
];
|
|
589
795
|
}
|
|
590
796
|
function validateApplicability(control, contextIds) {
|
|
591
797
|
const issues = [];
|
|
592
798
|
if (control.appliesWhen === undefined) {
|
|
593
799
|
return issues;
|
|
594
800
|
}
|
|
595
|
-
issues.push(...validateContextList(control.id,
|
|
596
|
-
issues.push(...validateContextList(control.id,
|
|
597
|
-
if (control.appliesWhen.allOf !== undefined
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
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"));
|
|
601
807
|
}
|
|
602
|
-
if (control.appliesWhen.anyOf !== undefined
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
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"));
|
|
606
812
|
}
|
|
607
813
|
return issues;
|
|
608
814
|
}
|
|
@@ -618,8 +824,8 @@ function validateContextList(controlId, property, values, contextIds) {
|
|
|
618
824
|
for (const contextId of values) {
|
|
619
825
|
if (seen.has(contextId)) {
|
|
620
826
|
issues.push({
|
|
621
|
-
code:
|
|
622
|
-
severity:
|
|
827
|
+
code: "device-interface-context-unresolved",
|
|
828
|
+
severity: "warning",
|
|
623
829
|
message: `Device interface control "${controlId}" repeats context "${contextId}" in ${property}`,
|
|
624
830
|
componentId: controlId,
|
|
625
831
|
property,
|
|
@@ -628,8 +834,8 @@ function validateContextList(controlId, property, values, contextIds) {
|
|
|
628
834
|
seen.add(contextId);
|
|
629
835
|
if (!contextIds.has(contextId)) {
|
|
630
836
|
issues.push({
|
|
631
|
-
code:
|
|
632
|
-
severity:
|
|
837
|
+
code: "device-interface-context-unresolved",
|
|
838
|
+
severity: "warning",
|
|
633
839
|
message: `Device interface control "${controlId}" references missing context "${contextId}"`,
|
|
634
840
|
componentId: controlId,
|
|
635
841
|
property,
|
|
@@ -640,8 +846,8 @@ function validateContextList(controlId, property, values, contextIds) {
|
|
|
640
846
|
}
|
|
641
847
|
function emptyApplicabilityIssue(controlId, property) {
|
|
642
848
|
return {
|
|
643
|
-
code:
|
|
644
|
-
severity:
|
|
849
|
+
code: "device-interface-context-unresolved",
|
|
850
|
+
severity: "warning",
|
|
645
851
|
message: `Device interface control "${controlId}" has empty ${property}; omit the predicate instead`,
|
|
646
852
|
componentId: controlId,
|
|
647
853
|
property,
|
|
@@ -649,43 +855,46 @@ function emptyApplicabilityIssue(controlId, property) {
|
|
|
649
855
|
}
|
|
650
856
|
function validateDeviceInterfaceBinding(control, binding, componentIds, externalInterfaceIds, componentsById, resolvedPanelElements) {
|
|
651
857
|
const issues = [];
|
|
652
|
-
if (binding.externalInterfaceId !== undefined &&
|
|
858
|
+
if (binding.externalInterfaceId !== undefined &&
|
|
859
|
+
!externalInterfaceIds.has(binding.externalInterfaceId)) {
|
|
653
860
|
issues.push({
|
|
654
|
-
code:
|
|
655
|
-
severity:
|
|
861
|
+
code: "device-interface-binding-unresolved",
|
|
862
|
+
severity: "warning",
|
|
656
863
|
message: `Device interface control "${control.id}" references missing external interface "${binding.externalInterfaceId}"`,
|
|
657
864
|
componentId: control.id,
|
|
658
|
-
property:
|
|
865
|
+
property: "binding.externalInterfaceId",
|
|
659
866
|
});
|
|
660
867
|
}
|
|
661
868
|
if (!componentIds.has(binding.componentId)) {
|
|
662
869
|
issues.push({
|
|
663
|
-
code:
|
|
664
|
-
severity:
|
|
870
|
+
code: "device-interface-binding-unresolved",
|
|
871
|
+
severity: "warning",
|
|
665
872
|
message: `Device interface control "${control.id}" references missing component "${binding.componentId}"`,
|
|
666
873
|
componentId: control.id,
|
|
667
|
-
property:
|
|
874
|
+
property: "binding.componentId",
|
|
668
875
|
});
|
|
669
876
|
return issues;
|
|
670
877
|
}
|
|
671
|
-
if (binding.controlId !== undefined
|
|
672
|
-
|
|
878
|
+
if (binding.controlId !== undefined &&
|
|
879
|
+
!resolvedPanelElements.some((resolved) => resolved.componentId === binding.componentId &&
|
|
880
|
+
resolved.id === binding.controlId)) {
|
|
673
881
|
issues.push({
|
|
674
|
-
code:
|
|
675
|
-
severity:
|
|
882
|
+
code: "device-interface-binding-unresolved",
|
|
883
|
+
severity: "warning",
|
|
676
884
|
message: `Device interface control "${control.id}" references missing control "${binding.controlId}"`,
|
|
677
885
|
componentId: control.id,
|
|
678
|
-
property:
|
|
886
|
+
property: "binding.controlId",
|
|
679
887
|
});
|
|
680
888
|
}
|
|
681
889
|
const component = componentsById.get(binding.componentId);
|
|
682
|
-
if (binding.property !== undefined &&
|
|
890
|
+
if (binding.property !== undefined &&
|
|
891
|
+
component?.properties[binding.property] === undefined) {
|
|
683
892
|
issues.push({
|
|
684
|
-
code:
|
|
685
|
-
severity:
|
|
893
|
+
code: "device-interface-binding-unresolved",
|
|
894
|
+
severity: "warning",
|
|
686
895
|
message: `Device interface control "${control.id}" references missing property "${binding.property}"`,
|
|
687
896
|
componentId: control.id,
|
|
688
|
-
property:
|
|
897
|
+
property: "binding.property",
|
|
689
898
|
});
|
|
690
899
|
}
|
|
691
900
|
return issues;
|
|
@@ -695,23 +904,30 @@ function validateDuplicateDeviceInterfaceRoles(controls, groups) {
|
|
|
695
904
|
const layoutsByControlId = deviceInterfaceRoleLayoutsByControlId(groups);
|
|
696
905
|
const seen = new Map();
|
|
697
906
|
for (const control of controls) {
|
|
698
|
-
const layouts = layoutsByControlId.get(control.id) ?? [
|
|
907
|
+
const layouts = layoutsByControlId.get(control.id) ?? [
|
|
908
|
+
{ groupId: control.groupId ?? "", order: control.order },
|
|
909
|
+
];
|
|
699
910
|
for (const layout of layouts) {
|
|
700
911
|
const key = `${layout.groupId}:${control.role}`;
|
|
701
912
|
const existing = seen.get(key);
|
|
702
|
-
if (existing !== undefined &&
|
|
703
|
-
|
|
704
|
-
|
|
913
|
+
if (existing !== undefined &&
|
|
914
|
+
existing.order === undefined &&
|
|
915
|
+
layout.order === undefined) {
|
|
916
|
+
if (deviceInterfaceBindingSignature(existing.control.binding) ===
|
|
917
|
+
deviceInterfaceBindingSignature(control.binding)) {
|
|
705
918
|
issues.push({
|
|
706
|
-
code:
|
|
707
|
-
severity:
|
|
919
|
+
code: "device-interface-duplicate-role",
|
|
920
|
+
severity: "warning",
|
|
708
921
|
message: `Device interface controls "${existing.control.id}" and "${control.id}" share role "${control.role}" without order or distinct binding`,
|
|
709
922
|
componentId: control.id,
|
|
710
|
-
property:
|
|
923
|
+
property: "role",
|
|
711
924
|
});
|
|
712
925
|
}
|
|
713
926
|
}
|
|
714
|
-
seen.set(key, {
|
|
927
|
+
seen.set(key, {
|
|
928
|
+
control,
|
|
929
|
+
...(layout.order === undefined ? {} : { order: layout.order }),
|
|
930
|
+
});
|
|
715
931
|
}
|
|
716
932
|
}
|
|
717
933
|
return issues;
|
|
@@ -732,15 +948,111 @@ function deviceInterfaceRoleLayoutsByControlId(groups) {
|
|
|
732
948
|
}
|
|
733
949
|
function deviceInterfaceBindingSignature(binding) {
|
|
734
950
|
if (binding === undefined) {
|
|
735
|
-
return
|
|
951
|
+
return "";
|
|
736
952
|
}
|
|
737
953
|
return [
|
|
738
954
|
binding.componentId,
|
|
739
|
-
binding.controlId ??
|
|
740
|
-
binding.controlName ??
|
|
741
|
-
binding.property ??
|
|
742
|
-
binding.externalInterfaceId ??
|
|
743
|
-
].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;
|
|
744
1056
|
}
|
|
745
1057
|
function validatePanel(doc, componentIds, semanticControlIds) {
|
|
746
1058
|
if (doc.panel === undefined) {
|
|
@@ -751,19 +1063,20 @@ function validatePanel(doc, componentIds, semanticControlIds) {
|
|
|
751
1063
|
for (const face of doc.panel.faces) {
|
|
752
1064
|
for (const element of face.elements) {
|
|
753
1065
|
const componentId = element.bind.componentId;
|
|
754
|
-
if (element.interfaceControlId !== undefined &&
|
|
1066
|
+
if (element.interfaceControlId !== undefined &&
|
|
1067
|
+
!semanticControlIds.has(element.interfaceControlId)) {
|
|
755
1068
|
issues.push({
|
|
756
|
-
code:
|
|
757
|
-
severity:
|
|
1069
|
+
code: "panel-interface-control-unresolved",
|
|
1070
|
+
severity: "warning",
|
|
758
1071
|
message: `Panel element on face "${face.id}" references missing interface control "${element.interfaceControlId}"`,
|
|
759
1072
|
componentId: element.interfaceControlId,
|
|
760
|
-
property:
|
|
1073
|
+
property: "interfaceControlId",
|
|
761
1074
|
});
|
|
762
1075
|
}
|
|
763
1076
|
if (!componentIds.has(componentId)) {
|
|
764
1077
|
issues.push({
|
|
765
|
-
code:
|
|
766
|
-
severity:
|
|
1078
|
+
code: "panel-binding-unresolved",
|
|
1079
|
+
severity: "warning",
|
|
767
1080
|
message: `Panel element on face "${face.id}" references missing component "${componentId}"`,
|
|
768
1081
|
componentId,
|
|
769
1082
|
});
|
|
@@ -772,18 +1085,19 @@ function validatePanel(doc, componentIds, semanticControlIds) {
|
|
|
772
1085
|
const resolved = resolvePanelElement(resolvedElements, element);
|
|
773
1086
|
if (element.bind.controlId !== undefined && resolved === undefined) {
|
|
774
1087
|
issues.push({
|
|
775
|
-
code:
|
|
776
|
-
severity:
|
|
1088
|
+
code: "panel-control-unresolved",
|
|
1089
|
+
severity: "warning",
|
|
777
1090
|
message: `Panel element on face "${face.id}" references missing control "${element.bind.controlId}" on component "${componentId}"`,
|
|
778
1091
|
componentId,
|
|
779
1092
|
property: element.bind.controlId,
|
|
780
1093
|
});
|
|
781
1094
|
continue;
|
|
782
1095
|
}
|
|
783
|
-
if (resolved !== undefined &&
|
|
1096
|
+
if (resolved !== undefined &&
|
|
1097
|
+
!panelKindsCompatible(element.kind, resolved.kind)) {
|
|
784
1098
|
issues.push({
|
|
785
|
-
code:
|
|
786
|
-
severity:
|
|
1099
|
+
code: "panel-kind-mismatch",
|
|
1100
|
+
severity: "warning",
|
|
787
1101
|
message: `Panel element on face "${face.id}" binds component "${componentId}" as ${element.kind} but resolved kind is ${resolved.kind}`,
|
|
788
1102
|
componentId,
|
|
789
1103
|
});
|
|
@@ -799,7 +1113,8 @@ function panelKindsCompatible(declared, resolved) {
|
|
|
799
1113
|
if (declared === resolved) {
|
|
800
1114
|
return true;
|
|
801
1115
|
}
|
|
802
|
-
return resolved ===
|
|
1116
|
+
return (resolved === "switch" &&
|
|
1117
|
+
(declared === "selector" || declared === "footswitch"));
|
|
803
1118
|
}
|
|
804
1119
|
function resolvePanelElements(doc) {
|
|
805
1120
|
const panel = extractPanel(doc);
|
|
@@ -808,47 +1123,51 @@ function resolvePanelElements(doc) {
|
|
|
808
1123
|
resolved.push({
|
|
809
1124
|
id: knob.id,
|
|
810
1125
|
componentId: componentIdFromPanelElementId(knob.id),
|
|
811
|
-
kind: knob.id.endsWith(
|
|
1126
|
+
kind: knob.id.endsWith(":mode") && knob.controlMode === "stepped"
|
|
1127
|
+
? "switch"
|
|
1128
|
+
: "knob",
|
|
812
1129
|
});
|
|
813
1130
|
}
|
|
814
1131
|
for (const slider of panel.sliders ?? []) {
|
|
815
1132
|
resolved.push({
|
|
816
1133
|
id: slider.id,
|
|
817
1134
|
componentId: componentIdFromPanelElementId(slider.id),
|
|
818
|
-
kind:
|
|
1135
|
+
kind: "slider",
|
|
819
1136
|
});
|
|
820
1137
|
}
|
|
821
1138
|
for (const switchControl of panel.switches) {
|
|
822
1139
|
resolved.push({
|
|
823
1140
|
id: switchControl.id,
|
|
824
1141
|
componentId: componentIdFromPanelElementId(switchControl.id),
|
|
825
|
-
kind:
|
|
1142
|
+
kind: "switch",
|
|
826
1143
|
});
|
|
827
1144
|
}
|
|
828
1145
|
for (const led of panel.leds) {
|
|
829
1146
|
resolved.push({
|
|
830
1147
|
id: led.id,
|
|
831
1148
|
componentId: componentIdFromPanelElementId(led.id),
|
|
832
|
-
kind:
|
|
1149
|
+
kind: "led",
|
|
833
1150
|
});
|
|
834
1151
|
}
|
|
835
1152
|
for (const jack of panel.jacks) {
|
|
836
1153
|
resolved.push({
|
|
837
1154
|
id: jack.id,
|
|
838
1155
|
componentId: jack.sourceComponentId ?? componentIdFromPanelElementId(jack.id),
|
|
839
|
-
kind:
|
|
1156
|
+
kind: "jack",
|
|
840
1157
|
});
|
|
841
1158
|
}
|
|
842
1159
|
return resolved;
|
|
843
1160
|
}
|
|
844
1161
|
function resolvePanelElement(resolvedElements, element) {
|
|
845
1162
|
if (element.bind.controlId !== undefined) {
|
|
846
|
-
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);
|
|
847
1165
|
}
|
|
848
|
-
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);
|
|
849
1168
|
}
|
|
850
1169
|
function componentIdFromPanelElementId(id) {
|
|
851
|
-
const separator = id.indexOf(
|
|
1170
|
+
const separator = id.indexOf(":");
|
|
852
1171
|
return separator <= 0 ? id : id.slice(0, separator);
|
|
853
1172
|
}
|
|
854
1173
|
function validatePanelCellCollisions(face) {
|
|
@@ -864,8 +1183,8 @@ function validatePanelCellCollisions(face) {
|
|
|
864
1183
|
const key = `${row}:${column}`;
|
|
865
1184
|
if (occupied.has(key)) {
|
|
866
1185
|
issues.push({
|
|
867
|
-
code:
|
|
868
|
-
severity:
|
|
1186
|
+
code: "panel-cell-collision",
|
|
1187
|
+
severity: "warning",
|
|
869
1188
|
message: `Panel face "${face.id}" has overlapping elements at row ${row}, column ${column}`,
|
|
870
1189
|
componentId: element.bind.componentId,
|
|
871
1190
|
});
|
|
@@ -897,21 +1216,21 @@ function validateV3BuildMetadata(doc, componentIds) {
|
|
|
897
1216
|
]));
|
|
898
1217
|
const selectedBoardId = doc.build?.selectedBoardId;
|
|
899
1218
|
if (selectedBoardId !== undefined && !boardsById.has(selectedBoardId)) {
|
|
900
|
-
issues.push(unresolvedIssue(
|
|
1219
|
+
issues.push(unresolvedIssue("build-board-unresolved", "error", `Build selectedBoardId references missing board "${selectedBoardId}"`, selectedBoardId, "selectedBoardId"));
|
|
901
1220
|
}
|
|
902
1221
|
for (const boardId of doc.build?.alternateBoardIds ?? []) {
|
|
903
1222
|
if (!boardsById.has(boardId)) {
|
|
904
|
-
issues.push(unresolvedIssue(
|
|
1223
|
+
issues.push(unresolvedIssue("build-board-unresolved", "warning", `Build alternateBoardIds references missing board "${boardId}"`, boardId, "alternateBoardIds"));
|
|
905
1224
|
}
|
|
906
1225
|
}
|
|
907
|
-
const preferredBoardId = dataString(doc.mechanical?.internalBoard,
|
|
1226
|
+
const preferredBoardId = dataString(doc.mechanical?.internalBoard, "preferredBoardId");
|
|
908
1227
|
if (preferredBoardId !== undefined && !boardsById.has(preferredBoardId)) {
|
|
909
|
-
issues.push(unresolvedIssue(
|
|
1228
|
+
issues.push(unresolvedIssue("build-board-unresolved", "warning", `Mechanical internalBoard.preferredBoardId references missing board "${preferredBoardId}"`, preferredBoardId, "mechanical.internalBoard.preferredBoardId"));
|
|
910
1229
|
}
|
|
911
1230
|
const harnessesById = new Map(doc.offBoardWiring?.harnesses.map((harness) => [harness.id, harness]) ?? []);
|
|
912
1231
|
for (const harnessId of doc.build?.selectedOffBoardWiringHarnessIds ?? []) {
|
|
913
1232
|
if (!harnessesById.has(harnessId)) {
|
|
914
|
-
issues.push(unresolvedIssue(
|
|
1233
|
+
issues.push(unresolvedIssue("build-harness-unresolved", "error", `Build selectedOffBoardWiringHarnessIds references missing harness "${harnessId}"`, harnessId, "selectedOffBoardWiringHarnessIds"));
|
|
915
1234
|
}
|
|
916
1235
|
}
|
|
917
1236
|
for (const item of doc.bom?.items ?? []) {
|
|
@@ -928,7 +1247,8 @@ function validateV3BuildMetadata(doc, componentIds) {
|
|
|
928
1247
|
if (doc.offBoardWiring !== undefined) {
|
|
929
1248
|
issues.push(...validateOffBoardWiring(doc, componentsById, panelElementIds, boardTerminalsByBoardId, boardNetsByBoardId));
|
|
930
1249
|
}
|
|
931
|
-
if (doc.build?.completeness ===
|
|
1250
|
+
if (doc.build?.completeness === "complete-selected-build" &&
|
|
1251
|
+
selectedBoardId !== undefined) {
|
|
932
1252
|
const selectedBoard = boardsById.get(selectedBoardId);
|
|
933
1253
|
if (selectedBoard !== undefined) {
|
|
934
1254
|
issues.push(...validateCompleteSelectedBoardRoutes(selectedBoard));
|
|
@@ -937,7 +1257,7 @@ function validateV3BuildMetadata(doc, componentIds) {
|
|
|
937
1257
|
return issues;
|
|
938
1258
|
}
|
|
939
1259
|
function hasV3BuildMetadata(doc) {
|
|
940
|
-
return doc.mechanical !== undefined ||
|
|
1260
|
+
return (doc.mechanical !== undefined ||
|
|
941
1261
|
doc.build !== undefined ||
|
|
942
1262
|
doc.bom !== undefined ||
|
|
943
1263
|
doc.partProfiles !== undefined ||
|
|
@@ -945,71 +1265,78 @@ function hasV3BuildMetadata(doc) {
|
|
|
945
1265
|
doc.offBoardWiring !== undefined ||
|
|
946
1266
|
doc.boards !== undefined ||
|
|
947
1267
|
doc.panel?.faces.some((face) => face.geometry !== undefined ||
|
|
948
|
-
face.elements.some((element) => element.id !== undefined || element.physical !== undefined)) === true;
|
|
1268
|
+
face.elements.some((element) => element.id !== undefined || element.physical !== undefined)) === true);
|
|
949
1269
|
}
|
|
950
1270
|
function validateBomRef(ref, componentIds, controlIds, panelElementIds, boardsById, itemId) {
|
|
951
|
-
if (ref.kind ===
|
|
952
|
-
|
|
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");
|
|
953
1274
|
}
|
|
954
|
-
if (ref.kind ===
|
|
1275
|
+
if (ref.kind === "device-interface-control" &&
|
|
955
1276
|
(ref.controlId === undefined || !controlIds.has(ref.controlId))) {
|
|
956
|
-
return unresolvedIssue(
|
|
1277
|
+
return unresolvedIssue("bom-ref-unresolved", "warning", `BOM item "${itemId}" references missing device interface control "${ref.controlId ?? ""}"`, itemId, "refs.controlId");
|
|
957
1278
|
}
|
|
958
|
-
if (ref.kind ===
|
|
959
|
-
|
|
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");
|
|
960
1283
|
}
|
|
961
|
-
if (ref.kind ===
|
|
962
|
-
|
|
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");
|
|
963
1287
|
}
|
|
964
1288
|
return undefined;
|
|
965
1289
|
}
|
|
966
1290
|
function validateBoardRealization(board, componentsById, boardNetsByBoardId) {
|
|
967
1291
|
const issues = [];
|
|
968
|
-
if (board.sourceCircuit !== undefined &&
|
|
1292
|
+
if (board.sourceCircuit !== undefined &&
|
|
1293
|
+
!isDigestShapedSourceHash(board.sourceCircuit.hash)) {
|
|
969
1294
|
issues.push({
|
|
970
|
-
code:
|
|
971
|
-
severity:
|
|
1295
|
+
code: "board-source-hash-invalid",
|
|
1296
|
+
severity: "error",
|
|
972
1297
|
message: `Board "${board.id}" sourceCircuit.hash must be sha256:<64 hex chars>`,
|
|
973
1298
|
componentId: board.id,
|
|
974
|
-
property:
|
|
1299
|
+
property: "sourceCircuit.hash",
|
|
975
1300
|
});
|
|
976
1301
|
}
|
|
977
1302
|
for (const terminal of board.edgeTerminals) {
|
|
978
|
-
if (terminal.terminalRef !== undefined &&
|
|
979
|
-
|
|
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));
|
|
980
1306
|
}
|
|
981
1307
|
}
|
|
982
1308
|
for (const placement of board.footprintPlacements) {
|
|
983
1309
|
if (!componentsById.has(placement.componentId)) {
|
|
984
|
-
issues.push(unresolvedIssue(
|
|
1310
|
+
issues.push(unresolvedIssue("board-terminal-unresolved", "warning", `Board "${board.id}" places missing component "${placement.componentId}"`, board.id, placement.componentId));
|
|
985
1311
|
continue;
|
|
986
1312
|
}
|
|
987
1313
|
for (const pad of placement.pads) {
|
|
988
1314
|
if (pad.terminalName !== undefined &&
|
|
989
1315
|
!componentHasTerminal(componentsById, placement.componentId, pad.terminalName)) {
|
|
990
|
-
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));
|
|
991
1317
|
}
|
|
992
1318
|
}
|
|
993
1319
|
}
|
|
994
1320
|
for (const net of board.netlist?.nets ?? []) {
|
|
995
1321
|
for (const member of net.members) {
|
|
996
1322
|
if (!componentTerminalExists(componentsById, member)) {
|
|
997
|
-
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));
|
|
998
1324
|
}
|
|
999
1325
|
}
|
|
1000
1326
|
}
|
|
1001
1327
|
for (const route of board.routes) {
|
|
1002
1328
|
if (route.zones !== undefined || route.drills !== undefined) {
|
|
1003
1329
|
issues.push({
|
|
1004
|
-
code:
|
|
1005
|
-
severity:
|
|
1330
|
+
code: "board-route-feature-invalid",
|
|
1331
|
+
severity: "error",
|
|
1006
1332
|
message: `Board "${board.id}" route "${route.id}" contains board-level zones or drills`,
|
|
1007
1333
|
componentId: board.id,
|
|
1008
1334
|
property: route.id,
|
|
1009
1335
|
});
|
|
1010
1336
|
}
|
|
1011
|
-
if (isBoardNetlistRef(route.netRef) &&
|
|
1012
|
-
|
|
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));
|
|
1013
1340
|
}
|
|
1014
1341
|
}
|
|
1015
1342
|
return issues;
|
|
@@ -1029,10 +1356,10 @@ function validateOffBoardWiring(doc, componentsById, panelElementIds, boardTermi
|
|
|
1029
1356
|
}
|
|
1030
1357
|
for (const connection of harness.connections) {
|
|
1031
1358
|
if (!localEndpointIds.has(connection.fromEndpointId)) {
|
|
1032
|
-
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));
|
|
1033
1360
|
}
|
|
1034
1361
|
if (!localEndpointIds.has(connection.toEndpointId)) {
|
|
1035
|
-
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));
|
|
1036
1363
|
}
|
|
1037
1364
|
if (connection.signalRef !== undefined) {
|
|
1038
1365
|
const issue = validateOffBoardSignalRef(connection.signalRef, componentsById, boardNetsByBoardId, harness.id);
|
|
@@ -1048,33 +1375,38 @@ function validateOffBoardWiring(doc, componentsById, panelElementIds, boardTermi
|
|
|
1048
1375
|
continue;
|
|
1049
1376
|
}
|
|
1050
1377
|
for (const connection of harness.connections) {
|
|
1051
|
-
if (!endpointIds.has(connection.fromEndpointId) ||
|
|
1052
|
-
|
|
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));
|
|
1053
1381
|
}
|
|
1054
1382
|
}
|
|
1055
1383
|
}
|
|
1056
1384
|
return issues;
|
|
1057
1385
|
}
|
|
1058
1386
|
function validateOffBoardEndpoint(endpoint, componentsById, panelElementIds, boardTerminalsByBoardId) {
|
|
1059
|
-
if (endpoint.kind ===
|
|
1060
|
-
const terminalIds = endpoint.boardId === undefined
|
|
1061
|
-
|
|
1062
|
-
|
|
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");
|
|
1063
1395
|
}
|
|
1064
1396
|
return undefined;
|
|
1065
1397
|
}
|
|
1066
|
-
if (endpoint.kind ===
|
|
1067
|
-
endpoint.kind ===
|
|
1068
|
-
endpoint.kind ===
|
|
1398
|
+
if (endpoint.kind === "panel-component-terminal" ||
|
|
1399
|
+
endpoint.kind === "power-terminal" ||
|
|
1400
|
+
endpoint.kind === "footswitch-terminal") {
|
|
1069
1401
|
if (endpoint.componentId === undefined ||
|
|
1070
1402
|
endpoint.terminalName === undefined ||
|
|
1071
1403
|
!componentHasTerminal(componentsById, endpoint.componentId, endpoint.terminalName)) {
|
|
1072
|
-
return unresolvedIssue(
|
|
1404
|
+
return unresolvedIssue("offboard-endpoint-unresolved", "error", `Off-board endpoint "${endpoint.id}" references missing component terminal`, endpoint.id, "componentId");
|
|
1073
1405
|
}
|
|
1074
1406
|
if (endpoint.panelElementId !== undefined &&
|
|
1075
|
-
endpoint.kind !==
|
|
1407
|
+
endpoint.kind !== "power-terminal" &&
|
|
1076
1408
|
!panelElementIds.has(endpoint.panelElementId)) {
|
|
1077
|
-
return unresolvedIssue(
|
|
1409
|
+
return unresolvedIssue("offboard-endpoint-unresolved", "warning", `Off-board endpoint "${endpoint.id}" references missing panel element "${endpoint.panelElementId}"`, endpoint.id, "panelElementId");
|
|
1078
1410
|
}
|
|
1079
1411
|
}
|
|
1080
1412
|
return undefined;
|
|
@@ -1082,16 +1414,18 @@ function validateOffBoardEndpoint(endpoint, componentsById, panelElementIds, boa
|
|
|
1082
1414
|
function validateOffBoardSignalRef(signalRef, componentsById, boardNetsByBoardId, harnessId) {
|
|
1083
1415
|
if (isBoardNetlistRef(signalRef)) {
|
|
1084
1416
|
if (!boardNetRefExists(signalRef, signalRef.boardId, boardNetsByBoardId)) {
|
|
1085
|
-
return unresolvedIssue(
|
|
1417
|
+
return unresolvedIssue("offboard-signal-unresolved", "error", `Harness "${harnessId}" references missing board net "${signalRef.netId}"`, harnessId, "signalRef");
|
|
1086
1418
|
}
|
|
1087
1419
|
return undefined;
|
|
1088
1420
|
}
|
|
1089
|
-
const member = dataObject(signalRef,
|
|
1090
|
-
const componentId = dataString(member,
|
|
1091
|
-
const terminalName = dataString(member,
|
|
1092
|
-
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) {
|
|
1093
1427
|
if (!componentHasTerminal(componentsById, componentId, terminalName)) {
|
|
1094
|
-
return unresolvedIssue(
|
|
1428
|
+
return unresolvedIssue("offboard-signal-unresolved", "error", `Harness "${harnessId}" references missing canonical component terminal`, harnessId, "signalRef");
|
|
1095
1429
|
}
|
|
1096
1430
|
}
|
|
1097
1431
|
return undefined;
|
|
@@ -1100,7 +1434,7 @@ function validateCompleteSelectedBoardRoutes(board) {
|
|
|
1100
1434
|
const issues = [];
|
|
1101
1435
|
const routedNetIds = new Set(board.routes
|
|
1102
1436
|
.filter((route) => isRouteForBoardNet(route, board.id))
|
|
1103
|
-
.map((route) => dataString(route.netRef,
|
|
1437
|
+
.map((route) => dataString(route.netRef, "netId"))
|
|
1104
1438
|
.filter((netId) => netId !== undefined));
|
|
1105
1439
|
for (const net of board.netlist?.nets ?? []) {
|
|
1106
1440
|
if (isSingleTerminalEdgeNet(net)) {
|
|
@@ -1108,8 +1442,8 @@ function validateCompleteSelectedBoardRoutes(board) {
|
|
|
1108
1442
|
}
|
|
1109
1443
|
if (!routedNetIds.has(net.id)) {
|
|
1110
1444
|
issues.push({
|
|
1111
|
-
code:
|
|
1112
|
-
severity:
|
|
1445
|
+
code: "board-net-unrouted",
|
|
1446
|
+
severity: "error",
|
|
1113
1447
|
message: `Selected board "${board.id}" net "${net.id}" has multiple members but no route`,
|
|
1114
1448
|
componentId: board.id,
|
|
1115
1449
|
property: net.id,
|
|
@@ -1128,7 +1462,8 @@ function isRouteForBoardNet(route, boardId) {
|
|
|
1128
1462
|
return route.netRef.boardId === undefined || route.netRef.boardId === boardId;
|
|
1129
1463
|
}
|
|
1130
1464
|
function isBoardNetlistRef(value) {
|
|
1131
|
-
return dataString(value,
|
|
1465
|
+
return (dataString(value, "source") === "board-netlist" &&
|
|
1466
|
+
dataString(value, "netId") !== undefined);
|
|
1132
1467
|
}
|
|
1133
1468
|
function boardNetRefExists(ref, fallbackBoardId, boardNetsByBoardId) {
|
|
1134
1469
|
const boardId = ref.boardId ?? fallbackBoardId;
|
|
@@ -1156,21 +1491,23 @@ function componentTerminalExists(componentsById, ref) {
|
|
|
1156
1491
|
return componentHasTerminal(componentsById, ref.componentId, ref.terminalName);
|
|
1157
1492
|
}
|
|
1158
1493
|
function componentHasTerminal(componentsById, componentId, terminalName) {
|
|
1159
|
-
return componentsById
|
|
1494
|
+
return (componentsById
|
|
1495
|
+
.get(componentId)
|
|
1496
|
+
?.terminals.some((terminal) => terminal.name === terminalName) === true);
|
|
1160
1497
|
}
|
|
1161
1498
|
function isDigestShapedSourceHash(hash) {
|
|
1162
1499
|
return /^sha256:[0-9a-f]{64}$/i.test(hash);
|
|
1163
1500
|
}
|
|
1164
1501
|
function dataString(object, key) {
|
|
1165
1502
|
const value = object?.[key];
|
|
1166
|
-
return typeof value ===
|
|
1503
|
+
return typeof value === "string" ? value : undefined;
|
|
1167
1504
|
}
|
|
1168
1505
|
function dataObject(object, key) {
|
|
1169
1506
|
const value = object?.[key];
|
|
1170
1507
|
return isBuildDataObject(value) ? value : undefined;
|
|
1171
1508
|
}
|
|
1172
1509
|
function isBuildDataObject(value) {
|
|
1173
|
-
return typeof value ===
|
|
1510
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1174
1511
|
}
|
|
1175
1512
|
function unresolvedIssue(code, severity, message, componentId, property) {
|
|
1176
1513
|
return {
|
|
@@ -1183,8 +1520,8 @@ function unresolvedIssue(code, severity, message, componentId, property) {
|
|
|
1183
1520
|
}
|
|
1184
1521
|
function missingPropertyIssue(component, rule) {
|
|
1185
1522
|
return {
|
|
1186
|
-
code: rule.kind ===
|
|
1187
|
-
severity:
|
|
1523
|
+
code: rule.kind === "string" ? "model-required" : "value-required",
|
|
1524
|
+
severity: "error",
|
|
1188
1525
|
message: `${component.id} (${component.kind}): missing required property "${rule.name}"`,
|
|
1189
1526
|
componentId: component.id,
|
|
1190
1527
|
property: rule.name,
|