@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.
Files changed (182) hide show
  1. package/README.md +10 -2
  2. package/dist/editor/commands.d.ts +13 -13
  3. package/dist/editor/commands.d.ts.map +1 -1
  4. package/dist/editor/commands.js +44 -29
  5. package/dist/editor/commands.js.map +1 -1
  6. package/dist/editor/factory.d.ts +1 -1
  7. package/dist/editor/factory.d.ts.map +1 -1
  8. package/dist/editor/factory.js +51 -51
  9. package/dist/editor/factory.js.map +1 -1
  10. package/dist/editor/history.d.ts +7 -7
  11. package/dist/editor/history.d.ts.map +1 -1
  12. package/dist/editor/history.js +20 -12
  13. package/dist/editor/history.js.map +1 -1
  14. package/dist/editor/index.d.ts +8 -8
  15. package/dist/editor/index.d.ts.map +1 -1
  16. package/dist/editor/index.js +4 -4
  17. package/dist/editor/index.js.map +1 -1
  18. package/dist/editor/layout.d.ts +1 -1
  19. package/dist/editor/layout.d.ts.map +1 -1
  20. package/dist/editor/layout.js +11 -6
  21. package/dist/editor/layout.js.map +1 -1
  22. package/dist/formats/circuit-json/serializer.d.ts +15 -15
  23. package/dist/formats/circuit-json/serializer.d.ts.map +1 -1
  24. package/dist/formats/circuit-json/serializer.js +486 -394
  25. package/dist/formats/circuit-json/serializer.js.map +1 -1
  26. package/dist/formats/document.d.ts +6 -6
  27. package/dist/formats/document.d.ts.map +1 -1
  28. package/dist/formats/document.js +112 -92
  29. package/dist/formats/document.js.map +1 -1
  30. package/dist/formats/interchange/parser.d.ts +1 -1
  31. package/dist/formats/interchange/parser.d.ts.map +1 -1
  32. package/dist/formats/interchange/parser.js +463 -286
  33. package/dist/formats/interchange/parser.js.map +1 -1
  34. package/dist/formats/interchange/serializer.d.ts +1 -1
  35. package/dist/formats/interchange/serializer.d.ts.map +1 -1
  36. package/dist/formats/interchange/serializer.js +41 -28
  37. package/dist/formats/interchange/serializer.js.map +1 -1
  38. package/dist/formats/ltspice/catalog.d.ts +1 -1
  39. package/dist/formats/ltspice/catalog.d.ts.map +1 -1
  40. package/dist/formats/ltspice/catalog.js +150 -48
  41. package/dist/formats/ltspice/catalog.js.map +1 -1
  42. package/dist/formats/ltspice/encoding.js +12 -40
  43. package/dist/formats/ltspice/encoding.js.map +1 -1
  44. package/dist/formats/ltspice/parser.d.ts +1 -1
  45. package/dist/formats/ltspice/parser.d.ts.map +1 -1
  46. package/dist/formats/ltspice/parser.js +122 -75
  47. package/dist/formats/ltspice/parser.js.map +1 -1
  48. package/dist/formats/ltspice/serializer.d.ts +1 -1
  49. package/dist/formats/ltspice/serializer.d.ts.map +1 -1
  50. package/dist/formats/ltspice/serializer.js +69 -47
  51. package/dist/formats/ltspice/serializer.js.map +1 -1
  52. package/dist/formats/schx/catalog.d.ts +1 -1
  53. package/dist/formats/schx/catalog.d.ts.map +1 -1
  54. package/dist/formats/schx/catalog.js +499 -254
  55. package/dist/formats/schx/catalog.js.map +1 -1
  56. package/dist/formats/schx/parser.d.ts +1 -1
  57. package/dist/formats/schx/parser.d.ts.map +1 -1
  58. package/dist/formats/schx/parser.js +40 -38
  59. package/dist/formats/schx/parser.js.map +1 -1
  60. package/dist/formats/schx/runtime-descriptors.d.ts +1 -1
  61. package/dist/formats/schx/runtime-descriptors.d.ts.map +1 -1
  62. package/dist/formats/schx/runtime-descriptors.js +239 -201
  63. package/dist/formats/schx/runtime-descriptors.js.map +1 -1
  64. package/dist/formats/schx/serializer.d.ts +2 -2
  65. package/dist/formats/schx/serializer.d.ts.map +1 -1
  66. package/dist/formats/schx/serializer.js +106 -106
  67. package/dist/formats/schx/serializer.js.map +1 -1
  68. package/dist/formats/schx/transforms.d.ts +1 -1
  69. package/dist/formats/schx/transforms.d.ts.map +1 -1
  70. package/dist/formats/schx/transforms.js +16 -8
  71. package/dist/formats/schx/transforms.js.map +1 -1
  72. package/dist/formats/spice/parser.d.ts +1 -1
  73. package/dist/formats/spice/parser.d.ts.map +1 -1
  74. package/dist/formats/spice/parser.js +105 -56
  75. package/dist/formats/spice/parser.js.map +1 -1
  76. package/dist/formats/spice/serializer.d.ts +1 -1
  77. package/dist/formats/spice/serializer.js +14 -12
  78. package/dist/formats/spice/serializer.js.map +1 -1
  79. package/dist/index.d.ts +47 -46
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +32 -31
  82. package/dist/index.js.map +1 -1
  83. package/dist/model/connectivity.d.ts +1 -1
  84. package/dist/model/connectivity.d.ts.map +1 -1
  85. package/dist/model/connectivity.js +22 -7
  86. package/dist/model/connectivity.js.map +1 -1
  87. package/dist/model/netlist.d.ts +3 -3
  88. package/dist/model/netlist.d.ts.map +1 -1
  89. package/dist/model/netlist.js +117 -100
  90. package/dist/model/netlist.js.map +1 -1
  91. package/dist/model/properties.d.ts +1 -1
  92. package/dist/model/properties.d.ts.map +1 -1
  93. package/dist/model/properties.js +16 -16
  94. package/dist/model/properties.js.map +1 -1
  95. package/dist/model/quantity.d.ts +1 -1
  96. package/dist/model/quantity.d.ts.map +1 -1
  97. package/dist/model/quantity.js +35 -35
  98. package/dist/model/quantity.js.map +1 -1
  99. package/dist/model/types.d.ts +61 -33
  100. package/dist/model/types.d.ts.map +1 -1
  101. package/dist/model/types.js +1 -1
  102. package/dist/model/types.js.map +1 -1
  103. package/dist/model/validation.d.ts +5 -5
  104. package/dist/model/validation.d.ts.map +1 -1
  105. package/dist/model/validation.js +668 -331
  106. package/dist/model/validation.js.map +1 -1
  107. package/dist/model/wires.d.ts +1 -1
  108. package/dist/model/wires.d.ts.map +1 -1
  109. package/dist/model/wires.js +4 -1
  110. package/dist/model/wires.js.map +1 -1
  111. package/dist/panel/extract.d.ts +2 -2
  112. package/dist/panel/extract.d.ts.map +1 -1
  113. package/dist/panel/extract.js +327 -225
  114. package/dist/panel/extract.js.map +1 -1
  115. package/dist/panel/index.d.ts +7 -7
  116. package/dist/panel/index.d.ts.map +1 -1
  117. package/dist/panel/index.js +5 -5
  118. package/dist/panel/index.js.map +1 -1
  119. package/dist/panel/knobs.d.ts +4 -4
  120. package/dist/panel/knobs.d.ts.map +1 -1
  121. package/dist/panel/knobs.js +1 -1
  122. package/dist/panel/knobs.js.map +1 -1
  123. package/dist/panel/placement.d.ts +1 -1
  124. package/dist/panel/placement.d.ts.map +1 -1
  125. package/dist/panel/placement.js +11 -9
  126. package/dist/panel/placement.js.map +1 -1
  127. package/dist/panel/protocol.d.ts +1 -1
  128. package/dist/panel/protocol.d.ts.map +1 -1
  129. package/dist/panel/protocol.js +32 -23
  130. package/dist/panel/protocol.js.map +1 -1
  131. package/dist/panel/types.d.ts +18 -18
  132. package/dist/panel/types.d.ts.map +1 -1
  133. package/dist/panel/types.js.map +1 -1
  134. package/dist/preview/bounds.d.ts +1 -1
  135. package/dist/preview/bounds.d.ts.map +1 -1
  136. package/dist/preview/bounds.js +3 -3
  137. package/dist/preview/bounds.js.map +1 -1
  138. package/dist/preview/box-layout.d.ts +2 -2
  139. package/dist/preview/box-layout.js.map +1 -1
  140. package/dist/preview/colors.d.ts +1 -1
  141. package/dist/preview/colors.js +35 -35
  142. package/dist/preview/colors.js.map +1 -1
  143. package/dist/preview/hanging.d.ts +1 -1
  144. package/dist/preview/hanging.d.ts.map +1 -1
  145. package/dist/preview/hanging.js +4 -1
  146. package/dist/preview/hanging.js.map +1 -1
  147. package/dist/preview/junctions.d.ts +1 -1
  148. package/dist/preview/junctions.d.ts.map +1 -1
  149. package/dist/preview/junctions.js.map +1 -1
  150. package/dist/preview/label-layout.d.ts.map +1 -1
  151. package/dist/preview/label-layout.js +4 -4
  152. package/dist/preview/label-layout.js.map +1 -1
  153. package/dist/preview/ports.d.ts +1 -1
  154. package/dist/preview/ports.d.ts.map +1 -1
  155. package/dist/preview/ports.js +2 -1
  156. package/dist/preview/ports.js.map +1 -1
  157. package/dist/preview/renderable-wires.d.ts +1 -1
  158. package/dist/preview/renderable-wires.d.ts.map +1 -1
  159. package/dist/preview/renderable-wires.js +3 -1
  160. package/dist/preview/renderable-wires.js.map +1 -1
  161. package/dist/preview/routing.d.ts +1 -1
  162. package/dist/preview/routing.js +1 -1
  163. package/dist/preview/routing.js.map +1 -1
  164. package/dist/preview/snap.d.ts +1 -1
  165. package/dist/preview/snap.d.ts.map +1 -1
  166. package/dist/preview/snap.js +11 -3
  167. package/dist/preview/snap.js.map +1 -1
  168. package/dist/preview/symbols/svg-content.d.ts.map +1 -1
  169. package/dist/preview/symbols/svg-content.js +200 -50
  170. package/dist/preview/symbols/svg-content.js.map +1 -1
  171. package/dist/preview/symbols.d.ts +2 -2
  172. package/dist/preview/symbols.d.ts.map +1 -1
  173. package/dist/preview/symbols.js +100 -97
  174. package/dist/preview/symbols.js.map +1 -1
  175. package/dist/preview/wire-chains.d.ts +1 -1
  176. package/dist/preview/wire-chains.d.ts.map +1 -1
  177. package/dist/preview/wire-chains.js.map +1 -1
  178. package/dist/profiles.d.ts +600 -0
  179. package/dist/profiles.d.ts.map +1 -0
  180. package/dist/profiles.js +118 -0
  181. package/dist/profiles.js.map +1 -0
  182. package/package.json +54 -54
@@ -1,93 +1,279 @@
1
- import { propertyQuantityValue, propertyStringValue } from './properties.js';
2
- import { extractPanel } from '../panel/extract.js';
3
- const MODEL_ALIASES = ['Model', 'Type', 'partNumber', 'PartNumber'];
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(['IdealOpAmp']);
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: ['Rin', 'Rout', 'Aol', 'GBP', 'SupplyVoltage'],
12
- triode: ['Mu', 'K', 'Kp', 'Kvb', 'Ex', 'Kg'],
13
- pentode: ['Mu', 'K', 'Kp', 'Kvb', 'Ex', 'Kg', 'Kg1', 'Kg2'],
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
- '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',
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
- kind: 'quantity', name: 'R', required: true, unit: 'Ω',
35
- min: 1e-9, max: 1e9, aliases: ['Resistance', 'resistance', 'r'],
36
- }],
37
- 'variable-resistor': [{
38
- kind: 'quantity', name: 'R', required: true, unit: 'Ω',
39
- min: 1e-9, max: 1e9, aliases: ['Resistance', 'resistance', 'r'],
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: 'quantity', name: 'R', required: true, unit: 'Ω',
44
- min: 1e-9, max: 1e9, aliases: ['Resistance', 'totalResistance'],
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 === 'string') {
106
- if (typeof value !== 'string' || value.trim().length === 0) {
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 === 'string' && isRawQuantityExpression(value)) {
301
+ if (typeof value === "string" && isRawQuantityExpression(value)) {
116
302
  continue;
117
303
  }
118
304
  issues.push({
119
- code: 'value-unparseable',
120
- severity: 'error',
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 && rule.unit.length > 0 && quantity.unit.length > 0 && quantity.unit !== rule.unit) {
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: 'unit-mismatch',
130
- severity: 'warning',
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: 'value-out-of-range',
139
- severity: 'error',
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: 'value-out-of-range',
148
- severity: 'error',
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: 'duplicate-id',
164
- severity: 'error',
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 === 'unsupported') {
359
+ if (component.kind === "unsupported") {
171
360
  issues.push({
172
- code: 'unsupported-component',
173
- severity: 'warning',
174
- message: `${component.id}: unsupported source type ${component.sourceTypeName ?? 'unknown'}`,
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: 'degenerate-wire',
191
- severity: 'warning',
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 === 'error');
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 !== 'string' || rule.name !== 'model') {
408
+ if (rule.kind !== "string" || rule.name !== "model") {
217
409
  return false;
218
410
  }
219
- if (component.kind === 'ic' && component.properties.RuntimeDescriptor === 'true') {
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 === 'string' && normalizeToken(interfaceOnly) === 'true') {
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 === 'string' && normalizeToken(support) === 'view-only';
432
+ return typeof support === "string" && normalizeToken(support) === "view-only";
239
433
  }
240
434
  function validateSemanticMetadata(component) {
241
435
  const issues = [];
242
- if (component.kind === 'jack') {
436
+ if (component.kind === "jack") {
243
437
  issues.push(...validateJackSemanticMetadata(component));
244
438
  }
245
- if (component.kind === 'ic' && component.properties.RuntimeDescriptor === 'true') {
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 ['Role', 'ControlRole']) {
447
+ for (const property of ["Role", "ControlRole"]) {
253
448
  const value = propertyString(component, property);
254
- if (value !== null && value.trim().length > 0 && !isRecognizedJackRole(value)) {
449
+ if (value !== null &&
450
+ value.trim().length > 0 &&
451
+ !isRecognizedJackRole(value)) {
255
452
  issues.push({
256
- code: 'invalid-jack-role',
257
- severity: 'warning',
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, 'Interface');
265
- if (interfaceName !== null && interfaceName.trim().length > 0 && !isRecognizedJackInterface(interfaceName)) {
461
+ const interfaceName = propertyString(component, "Interface");
462
+ if (interfaceName !== null &&
463
+ interfaceName.trim().length > 0 &&
464
+ !isRecognizedJackInterface(interfaceName)) {
266
465
  issues.push({
267
- code: 'invalid-jack-interface',
268
- severity: 'warning',
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: 'Interface',
470
+ property: "Interface",
272
471
  });
273
472
  }
274
- const audioRole = propertyString(component, 'AudioRole');
473
+ const audioRole = propertyString(component, "AudioRole");
275
474
  if (audioRole !== null && !isValidJackAudioRole(audioRole)) {
276
475
  issues.push({
277
- code: 'invalid-jack-audio-role',
278
- severity: 'warning',
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: 'AudioRole',
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: 'descriptor-control-empty',
293
- severity: 'warning',
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, ['ModeLabels', 'ModeOptions']));
301
- const stepCount = parsePositiveInteger(propertyStringAny(component, ['ModeStepCount', 'ModeSteps', 'ModeCount']));
302
- if (labels.length > 0 && stepCount !== undefined && labels.length !== stepCount) {
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: 'descriptor-mode-label-mismatch',
305
- severity: 'warning',
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: 'ModeLabels',
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(',')[0]?.trim() ?? '';
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('{') && trimmed.endsWith('}')) {
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
- 'input',
364
- 'audio-input',
365
- 'in',
366
- 'direct-output',
367
- 'direct-out',
368
- 'dry-output',
369
- 'dry-out',
370
- 'output',
371
- 'audio-output',
372
- 'out',
373
- 'send',
374
- 'return',
375
- 'expression',
376
- 'exp',
377
- 'expression-pedal',
378
- 'tempo-tap',
379
- 'tap-tempo',
380
- 'tempo-in',
381
- 'tap',
382
- 'tempo',
383
- 'external-control',
384
- 'external-control-input',
385
- 'control-input',
386
- 'remote',
387
- 'footswitch',
388
- 'trigger',
389
- 'reset',
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
- 'audio',
397
- 'audio-port',
398
- 'control',
399
- 'control-port',
400
- 'power',
401
- 'power-port',
402
- 'dc-power',
403
- 'dc-power-input',
404
- 'tap-tempo-input',
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.trim().toLowerCase().replace(/[\s_]+/g, '-');
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, 'role'));
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: 'control-group-context-unresolved',
448
- severity: 'warning',
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: 'contextIds',
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, 'role'));
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: 'duplicate-device-interface-control-id',
464
- severity: 'error',
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, 'role'));
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: 'device-interface-group-unresolved',
474
- severity: 'warning',
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: 'groupId',
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: 'control-group-member-unresolved',
495
- severity: 'warning',
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: 'members.controlId',
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: 'control-group-member-order-duplicate',
506
- severity: 'warning',
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: 'members.order',
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, 'members.appliesWhen.allOf', member.appliesWhen.allOf, contextIds));
524
- issues.push(...validateGroupMemberContextList(groupId, member, 'members.appliesWhen.anyOf', member.appliesWhen.anyOf, contextIds));
525
- if (member.appliesWhen.allOf !== undefined
526
- && member.appliesWhen.allOf.length === 0
527
- && member.appliesWhen.anyOf === undefined) {
528
- issues.push(emptyGroupMemberApplicabilityIssue(groupId, member.controlId, 'members.appliesWhen.allOf'));
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
- && member.appliesWhen.anyOf.length === 0
532
- && member.appliesWhen.allOf === undefined) {
533
- issues.push(emptyGroupMemberApplicabilityIssue(groupId, member.controlId, 'members.appliesWhen.anyOf'));
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: 'control-group-member-context-unresolved',
550
- severity: 'warning',
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: 'control-group-member-context-unresolved',
560
- severity: 'warning',
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: 'control-group-member-context-unresolved',
572
- severity: 'warning',
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
- code: 'invalid-device-interface-token',
584
- severity: 'warning',
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, 'appliesWhen.allOf', control.appliesWhen.allOf, contextIds));
596
- issues.push(...validateContextList(control.id, 'appliesWhen.anyOf', control.appliesWhen.anyOf, contextIds));
597
- if (control.appliesWhen.allOf !== undefined
598
- && control.appliesWhen.allOf.length === 0
599
- && control.appliesWhen.anyOf === undefined) {
600
- issues.push(emptyApplicabilityIssue(control.id, 'appliesWhen.allOf'));
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
- && control.appliesWhen.anyOf.length === 0
604
- && control.appliesWhen.allOf === undefined) {
605
- issues.push(emptyApplicabilityIssue(control.id, 'appliesWhen.anyOf'));
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: 'device-interface-context-unresolved',
622
- severity: 'warning',
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: 'device-interface-context-unresolved',
632
- severity: 'warning',
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: 'device-interface-context-unresolved',
644
- severity: 'warning',
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 && !externalInterfaceIds.has(binding.externalInterfaceId)) {
858
+ if (binding.externalInterfaceId !== undefined &&
859
+ !externalInterfaceIds.has(binding.externalInterfaceId)) {
653
860
  issues.push({
654
- code: 'device-interface-binding-unresolved',
655
- severity: 'warning',
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: 'binding.externalInterfaceId',
865
+ property: "binding.externalInterfaceId",
659
866
  });
660
867
  }
661
868
  if (!componentIds.has(binding.componentId)) {
662
869
  issues.push({
663
- code: 'device-interface-binding-unresolved',
664
- severity: 'warning',
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: 'binding.componentId',
874
+ property: "binding.componentId",
668
875
  });
669
876
  return issues;
670
877
  }
671
- if (binding.controlId !== undefined
672
- && !resolvedPanelElements.some((resolved) => resolved.componentId === binding.componentId && resolved.id === binding.controlId)) {
878
+ if (binding.controlId !== undefined &&
879
+ !resolvedPanelElements.some((resolved) => resolved.componentId === binding.componentId &&
880
+ resolved.id === binding.controlId)) {
673
881
  issues.push({
674
- code: 'device-interface-binding-unresolved',
675
- severity: 'warning',
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: 'binding.controlId',
886
+ property: "binding.controlId",
679
887
  });
680
888
  }
681
889
  const component = componentsById.get(binding.componentId);
682
- if (binding.property !== undefined && component?.properties[binding.property] === undefined) {
890
+ if (binding.property !== undefined &&
891
+ component?.properties[binding.property] === undefined) {
683
892
  issues.push({
684
- code: 'device-interface-binding-unresolved',
685
- severity: 'warning',
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: 'binding.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) ?? [{ groupId: control.groupId ?? '', order: control.order }];
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 && existing.order === undefined && layout.order === undefined) {
703
- if (deviceInterfaceBindingSignature(existing.control.binding)
704
- === deviceInterfaceBindingSignature(control.binding)) {
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: 'device-interface-duplicate-role',
707
- severity: 'warning',
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: 'role',
923
+ property: "role",
711
924
  });
712
925
  }
713
926
  }
714
- seen.set(key, { control, ...(layout.order === undefined ? {} : { order: layout.order }) });
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 && !semanticControlIds.has(element.interfaceControlId)) {
1066
+ if (element.interfaceControlId !== undefined &&
1067
+ !semanticControlIds.has(element.interfaceControlId)) {
755
1068
  issues.push({
756
- code: 'panel-interface-control-unresolved',
757
- severity: 'warning',
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: 'interfaceControlId',
1073
+ property: "interfaceControlId",
761
1074
  });
762
1075
  }
763
1076
  if (!componentIds.has(componentId)) {
764
1077
  issues.push({
765
- code: 'panel-binding-unresolved',
766
- severity: 'warning',
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: 'panel-control-unresolved',
776
- severity: 'warning',
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 && !panelKindsCompatible(element.kind, resolved.kind)) {
1096
+ if (resolved !== undefined &&
1097
+ !panelKindsCompatible(element.kind, resolved.kind)) {
784
1098
  issues.push({
785
- code: 'panel-kind-mismatch',
786
- severity: 'warning',
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 === 'switch' && (declared === 'selector' || declared === 'footswitch');
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(':mode') && knob.controlMode === 'stepped' ? 'switch' : 'knob',
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: 'slider',
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: 'switch',
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: 'led',
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: 'jack',
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 && resolved.id === element.bind.controlId);
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 && resolved.id === 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: 'panel-cell-collision',
868
- severity: 'warning',
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('build-board-unresolved', 'error', `Build selectedBoardId references missing board "${selectedBoardId}"`, selectedBoardId, 'selectedBoardId'));
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('build-board-unresolved', 'warning', `Build alternateBoardIds references missing board "${boardId}"`, boardId, 'alternateBoardIds'));
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, 'preferredBoardId');
1226
+ const preferredBoardId = dataString(doc.mechanical?.internalBoard, "preferredBoardId");
908
1227
  if (preferredBoardId !== undefined && !boardsById.has(preferredBoardId)) {
909
- issues.push(unresolvedIssue('build-board-unresolved', 'warning', `Mechanical internalBoard.preferredBoardId references missing board "${preferredBoardId}"`, preferredBoardId, 'mechanical.internalBoard.preferredBoardId'));
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('build-harness-unresolved', 'error', `Build selectedOffBoardWiringHarnessIds references missing harness "${harnessId}"`, harnessId, 'selectedOffBoardWiringHarnessIds'));
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 === 'complete-selected-build' && selectedBoardId !== undefined) {
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 === 'component' && (ref.componentId === undefined || !componentIds.has(ref.componentId))) {
952
- return unresolvedIssue('bom-ref-unresolved', 'warning', `BOM item "${itemId}" references missing component "${ref.componentId ?? ''}"`, itemId, 'refs.componentId');
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 === 'device-interface-control' &&
1275
+ if (ref.kind === "device-interface-control" &&
955
1276
  (ref.controlId === undefined || !controlIds.has(ref.controlId))) {
956
- return unresolvedIssue('bom-ref-unresolved', 'warning', `BOM item "${itemId}" references missing device interface control "${ref.controlId ?? ''}"`, itemId, 'refs.controlId');
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 === 'panel-element' && (ref.panelElementId === undefined || !panelElementIds.has(ref.panelElementId))) {
959
- return unresolvedIssue('bom-ref-unresolved', 'warning', `BOM item "${itemId}" references missing panel element "${ref.panelElementId ?? ''}"`, itemId, 'refs.panelElementId');
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 === 'board' && (ref.boardId === undefined || !boardsById.has(ref.boardId))) {
962
- return unresolvedIssue('bom-ref-unresolved', 'warning', `BOM item "${itemId}" references missing board "${ref.boardId ?? ''}"`, itemId, 'refs.boardId');
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 && !isDigestShapedSourceHash(board.sourceCircuit.hash)) {
1292
+ if (board.sourceCircuit !== undefined &&
1293
+ !isDigestShapedSourceHash(board.sourceCircuit.hash)) {
969
1294
  issues.push({
970
- code: 'board-source-hash-invalid',
971
- severity: 'error',
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: 'sourceCircuit.hash',
1299
+ property: "sourceCircuit.hash",
975
1300
  });
976
1301
  }
977
1302
  for (const terminal of board.edgeTerminals) {
978
- if (terminal.terminalRef !== undefined && !componentTerminalExists(componentsById, terminal.terminalRef)) {
979
- issues.push(unresolvedIssue('board-terminal-unresolved', 'warning', `Board "${board.id}" edge terminal "${terminal.id}" references missing component terminal`, board.id, terminal.id));
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('board-terminal-unresolved', 'warning', `Board "${board.id}" places missing component "${placement.componentId}"`, board.id, placement.componentId));
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('board-terminal-unresolved', 'warning', `Board "${board.id}" pad "${pad.padId}" references missing terminal "${pad.terminalName}"`, board.id, pad.padId));
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('board-terminal-unresolved', 'warning', `Board "${board.id}" net "${net.id}" references missing component terminal`, board.id, net.id));
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: 'board-route-feature-invalid',
1005
- severity: 'error',
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) && !boardNetRefExists(route.netRef, board.id, boardNetsByBoardId)) {
1012
- issues.push(unresolvedIssue('offboard-signal-unresolved', 'warning', `Board "${board.id}" route "${route.id}" references missing board net "${route.netRef.netId}"`, board.id, route.id));
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('offboard-endpoint-unresolved', 'error', `Harness "${harness.id}" connection "${connection.id}" references missing endpoint "${connection.fromEndpointId}"`, harness.id, connection.id));
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('offboard-endpoint-unresolved', 'error', `Harness "${harness.id}" connection "${connection.id}" references missing endpoint "${connection.toEndpointId}"`, harness.id, connection.id));
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) || !endpointIds.has(connection.toEndpointId)) {
1052
- issues.push(unresolvedIssue('offboard-endpoint-unresolved', 'error', `Selected harness "${harnessId}" contains an unresolved connection endpoint`, harnessId, connection.id));
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 === 'board-terminal') {
1060
- const terminalIds = endpoint.boardId === undefined ? undefined : boardTerminalsByBoardId.get(endpoint.boardId);
1061
- if (terminalIds === undefined || endpoint.terminalId === undefined || !terminalIds.has(endpoint.terminalId)) {
1062
- return unresolvedIssue('offboard-endpoint-unresolved', 'error', `Off-board endpoint "${endpoint.id}" references missing board terminal`, endpoint.id, 'terminalId');
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 === 'panel-component-terminal' ||
1067
- endpoint.kind === 'power-terminal' ||
1068
- endpoint.kind === 'footswitch-terminal') {
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('offboard-endpoint-unresolved', 'error', `Off-board endpoint "${endpoint.id}" references missing component terminal`, endpoint.id, 'componentId');
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 !== 'power-terminal' &&
1407
+ endpoint.kind !== "power-terminal" &&
1076
1408
  !panelElementIds.has(endpoint.panelElementId)) {
1077
- return unresolvedIssue('offboard-endpoint-unresolved', 'warning', `Off-board endpoint "${endpoint.id}" references missing panel element "${endpoint.panelElementId}"`, endpoint.id, 'panelElementId');
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('offboard-signal-unresolved', 'error', `Harness "${harnessId}" references missing board net "${signalRef.netId}"`, harnessId, 'signalRef');
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, 'member');
1090
- const componentId = dataString(member, 'componentId');
1091
- const terminalName = dataString(member, 'terminalName');
1092
- if (dataString(signalRef, 'source') === 'canonical-circuit' && componentId !== undefined && terminalName !== undefined) {
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('offboard-signal-unresolved', 'error', `Harness "${harnessId}" references missing canonical component terminal`, harnessId, 'signalRef');
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, 'netId'))
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: 'board-net-unrouted',
1112
- severity: 'error',
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, 'source') === 'board-netlist' && dataString(value, 'netId') !== undefined;
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.get(componentId)?.terminals.some((terminal) => terminal.name === terminalName) === true;
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 === 'string' ? value : undefined;
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 === 'object' && value !== null && !Array.isArray(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 === 'string' ? 'model-required' : 'value-required',
1187
- severity: 'error',
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,