@vessel-dsp/core 0.5.0 → 0.6.1

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 (241) hide show
  1. package/README.md +10 -0
  2. package/dist/editor/commands.d.ts +48 -0
  3. package/dist/editor/commands.d.ts.map +1 -0
  4. package/{src/editor/commands.ts → dist/editor/commands.js} +44 -91
  5. package/dist/editor/commands.js.map +1 -0
  6. package/dist/editor/factory.d.ts +10 -0
  7. package/dist/editor/factory.d.ts.map +1 -0
  8. package/{src/editor/factory.ts → dist/editor/factory.js} +11 -27
  9. package/dist/editor/factory.js.map +1 -0
  10. package/dist/editor/history.d.ts +29 -0
  11. package/dist/editor/history.d.ts.map +1 -0
  12. package/{src/editor/history.ts → dist/editor/history.js} +12 -42
  13. package/dist/editor/history.js.map +1 -0
  14. package/{src/editor/index.ts → dist/editor/index.d.ts} +1 -3
  15. package/dist/editor/index.d.ts.map +1 -0
  16. package/dist/editor/index.js +5 -0
  17. package/dist/editor/index.js.map +1 -0
  18. package/dist/editor/layout.d.ts +8 -0
  19. package/dist/editor/layout.d.ts.map +1 -0
  20. package/{src/editor/layout.ts → dist/editor/layout.js} +36 -90
  21. package/dist/editor/layout.js.map +1 -0
  22. package/dist/formats/circuit-json/serializer.d.ts +86 -0
  23. package/dist/formats/circuit-json/serializer.d.ts.map +1 -0
  24. package/{src/formats/circuit-json/serializer.ts → dist/formats/circuit-json/serializer.js} +114 -414
  25. package/dist/formats/circuit-json/serializer.js.map +1 -0
  26. package/dist/formats/document.d.ts +64 -0
  27. package/dist/formats/document.d.ts.map +1 -0
  28. package/dist/formats/document.js +300 -0
  29. package/dist/formats/document.js.map +1 -0
  30. package/dist/formats/interchange/parser.d.ts +3 -0
  31. package/dist/formats/interchange/parser.d.ts.map +1 -0
  32. package/{src/formats/interchange/parser.ts → dist/formats/interchange/parser.js} +651 -299
  33. package/dist/formats/interchange/parser.js.map +1 -0
  34. package/dist/formats/interchange/serializer.d.ts +9 -0
  35. package/dist/formats/interchange/serializer.d.ts.map +1 -0
  36. package/{src/formats/interchange/serializer.ts → dist/formats/interchange/serializer.js} +151 -158
  37. package/dist/formats/interchange/serializer.js.map +1 -0
  38. package/dist/formats/ltspice/catalog.d.ts +19 -0
  39. package/dist/formats/ltspice/catalog.d.ts.map +1 -0
  40. package/{src/formats/ltspice/catalog.ts → dist/formats/ltspice/catalog.js} +18 -52
  41. package/dist/formats/ltspice/catalog.js.map +1 -0
  42. package/dist/formats/ltspice/encoding.d.ts +2 -0
  43. package/dist/formats/ltspice/encoding.d.ts.map +1 -0
  44. package/{src/formats/ltspice/encoding.ts → dist/formats/ltspice/encoding.js} +17 -41
  45. package/dist/formats/ltspice/encoding.js.map +1 -0
  46. package/dist/formats/ltspice/parser.d.ts +3 -0
  47. package/dist/formats/ltspice/parser.d.ts.map +1 -0
  48. package/{src/formats/ltspice/parser.ts → dist/formats/ltspice/parser.js} +39 -141
  49. package/dist/formats/ltspice/parser.js.map +1 -0
  50. package/dist/formats/ltspice/serializer.d.ts +7 -0
  51. package/dist/formats/ltspice/serializer.d.ts.map +1 -0
  52. package/{src/formats/ltspice/serializer.ts → dist/formats/ltspice/serializer.js} +18 -45
  53. package/dist/formats/ltspice/serializer.js.map +1 -0
  54. package/dist/formats/schx/catalog.d.ts +19 -0
  55. package/dist/formats/schx/catalog.d.ts.map +1 -0
  56. package/{src/formats/schx/catalog.ts → dist/formats/schx/catalog.js} +48 -101
  57. package/dist/formats/schx/catalog.js.map +1 -0
  58. package/dist/formats/schx/parser.d.ts +3 -0
  59. package/dist/formats/schx/parser.d.ts.map +1 -0
  60. package/{src/formats/schx/parser.ts → dist/formats/schx/parser.js} +31 -86
  61. package/dist/formats/schx/parser.js.map +1 -0
  62. package/dist/formats/schx/runtime-descriptors.d.ts +3 -0
  63. package/dist/formats/schx/runtime-descriptors.d.ts.map +1 -0
  64. package/{src/formats/schx/runtime-descriptors.ts → dist/formats/schx/runtime-descriptors.js} +36 -123
  65. package/dist/formats/schx/runtime-descriptors.js.map +1 -0
  66. package/dist/formats/schx/serializer.d.ts +5 -0
  67. package/dist/formats/schx/serializer.d.ts.map +1 -0
  68. package/{src/formats/schx/serializer.ts → dist/formats/schx/serializer.js} +17 -42
  69. package/dist/formats/schx/serializer.js.map +1 -0
  70. package/dist/formats/schx/transforms.d.ts +4 -0
  71. package/dist/formats/schx/transforms.d.ts.map +1 -0
  72. package/{src/formats/schx/transforms.ts → dist/formats/schx/transforms.js} +6 -10
  73. package/dist/formats/schx/transforms.js.map +1 -0
  74. package/dist/formats/spice/parser.d.ts +3 -0
  75. package/dist/formats/spice/parser.d.ts.map +1 -0
  76. package/{src/formats/spice/parser.ts → dist/formats/spice/parser.js} +50 -96
  77. package/dist/formats/spice/parser.js.map +1 -0
  78. package/dist/formats/spice/serializer.d.ts +3 -0
  79. package/dist/formats/spice/serializer.d.ts.map +1 -0
  80. package/{src/formats/spice/serializer.ts → dist/formats/spice/serializer.js} +8 -13
  81. package/dist/formats/spice/serializer.js.map +1 -0
  82. package/dist/index.d.ts +47 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +32 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/model/connectivity.d.ts +16 -0
  87. package/dist/model/connectivity.d.ts.map +1 -0
  88. package/{src/model/connectivity.ts → dist/model/connectivity.js} +28 -63
  89. package/dist/model/connectivity.js.map +1 -0
  90. package/dist/model/netlist.d.ts +24 -0
  91. package/dist/model/netlist.d.ts.map +1 -0
  92. package/{src/model/netlist.ts → dist/model/netlist.js} +42 -110
  93. package/dist/model/netlist.js.map +1 -0
  94. package/dist/model/properties.d.ts +9 -0
  95. package/dist/model/properties.d.ts.map +1 -0
  96. package/{src/model/properties.ts → dist/model/properties.js} +10 -18
  97. package/dist/model/properties.js.map +1 -0
  98. package/dist/model/quantity.d.ts +3 -0
  99. package/dist/model/quantity.d.ts.map +1 -0
  100. package/{src/model/quantity.ts → dist/model/quantity.js} +7 -30
  101. package/dist/model/quantity.js.map +1 -0
  102. package/dist/model/types.d.ts +431 -0
  103. package/dist/model/types.d.ts.map +1 -0
  104. package/dist/model/types.js +10 -0
  105. package/dist/model/types.js.map +1 -0
  106. package/dist/model/validation.d.ts +32 -0
  107. package/dist/model/validation.d.ts.map +1 -0
  108. package/{src/model/validation.ts → dist/model/validation.js} +420 -323
  109. package/dist/model/validation.js.map +1 -0
  110. package/dist/model/wires.d.ts +3 -0
  111. package/dist/model/wires.d.ts.map +1 -0
  112. package/{src/model/wires.ts → dist/model/wires.js} +10 -16
  113. package/dist/model/wires.js.map +1 -0
  114. package/dist/panel/extract.d.ts +5 -0
  115. package/dist/panel/extract.d.ts.map +1 -0
  116. package/{src/panel/extract.ts → dist/panel/extract.js} +146 -235
  117. package/dist/panel/extract.js.map +1 -0
  118. package/dist/panel/index.d.ts +6 -0
  119. package/dist/panel/index.d.ts.map +1 -0
  120. package/dist/panel/index.js +5 -0
  121. package/dist/panel/index.js.map +1 -0
  122. package/dist/panel/knobs.d.ts +7 -0
  123. package/dist/panel/knobs.d.ts.map +1 -0
  124. package/{src/panel/knobs.ts → dist/panel/knobs.js} +7 -18
  125. package/dist/panel/knobs.js.map +1 -0
  126. package/dist/panel/protocol.d.ts +9 -0
  127. package/dist/panel/protocol.d.ts.map +1 -0
  128. package/{src/panel/protocol.ts → dist/panel/protocol.js} +10 -26
  129. package/dist/panel/protocol.js.map +1 -0
  130. package/{src/panel/types.ts → dist/panel/types.d.ts} +50 -89
  131. package/dist/panel/types.d.ts.map +1 -0
  132. package/dist/panel/types.js +2 -0
  133. package/dist/panel/types.js.map +1 -0
  134. package/dist/preview/bounds.d.ts +12 -0
  135. package/dist/preview/bounds.d.ts.map +1 -0
  136. package/{src/preview/bounds.ts → dist/preview/bounds.js} +15 -29
  137. package/dist/preview/bounds.js.map +1 -0
  138. package/dist/preview/box-layout.d.ts +4 -0
  139. package/dist/preview/box-layout.d.ts.map +1 -0
  140. package/{src/preview/box-layout.ts → dist/preview/box-layout.js} +2 -6
  141. package/dist/preview/box-layout.js.map +1 -0
  142. package/dist/preview/colors.d.ts +3 -0
  143. package/dist/preview/colors.d.ts.map +1 -0
  144. package/{src/preview/colors.ts → dist/preview/colors.js} +3 -5
  145. package/dist/preview/colors.js.map +1 -0
  146. package/dist/preview/hanging.d.ts +8 -0
  147. package/dist/preview/hanging.d.ts.map +1 -0
  148. package/{src/preview/hanging.ts → dist/preview/hanging.js} +9 -28
  149. package/dist/preview/hanging.js.map +1 -0
  150. package/dist/preview/junctions.d.ts +3 -0
  151. package/dist/preview/junctions.d.ts.map +1 -0
  152. package/{src/preview/junctions.ts → dist/preview/junctions.js} +9 -24
  153. package/dist/preview/junctions.js.map +1 -0
  154. package/dist/preview/label-layout.d.ts +12 -0
  155. package/dist/preview/label-layout.d.ts.map +1 -0
  156. package/{src/preview/label-layout.ts → dist/preview/label-layout.js} +15 -36
  157. package/dist/preview/label-layout.js.map +1 -0
  158. package/dist/preview/ports.d.ts +17 -0
  159. package/dist/preview/ports.d.ts.map +1 -0
  160. package/{src/preview/ports.ts → dist/preview/ports.js} +10 -37
  161. package/dist/preview/ports.js.map +1 -0
  162. package/dist/preview/renderable-wires.d.ts +3 -0
  163. package/dist/preview/renderable-wires.d.ts.map +1 -0
  164. package/{src/preview/renderable-wires.ts → dist/preview/renderable-wires.js} +12 -29
  165. package/dist/preview/renderable-wires.js.map +1 -0
  166. package/dist/preview/routing.d.ts +4 -0
  167. package/dist/preview/routing.d.ts.map +1 -0
  168. package/dist/preview/routing.js +13 -0
  169. package/dist/preview/routing.js.map +1 -0
  170. package/dist/preview/snap.d.ts +9 -0
  171. package/dist/preview/snap.d.ts.map +1 -0
  172. package/{src/preview/snap.ts → dist/preview/snap.js} +9 -31
  173. package/dist/preview/snap.js.map +1 -0
  174. package/dist/preview/symbols/svg-content.d.ts +7 -0
  175. package/dist/preview/symbols/svg-content.d.ts.map +1 -0
  176. package/{src/preview/symbols/svg-content.ts → dist/preview/symbols/svg-content.js} +3 -6
  177. package/dist/preview/symbols/svg-content.js.map +1 -0
  178. package/dist/preview/symbols.d.ts +7 -0
  179. package/dist/preview/symbols.d.ts.map +1 -0
  180. package/{src/preview/symbols.ts → dist/preview/symbols.js} +18 -43
  181. package/dist/preview/symbols.js.map +1 -0
  182. package/dist/preview/wire-chains.d.ts +4 -0
  183. package/dist/preview/wire-chains.d.ts.map +1 -0
  184. package/{src/preview/wire-chains.ts → dist/preview/wire-chains.js} +37 -37
  185. package/dist/preview/wire-chains.js.map +1 -0
  186. package/package.json +3 -3
  187. package/src/formats/document.ts +0 -274
  188. package/src/index.ts +0 -205
  189. package/src/model/types.ts +0 -309
  190. package/src/panel/index.ts +0 -39
  191. package/src/preview/routing.ts +0 -15
  192. package/src/preview/symbols/analog-switch.svg +0 -17
  193. package/src/preview/symbols/battery.svg +0 -16
  194. package/src/preview/symbols/bbd.svg +0 -21
  195. package/src/preview/symbols/bjt-npn.svg +0 -16
  196. package/src/preview/symbols/bjt-pnp.svg +0 -17
  197. package/src/preview/symbols/capacitor-electrolytic.svg +0 -13
  198. package/src/preview/symbols/capacitor.svg +0 -12
  199. package/src/preview/symbols/current-source.svg +0 -14
  200. package/src/preview/symbols/delay-ic.svg +0 -22
  201. package/src/preview/symbols/diode-schottky.svg +0 -12
  202. package/src/preview/symbols/diode-zener.svg +0 -12
  203. package/src/preview/symbols/diode.svg +0 -13
  204. package/src/preview/symbols/flipflop.svg +0 -20
  205. package/src/preview/symbols/ground.svg +0 -12
  206. package/src/preview/symbols/ic-block.svg +0 -20
  207. package/src/preview/symbols/ic.svg +0 -19
  208. package/src/preview/symbols/inductor.svg +0 -11
  209. package/src/preview/symbols/jack-input.svg +0 -16
  210. package/src/preview/symbols/jack-output.svg +0 -16
  211. package/src/preview/symbols/jfet-junction-n.svg +0 -17
  212. package/src/preview/symbols/jfet-n.svg +0 -17
  213. package/src/preview/symbols/jfet-p.svg +0 -17
  214. package/src/preview/symbols/label.svg +0 -8
  215. package/src/preview/symbols/led.svg +0 -18
  216. package/src/preview/symbols/mosfet-n.svg +0 -21
  217. package/src/preview/symbols/mosfet-p.svg +0 -21
  218. package/src/preview/symbols/named-wire.svg +0 -11
  219. package/src/preview/symbols/opamp.svg +0 -21
  220. package/src/preview/symbols/optocoupler.svg +0 -30
  221. package/src/preview/symbols/ota.svg +0 -20
  222. package/src/preview/symbols/pentode.svg +0 -25
  223. package/src/preview/symbols/photoresistor.svg +0 -19
  224. package/src/preview/symbols/port.svg +0 -8
  225. package/src/preview/symbols/potentiometer.svg +0 -15
  226. package/src/preview/symbols/power-amp.svg +0 -20
  227. package/src/preview/symbols/rail.svg +0 -11
  228. package/src/preview/symbols/regulator.svg +0 -13
  229. package/src/preview/symbols/relay.svg +0 -20
  230. package/src/preview/symbols/resistor.svg +0 -11
  231. package/src/preview/symbols/switch-3pdt.svg +0 -32
  232. package/src/preview/symbols/switch-rotary.svg +0 -23
  233. package/src/preview/symbols/switch-spdt.svg +0 -16
  234. package/src/preview/symbols/switch-spst.svg +0 -14
  235. package/src/preview/symbols/switch-toggle.svg +0 -14
  236. package/src/preview/symbols/transformer.svg +0 -17
  237. package/src/preview/symbols/triode.svg +0 -17
  238. package/src/preview/symbols/tube-diode.svg +0 -13
  239. package/src/preview/symbols/unsupported.svg +0 -8
  240. package/src/preview/symbols/variable-resistor.svg +0 -13
  241. package/src/preview/symbols/voltage-source.svg +0 -15
@@ -1,90 +1,17 @@
1
- import { propertyQuantityValue, propertyStringValue } from './properties';
2
- import { extractPanel } from '../panel/extract';
3
- import type {
4
- CircuitDocument,
5
- Component,
6
- ComponentKind,
7
- DeviceInterfaceBinding,
8
- DeviceInterfaceControl,
9
- PanelControlKind,
10
- PanelElementPlacement,
11
- PanelFace,
12
- ParsedQuantity,
13
- PropertyValue,
14
- } from './types';
15
-
16
- export type ValidationSeverity = 'error' | 'warning';
17
-
18
- export type ValidationCode =
19
- | 'value-required'
20
- | 'model-required'
21
- | 'value-unparseable'
22
- | 'value-out-of-range'
23
- | 'unit-mismatch'
24
- | 'unsupported-component'
25
- | 'invalid-jack-role'
26
- | 'invalid-jack-interface'
27
- | 'invalid-jack-audio-role'
28
- | 'descriptor-control-empty'
29
- | 'descriptor-mode-label-mismatch'
30
- | 'duplicate-device-interface-control-id'
31
- | 'invalid-device-interface-token'
32
- | 'control-group-context-unresolved'
33
- | 'device-interface-group-unresolved'
34
- | 'device-interface-context-unresolved'
35
- | 'device-interface-binding-unresolved'
36
- | 'device-interface-duplicate-role'
37
- | 'panel-interface-control-unresolved'
38
- | 'panel-binding-unresolved'
39
- | 'panel-control-unresolved'
40
- | 'panel-kind-mismatch'
41
- | 'panel-cell-collision'
42
- | 'duplicate-id'
43
- | 'degenerate-wire';
44
-
45
- export type ValidationIssue = Readonly<{
46
- code: ValidationCode;
47
- severity: ValidationSeverity;
48
- message: string;
49
- componentId?: string;
50
- property?: string;
51
- wireId?: string;
52
- }>;
53
-
54
- export type QuantityRule = Readonly<{
55
- kind: 'quantity';
56
- name: string;
57
- required: boolean;
58
- aliases?: readonly string[];
59
- unit?: string;
60
- min?: number;
61
- max?: number;
62
- }>;
63
-
64
- export type StringRule = Readonly<{
65
- kind: 'string';
66
- name: string;
67
- required: boolean;
68
- aliases?: readonly string[];
69
- }>;
70
-
71
- export type PropertyRule = QuantityRule | StringRule;
72
-
73
- const MODEL_ALIASES = ['Model', 'Type', 'partNumber', 'PartNumber'] as const;
74
-
1
+ import { propertyQuantityValue, propertyStringValue } from './properties.js';
2
+ import { extractPanel } from '../panel/extract.js';
3
+ const MODEL_ALIASES = ['Model', 'Type', 'partNumber', 'PartNumber'];
75
4
  // Short source-type names (last dotted segment) that represent an "ideal" component variant —
76
5
  // no model name is required because the component is a mathematical abstraction.
77
- const IDEAL_SOURCE_TYPES: ReadonlySet<string> = new Set(['IdealOpAmp']);
78
-
6
+ const IDEAL_SOURCE_TYPES = new Set(['IdealOpAmp']);
79
7
  // Per-kind property names that, if present, satisfy the "needs a model" requirement.
80
8
  // LiveSPICE stores tube Koren parameters and opamp small-signal parameters inline; when those
81
9
  // are present, the parameters ARE the model definition and no separate model name is needed.
82
- const INLINE_MODEL_PARAMETERS: Partial<Record<ComponentKind, readonly string[]>> = {
10
+ const INLINE_MODEL_PARAMETERS = {
83
11
  opamp: ['Rin', 'Rout', 'Aol', 'GBP', 'SupplyVoltage'],
84
12
  triode: ['Mu', 'K', 'Kp', 'Kvb', 'Ex', 'Kg'],
85
13
  pentode: ['Mu', 'K', 'Kp', 'Kvb', 'Ex', 'Kg', 'Kg1', 'Kg2'],
86
14
  };
87
-
88
15
  const RUNTIME_DESCRIPTOR_CONTROL_PROPERTIES = [
89
16
  'TimeControl',
90
17
  'FeedbackControl',
@@ -101,23 +28,16 @@ const RUNTIME_DESCRIPTOR_CONTROL_PROPERTIES = [
101
28
  'DirectOutJack',
102
29
  'DirectOutputControl',
103
30
  'DirectOutControl',
104
- ] as const;
105
-
106
- type ResolvedPanelElement = Readonly<{
107
- id: string;
108
- componentId: string;
109
- kind: PanelControlKind;
110
- }>;
111
-
112
- const KIND_RULES: Partial<Record<ComponentKind, readonly PropertyRule[]>> = {
31
+ ];
32
+ const KIND_RULES = {
113
33
  resistor: [{
114
- kind: 'quantity', name: 'R', required: true, unit: 'Ω',
115
- min: 1e-9, max: 1e9, aliases: ['Resistance', 'resistance', 'r'],
116
- }],
34
+ kind: 'quantity', name: 'R', required: true, unit: 'Ω',
35
+ min: 1e-9, max: 1e9, aliases: ['Resistance', 'resistance', 'r'],
36
+ }],
117
37
  'variable-resistor': [{
118
- kind: 'quantity', name: 'R', required: true, unit: 'Ω',
119
- min: 1e-9, max: 1e9, aliases: ['Resistance', 'resistance', 'r'],
120
- }],
38
+ kind: 'quantity', name: 'R', required: true, unit: 'Ω',
39
+ min: 1e-9, max: 1e9, aliases: ['Resistance', 'resistance', 'r'],
40
+ }],
121
41
  potentiometer: [
122
42
  {
123
43
  kind: 'quantity', name: 'R', required: true, unit: 'Ω',
@@ -126,29 +46,29 @@ const KIND_RULES: Partial<Record<ComponentKind, readonly PropertyRule[]>> = {
126
46
  { kind: 'string', name: 'taper', required: false, aliases: ['Taper'] },
127
47
  ],
128
48
  capacitor: [{
129
- kind: 'quantity', name: 'C', required: true, unit: 'F',
130
- min: 1e-15, max: 1, aliases: ['Capacitance', 'capacitance', 'c'],
131
- }],
49
+ kind: 'quantity', name: 'C', required: true, unit: 'F',
50
+ min: 1e-15, max: 1, aliases: ['Capacitance', 'capacitance', 'c'],
51
+ }],
132
52
  inductor: [{
133
- kind: 'quantity', name: 'L', required: true, unit: 'H',
134
- min: 1e-12, max: 100, aliases: ['Inductance', 'inductance', 'l'],
135
- }],
53
+ kind: 'quantity', name: 'L', required: true, unit: 'H',
54
+ min: 1e-12, max: 100, aliases: ['Inductance', 'inductance', 'l'],
55
+ }],
136
56
  'voltage-source': [{
137
- kind: 'quantity', name: 'V', required: true, unit: 'V',
138
- aliases: ['Voltage', 'voltage', 'v'],
139
- }],
57
+ kind: 'quantity', name: 'V', required: true, unit: 'V',
58
+ aliases: ['Voltage', 'voltage', 'v'],
59
+ }],
140
60
  'current-source': [{
141
- kind: 'quantity', name: 'I', required: true, unit: 'A',
142
- aliases: ['Current', 'current', 'i'],
143
- }],
61
+ kind: 'quantity', name: 'I', required: true, unit: 'A',
62
+ aliases: ['Current', 'current', 'i'],
63
+ }],
144
64
  battery: [{
145
- kind: 'quantity', name: 'V', required: true, unit: 'V',
146
- aliases: ['Voltage', 'voltage', 'v'],
147
- }],
65
+ kind: 'quantity', name: 'V', required: true, unit: 'V',
66
+ aliases: ['Voltage', 'voltage', 'v'],
67
+ }],
148
68
  rail: [{
149
- kind: 'quantity', name: 'V', required: true, unit: 'V',
150
- aliases: ['Voltage', 'voltage', 'v'],
151
- }],
69
+ kind: 'quantity', name: 'V', required: true, unit: 'V',
70
+ aliases: ['Voltage', 'voltage', 'v'],
71
+ }],
152
72
  diode: [{ kind: 'string', name: 'model', required: true, aliases: [...MODEL_ALIASES] }],
153
73
  led: [{ kind: 'string', name: 'model', required: true, aliases: [...MODEL_ALIASES] }],
154
74
  bjt: [{ kind: 'string', name: 'model', required: true, aliases: [...MODEL_ALIASES] }],
@@ -169,27 +89,19 @@ const KIND_RULES: Partial<Record<ComponentKind, readonly PropertyRule[]>> = {
169
89
  flipflop: [{ kind: 'string', name: 'model', required: true, aliases: [...MODEL_ALIASES] }],
170
90
  ic: [{ kind: 'string', name: 'model', required: true, aliases: [...MODEL_ALIASES] }],
171
91
  };
172
-
173
- export function getRulesForKind(kind: ComponentKind): readonly PropertyRule[] {
92
+ export function getRulesForKind(kind) {
174
93
  return KIND_RULES[kind] ?? [];
175
94
  }
176
-
177
- export function validateComponent(
178
- component: Component,
179
- rules: readonly PropertyRule[] = getRulesForKind(component.kind),
180
- ): readonly ValidationIssue[] {
181
- const issues: ValidationIssue[] = [];
182
-
95
+ export function validateComponent(component, rules = getRulesForKind(component.kind)) {
96
+ const issues = [];
183
97
  for (const rule of rules) {
184
98
  const value = findProperty(component, rule);
185
-
186
99
  if (value === undefined) {
187
100
  if (rule.required && !isRequirementWaived(component, rule)) {
188
101
  issues.push(missingPropertyIssue(component, rule));
189
102
  }
190
103
  continue;
191
104
  }
192
-
193
105
  if (rule.kind === 'string') {
194
106
  if (typeof value !== 'string' || value.trim().length === 0) {
195
107
  if (rule.required && !isRequirementWaived(component, rule)) {
@@ -198,7 +110,6 @@ export function validateComponent(
198
110
  }
199
111
  continue;
200
112
  }
201
-
202
113
  const quantity = coerceQuantity(value);
203
114
  if (quantity === null) {
204
115
  if (typeof value === 'string' && isRawQuantityExpression(value)) {
@@ -213,7 +124,6 @@ export function validateComponent(
213
124
  });
214
125
  continue;
215
126
  }
216
-
217
127
  if (rule.unit !== undefined && rule.unit.length > 0 && quantity.unit.length > 0 && quantity.unit !== rule.unit) {
218
128
  issues.push({
219
129
  code: 'unit-mismatch',
@@ -223,7 +133,6 @@ export function validateComponent(
223
133
  property: rule.name,
224
134
  });
225
135
  }
226
-
227
136
  if (rule.min !== undefined && quantity.value < rule.min) {
228
137
  issues.push({
229
138
  code: 'value-out-of-range',
@@ -243,14 +152,11 @@ export function validateComponent(
243
152
  });
244
153
  }
245
154
  }
246
-
247
155
  return issues;
248
156
  }
249
-
250
- export function validateDocument(doc: CircuitDocument): readonly ValidationIssue[] {
251
- const issues: ValidationIssue[] = [];
252
- const seen = new Set<string>();
253
-
157
+ export function validateDocument(doc) {
158
+ const issues = [];
159
+ const seen = new Set();
254
160
  for (const component of doc.components) {
255
161
  if (seen.has(component.id)) {
256
162
  issues.push({
@@ -261,7 +167,6 @@ export function validateDocument(doc: CircuitDocument): readonly ValidationIssue
261
167
  });
262
168
  }
263
169
  seen.add(component.id);
264
-
265
170
  if (component.kind === 'unsupported') {
266
171
  issues.push({
267
172
  code: 'unsupported-component',
@@ -271,16 +176,13 @@ export function validateDocument(doc: CircuitDocument): readonly ValidationIssue
271
176
  });
272
177
  continue;
273
178
  }
274
-
275
179
  for (const issue of validateComponent(component)) {
276
180
  issues.push(issue);
277
181
  }
278
-
279
182
  for (const issue of validateSemanticMetadata(component)) {
280
183
  issues.push(issue);
281
184
  }
282
185
  }
283
-
284
186
  for (const wire of doc.wires) {
285
187
  const [a, b] = wire.endpoints;
286
188
  if (a.x === b.x && a.y === b.y) {
@@ -292,27 +194,24 @@ export function validateDocument(doc: CircuitDocument): readonly ValidationIssue
292
194
  });
293
195
  }
294
196
  }
295
-
296
197
  for (const issue of validateDeviceInterface(doc, seen)) {
297
198
  issues.push(issue);
298
199
  }
299
-
300
200
  for (const issue of validatePanel(doc, seen, new Set(doc.deviceInterface?.controls.map((control) => control.id) ?? []))) {
301
201
  issues.push(issue);
302
202
  }
303
-
203
+ for (const issue of validateV3BuildMetadata(doc, seen)) {
204
+ issues.push(issue);
205
+ }
304
206
  return issues;
305
207
  }
306
-
307
- export function hasErrors(issues: readonly ValidationIssue[]): boolean {
208
+ export function hasErrors(issues) {
308
209
  return issues.some((issue) => issue.severity === 'error');
309
210
  }
310
-
311
- function isRequirementWaived(component: Component, rule: PropertyRule): boolean {
211
+ function isRequirementWaived(component, rule) {
312
212
  if (isInterfaceOnlyComponent(component)) {
313
213
  return true;
314
214
  }
315
-
316
215
  // Only the "model" string requirement has a waiver path today.
317
216
  if (rule.kind !== 'string' || rule.name !== 'model') {
318
217
  return false;
@@ -327,8 +226,7 @@ function isRequirementWaived(component: Component, rule: PropertyRule): boolean
327
226
  const inline = INLINE_MODEL_PARAMETERS[component.kind] ?? [];
328
227
  return inline.some((name) => component.properties[name] !== undefined);
329
228
  }
330
-
331
- function isInterfaceOnlyComponent(component: Component): boolean {
229
+ function isInterfaceOnlyComponent(component) {
332
230
  const interfaceOnly = component.properties.InterfaceOnly;
333
231
  if (interfaceOnly === true) {
334
232
  return true;
@@ -339,25 +237,19 @@ function isInterfaceOnlyComponent(component: Component): boolean {
339
237
  const support = component.properties.Support;
340
238
  return typeof support === 'string' && normalizeToken(support) === 'view-only';
341
239
  }
342
-
343
- function validateSemanticMetadata(component: Component): readonly ValidationIssue[] {
344
- const issues: ValidationIssue[] = [];
345
-
240
+ function validateSemanticMetadata(component) {
241
+ const issues = [];
346
242
  if (component.kind === 'jack') {
347
243
  issues.push(...validateJackSemanticMetadata(component));
348
244
  }
349
-
350
245
  if (component.kind === 'ic' && component.properties.RuntimeDescriptor === 'true') {
351
246
  issues.push(...validateRuntimeDescriptorMetadata(component));
352
247
  }
353
-
354
248
  return issues;
355
249
  }
356
-
357
- function validateJackSemanticMetadata(component: Component): readonly ValidationIssue[] {
358
- const issues: ValidationIssue[] = [];
359
-
360
- for (const property of ['Role', 'ControlRole'] as const) {
250
+ function validateJackSemanticMetadata(component) {
251
+ const issues = [];
252
+ for (const property of ['Role', 'ControlRole']) {
361
253
  const value = propertyString(component, property);
362
254
  if (value !== null && value.trim().length > 0 && !isRecognizedJackRole(value)) {
363
255
  issues.push({
@@ -369,7 +261,6 @@ function validateJackSemanticMetadata(component: Component): readonly Validation
369
261
  });
370
262
  }
371
263
  }
372
-
373
264
  const interfaceName = propertyString(component, 'Interface');
374
265
  if (interfaceName !== null && interfaceName.trim().length > 0 && !isRecognizedJackInterface(interfaceName)) {
375
266
  issues.push({
@@ -380,7 +271,6 @@ function validateJackSemanticMetadata(component: Component): readonly Validation
380
271
  property: 'Interface',
381
272
  });
382
273
  }
383
-
384
274
  const audioRole = propertyString(component, 'AudioRole');
385
275
  if (audioRole !== null && !isValidJackAudioRole(audioRole)) {
386
276
  issues.push({
@@ -391,13 +281,10 @@ function validateJackSemanticMetadata(component: Component): readonly Validation
391
281
  property: 'AudioRole',
392
282
  });
393
283
  }
394
-
395
284
  return issues;
396
285
  }
397
-
398
- function validateRuntimeDescriptorMetadata(component: Component): readonly ValidationIssue[] {
399
- const issues: ValidationIssue[] = [];
400
-
286
+ function validateRuntimeDescriptorMetadata(component) {
287
+ const issues = [];
401
288
  for (const property of RUNTIME_DESCRIPTOR_CONTROL_PROPERTIES) {
402
289
  const value = propertyString(component, property);
403
290
  if (value !== null && value.trim().length === 0) {
@@ -410,7 +297,6 @@ function validateRuntimeDescriptorMetadata(component: Component): readonly Valid
410
297
  });
411
298
  }
412
299
  }
413
-
414
300
  const labels = parseStringList(propertyStringAny(component, ['ModeLabels', 'ModeOptions']));
415
301
  const stepCount = parsePositiveInteger(propertyStringAny(component, ['ModeStepCount', 'ModeSteps', 'ModeCount']));
416
302
  if (labels.length > 0 && stepCount !== undefined && labels.length !== stepCount) {
@@ -422,11 +308,9 @@ function validateRuntimeDescriptorMetadata(component: Component): readonly Valid
422
308
  property: 'ModeLabels',
423
309
  });
424
310
  }
425
-
426
311
  return issues;
427
312
  }
428
-
429
- function shortSourceType(sourceTypeName: string | null): string | null {
313
+ function shortSourceType(sourceTypeName) {
430
314
  if (sourceTypeName === null) {
431
315
  return null;
432
316
  }
@@ -437,8 +321,7 @@ function shortSourceType(sourceTypeName: string | null): string | null {
437
321
  const lastDot = head.lastIndexOf('.');
438
322
  return lastDot >= 0 ? head.slice(lastDot + 1) : head;
439
323
  }
440
-
441
- function findProperty(component: Component, rule: PropertyRule): PropertyValue | undefined {
324
+ function findProperty(component, rule) {
442
325
  const candidates = [rule.name, ...(rule.aliases ?? [])];
443
326
  for (const name of candidates) {
444
327
  const value = component.properties[name];
@@ -448,12 +331,10 @@ function findProperty(component: Component, rule: PropertyRule): PropertyValue |
448
331
  }
449
332
  return undefined;
450
333
  }
451
-
452
- function propertyString(component: Component, name: string): string | null {
334
+ function propertyString(component, name) {
453
335
  return propertyStringValue(component.properties[name]);
454
336
  }
455
-
456
- function propertyStringAny(component: Component, names: readonly string[]): string | null {
337
+ function propertyStringAny(component, names) {
457
338
  for (const name of names) {
458
339
  const value = propertyString(component, name);
459
340
  if (value !== null) {
@@ -462,12 +343,10 @@ function propertyStringAny(component: Component, names: readonly string[]): stri
462
343
  }
463
344
  return null;
464
345
  }
465
-
466
- function coerceQuantity(value: PropertyValue): ParsedQuantity | null {
346
+ function coerceQuantity(value) {
467
347
  return propertyQuantityValue(value);
468
348
  }
469
-
470
- function isRawQuantityExpression(value: string): boolean {
349
+ function isRawQuantityExpression(value) {
471
350
  const trimmed = value.trim();
472
351
  if (trimmed.length === 0) {
473
352
  return false;
@@ -478,8 +357,7 @@ function isRawQuantityExpression(value: string): boolean {
478
357
  return /^(AC|DC)\b/i.test(trimmed) ||
479
358
  /^(SINE|PULSE|PWL|EXP|SFFM|AM|WAVEFILE)\s*\(/i.test(trimmed);
480
359
  }
481
-
482
- function isRecognizedJackRole(value: string): boolean {
360
+ function isRecognizedJackRole(value) {
483
361
  const normalized = normalizeToken(value);
484
362
  return [
485
363
  'input',
@@ -511,8 +389,7 @@ function isRecognizedJackRole(value: string): boolean {
511
389
  'reset',
512
390
  ].includes(normalized);
513
391
  }
514
-
515
- function isRecognizedJackInterface(value: string): boolean {
392
+ function isRecognizedJackInterface(value) {
516
393
  const normalized = normalizeToken(value);
517
394
  return isRecognizedJackRole(value) ||
518
395
  [
@@ -520,15 +397,17 @@ function isRecognizedJackInterface(value: string): boolean {
520
397
  'audio-port',
521
398
  'control',
522
399
  'control-port',
400
+ 'power',
401
+ 'power-port',
402
+ 'dc-power',
403
+ 'dc-power-input',
523
404
  'tap-tempo-input',
524
405
  ].includes(normalized);
525
406
  }
526
-
527
- function isValidJackAudioRole(value: string): boolean {
407
+ function isValidJackAudioRole(value) {
528
408
  return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
529
409
  }
530
-
531
- function parseStringList(value: string | null): readonly string[] {
410
+ function parseStringList(value) {
532
411
  if (value === null) {
533
412
  return [];
534
413
  }
@@ -537,8 +416,7 @@ function parseStringList(value: string | null): readonly string[] {
537
416
  .map((part) => part.trim())
538
417
  .filter((part) => part.length > 0);
539
418
  }
540
-
541
- function parsePositiveInteger(value: string | null): number | undefined {
419
+ function parsePositiveInteger(value) {
542
420
  if (value === null) {
543
421
  return undefined;
544
422
  }
@@ -549,23 +427,17 @@ function parsePositiveInteger(value: string | null): number | undefined {
549
427
  const count = Number(trimmed);
550
428
  return Number.isInteger(count) && count > 0 ? count : undefined;
551
429
  }
552
-
553
- function normalizeToken(value: string): string {
430
+ function normalizeToken(value) {
554
431
  return value.trim().toLowerCase().replace(/[\s_]+/g, '-');
555
432
  }
556
-
557
- function validateDeviceInterface(
558
- doc: CircuitDocument,
559
- componentIds: ReadonlySet<string>,
560
- ): readonly ValidationIssue[] {
561
- const issues: ValidationIssue[] = [];
433
+ function validateDeviceInterface(doc, componentIds) {
434
+ const issues = [];
562
435
  const groupIds = new Set(doc.controlGroups?.map((group) => group.id) ?? []);
563
436
  const contextIds = new Set(doc.controlContexts?.map((context) => context.id) ?? []);
564
- const semanticControlIds = new Set<string>();
437
+ const semanticControlIds = new Set();
565
438
  const externalInterfaceIds = new Set(doc.controlInterfaces?.map((controlInterface) => controlInterface.id) ?? []);
566
439
  const componentsById = new Map(doc.components.map((component) => [component.id, component]));
567
440
  const resolvedPanelElements = resolvePanelElements(doc);
568
-
569
441
  for (const group of doc.controlGroups ?? []) {
570
442
  issues.push(...validateOpenToken(group.role, group.id, 'role'));
571
443
  for (const contextId of group.contextIds ?? []) {
@@ -580,11 +452,9 @@ function validateDeviceInterface(
580
452
  }
581
453
  }
582
454
  }
583
-
584
455
  for (const context of doc.controlContexts ?? []) {
585
456
  issues.push(...validateOpenToken(context.role, context.id, 'role'));
586
457
  }
587
-
588
458
  for (const control of doc.deviceInterface?.controls ?? []) {
589
459
  if (semanticControlIds.has(control.id)) {
590
460
  issues.push({
@@ -595,9 +465,7 @@ function validateDeviceInterface(
595
465
  });
596
466
  }
597
467
  semanticControlIds.add(control.id);
598
-
599
468
  issues.push(...validateOpenToken(control.role, control.id, 'role'));
600
-
601
469
  if (control.groupId !== undefined && !groupIds.has(control.groupId)) {
602
470
  issues.push({
603
471
  code: 'device-interface-group-unresolved',
@@ -607,85 +475,54 @@ function validateDeviceInterface(
607
475
  property: 'groupId',
608
476
  });
609
477
  }
610
-
611
478
  issues.push(...validateApplicability(control, contextIds));
612
-
613
479
  if (control.binding !== undefined) {
614
- issues.push(...validateDeviceInterfaceBinding(
615
- control,
616
- control.binding,
617
- componentIds,
618
- externalInterfaceIds,
619
- componentsById,
620
- resolvedPanelElements,
621
- ));
480
+ issues.push(...validateDeviceInterfaceBinding(control, control.binding, componentIds, externalInterfaceIds, componentsById, resolvedPanelElements));
622
481
  }
623
482
  }
624
-
625
483
  issues.push(...validateDuplicateDeviceInterfaceRoles(doc.deviceInterface?.controls ?? []));
626
-
627
484
  return issues;
628
485
  }
629
-
630
- function validateOpenToken(value: string, componentId: string, property: string): readonly ValidationIssue[] {
486
+ function validateOpenToken(value, componentId, property) {
631
487
  if (/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(value)) {
632
488
  return [];
633
489
  }
634
490
  return [{
635
- code: 'invalid-device-interface-token',
636
- severity: 'warning',
637
- message: `${componentId}: ${property} "${value}" must be a lower-kebab token`,
638
- componentId,
639
- property,
640
- }];
641
- }
642
-
643
- function validateApplicability(
644
- control: DeviceInterfaceControl,
645
- contextIds: ReadonlySet<string>,
646
- ): readonly ValidationIssue[] {
647
- const issues: ValidationIssue[] = [];
491
+ code: 'invalid-device-interface-token',
492
+ severity: 'warning',
493
+ message: `${componentId}: ${property} "${value}" must be a lower-kebab token`,
494
+ componentId,
495
+ property,
496
+ }];
497
+ }
498
+ function validateApplicability(control, contextIds) {
499
+ const issues = [];
648
500
  if (control.appliesWhen === undefined) {
649
501
  return issues;
650
502
  }
651
-
652
503
  issues.push(...validateContextList(control.id, 'appliesWhen.allOf', control.appliesWhen.allOf, contextIds));
653
504
  issues.push(...validateContextList(control.id, 'appliesWhen.anyOf', control.appliesWhen.anyOf, contextIds));
654
-
655
- if (
656
- control.appliesWhen.allOf !== undefined
505
+ if (control.appliesWhen.allOf !== undefined
657
506
  && control.appliesWhen.allOf.length === 0
658
- && control.appliesWhen.anyOf === undefined
659
- ) {
507
+ && control.appliesWhen.anyOf === undefined) {
660
508
  issues.push(emptyApplicabilityIssue(control.id, 'appliesWhen.allOf'));
661
509
  }
662
- if (
663
- control.appliesWhen.anyOf !== undefined
510
+ if (control.appliesWhen.anyOf !== undefined
664
511
  && control.appliesWhen.anyOf.length === 0
665
- && control.appliesWhen.allOf === undefined
666
- ) {
512
+ && control.appliesWhen.allOf === undefined) {
667
513
  issues.push(emptyApplicabilityIssue(control.id, 'appliesWhen.anyOf'));
668
514
  }
669
-
670
515
  return issues;
671
516
  }
672
-
673
- function validateContextList(
674
- controlId: string,
675
- property: string,
676
- values: readonly string[] | undefined,
677
- contextIds: ReadonlySet<string>,
678
- ): readonly ValidationIssue[] {
517
+ function validateContextList(controlId, property, values, contextIds) {
679
518
  if (values === undefined) {
680
519
  return [];
681
520
  }
682
-
683
- const issues: ValidationIssue[] = [];
684
- const seen = new Set<string>();
521
+ const issues = [];
522
+ const seen = new Set();
685
523
  if (values.length === 0) {
686
524
  issues.push(emptyApplicabilityIssue(controlId, property));
687
525
  }
688
-
689
526
  for (const contextId of values) {
690
527
  if (seen.has(contextId)) {
691
528
  issues.push({
@@ -697,7 +534,6 @@ function validateContextList(
697
534
  });
698
535
  }
699
536
  seen.add(contextId);
700
-
701
537
  if (!contextIds.has(contextId)) {
702
538
  issues.push({
703
539
  code: 'device-interface-context-unresolved',
@@ -708,11 +544,9 @@ function validateContextList(
708
544
  });
709
545
  }
710
546
  }
711
-
712
547
  return issues;
713
548
  }
714
-
715
- function emptyApplicabilityIssue(controlId: string, property: string): ValidationIssue {
549
+ function emptyApplicabilityIssue(controlId, property) {
716
550
  return {
717
551
  code: 'device-interface-context-unresolved',
718
552
  severity: 'warning',
@@ -721,16 +555,8 @@ function emptyApplicabilityIssue(controlId: string, property: string): Validatio
721
555
  property,
722
556
  };
723
557
  }
724
-
725
- function validateDeviceInterfaceBinding(
726
- control: DeviceInterfaceControl,
727
- binding: DeviceInterfaceBinding,
728
- componentIds: ReadonlySet<string>,
729
- externalInterfaceIds: ReadonlySet<string>,
730
- componentsById: ReadonlyMap<string, Component>,
731
- resolvedPanelElements: readonly ResolvedPanelElement[],
732
- ): readonly ValidationIssue[] {
733
- const issues: ValidationIssue[] = [];
558
+ function validateDeviceInterfaceBinding(control, binding, componentIds, externalInterfaceIds, componentsById, resolvedPanelElements) {
559
+ const issues = [];
734
560
  if (binding.externalInterfaceId !== undefined && !externalInterfaceIds.has(binding.externalInterfaceId)) {
735
561
  issues.push({
736
562
  code: 'device-interface-binding-unresolved',
@@ -740,7 +566,6 @@ function validateDeviceInterfaceBinding(
740
566
  property: 'binding.externalInterfaceId',
741
567
  });
742
568
  }
743
-
744
569
  if (!componentIds.has(binding.componentId)) {
745
570
  issues.push({
746
571
  code: 'device-interface-binding-unresolved',
@@ -751,13 +576,8 @@ function validateDeviceInterfaceBinding(
751
576
  });
752
577
  return issues;
753
578
  }
754
-
755
- if (
756
- binding.controlId !== undefined
757
- && !resolvedPanelElements.some((resolved) =>
758
- resolved.componentId === binding.componentId && resolved.id === binding.controlId
759
- )
760
- ) {
579
+ if (binding.controlId !== undefined
580
+ && !resolvedPanelElements.some((resolved) => resolved.componentId === binding.componentId && resolved.id === binding.controlId)) {
761
581
  issues.push({
762
582
  code: 'device-interface-binding-unresolved',
763
583
  severity: 'warning',
@@ -766,7 +586,6 @@ function validateDeviceInterfaceBinding(
766
586
  property: 'binding.controlId',
767
587
  });
768
588
  }
769
-
770
589
  const component = componentsById.get(binding.componentId);
771
590
  if (binding.property !== undefined && component?.properties[binding.property] === undefined) {
772
591
  issues.push({
@@ -777,15 +596,11 @@ function validateDeviceInterfaceBinding(
777
596
  property: 'binding.property',
778
597
  });
779
598
  }
780
-
781
599
  return issues;
782
600
  }
783
-
784
- function validateDuplicateDeviceInterfaceRoles(
785
- controls: readonly DeviceInterfaceControl[],
786
- ): readonly ValidationIssue[] {
787
- const issues: ValidationIssue[] = [];
788
- const seen = new Map<string, DeviceInterfaceControl>();
601
+ function validateDuplicateDeviceInterfaceRoles(controls) {
602
+ const issues = [];
603
+ const seen = new Map();
789
604
  for (const control of controls) {
790
605
  const key = `${control.groupId ?? ''}:${control.role}`;
791
606
  const existing = seen.get(key);
@@ -804,8 +619,7 @@ function validateDuplicateDeviceInterfaceRoles(
804
619
  }
805
620
  return issues;
806
621
  }
807
-
808
- function deviceInterfaceBindingSignature(binding: DeviceInterfaceBinding | undefined): string {
622
+ function deviceInterfaceBindingSignature(binding) {
809
623
  if (binding === undefined) {
810
624
  return '';
811
625
  }
@@ -817,19 +631,12 @@ function deviceInterfaceBindingSignature(binding: DeviceInterfaceBinding | undef
817
631
  binding.externalInterfaceId ?? '',
818
632
  ].join(':');
819
633
  }
820
-
821
- function validatePanel(
822
- doc: CircuitDocument,
823
- componentIds: ReadonlySet<string>,
824
- semanticControlIds: ReadonlySet<string>,
825
- ): readonly ValidationIssue[] {
634
+ function validatePanel(doc, componentIds, semanticControlIds) {
826
635
  if (doc.panel === undefined) {
827
636
  return [];
828
637
  }
829
-
830
- const issues: ValidationIssue[] = [];
638
+ const issues = [];
831
639
  const resolvedElements = resolvePanelElements(doc);
832
-
833
640
  for (const face of doc.panel.faces) {
834
641
  for (const element of face.elements) {
835
642
  const componentId = element.bind.componentId;
@@ -851,7 +658,6 @@ function validatePanel(
851
658
  });
852
659
  continue;
853
660
  }
854
-
855
661
  const resolved = resolvePanelElement(resolvedElements, element);
856
662
  if (element.bind.controlId !== undefined && resolved === undefined) {
857
663
  issues.push({
@@ -863,8 +669,7 @@ function validatePanel(
863
669
  });
864
670
  continue;
865
671
  }
866
-
867
- if (resolved !== undefined && resolved.kind !== element.kind) {
672
+ if (resolved !== undefined && !panelKindsCompatible(element.kind, resolved.kind)) {
868
673
  issues.push({
869
674
  code: 'panel-kind-mismatch',
870
675
  severity: 'warning',
@@ -873,19 +678,21 @@ function validatePanel(
873
678
  });
874
679
  }
875
680
  }
876
-
877
681
  for (const issue of validatePanelCellCollisions(face)) {
878
682
  issues.push(issue);
879
683
  }
880
684
  }
881
-
882
685
  return issues;
883
686
  }
884
-
885
- function resolvePanelElements(doc: CircuitDocument): readonly ResolvedPanelElement[] {
687
+ function panelKindsCompatible(declared, resolved) {
688
+ if (declared === resolved) {
689
+ return true;
690
+ }
691
+ return resolved === 'switch' && (declared === 'selector' || declared === 'footswitch');
692
+ }
693
+ function resolvePanelElements(doc) {
886
694
  const panel = extractPanel(doc);
887
- const resolved: ResolvedPanelElement[] = [];
888
-
695
+ const resolved = [];
889
696
  for (const knob of panel.knobs) {
890
697
  resolved.push({
891
698
  id: knob.id,
@@ -921,34 +728,21 @@ function resolvePanelElements(doc: CircuitDocument): readonly ResolvedPanelEleme
921
728
  kind: 'jack',
922
729
  });
923
730
  }
924
-
925
731
  return resolved;
926
732
  }
927
-
928
- function resolvePanelElement(
929
- resolvedElements: readonly ResolvedPanelElement[],
930
- element: PanelElementPlacement,
931
- ): ResolvedPanelElement | undefined {
733
+ function resolvePanelElement(resolvedElements, element) {
932
734
  if (element.bind.controlId !== undefined) {
933
- return resolvedElements.find((resolved) =>
934
- resolved.componentId === element.bind.componentId && resolved.id === element.bind.controlId,
935
- );
735
+ return resolvedElements.find((resolved) => resolved.componentId === element.bind.componentId && resolved.id === element.bind.controlId);
936
736
  }
937
-
938
- return resolvedElements.find((resolved) =>
939
- resolved.componentId === element.bind.componentId && resolved.id === element.bind.componentId,
940
- );
737
+ return resolvedElements.find((resolved) => resolved.componentId === element.bind.componentId && resolved.id === element.bind.componentId);
941
738
  }
942
-
943
- function componentIdFromPanelElementId(id: string): string {
739
+ function componentIdFromPanelElementId(id) {
944
740
  const separator = id.indexOf(':');
945
741
  return separator <= 0 ? id : id.slice(0, separator);
946
742
  }
947
-
948
- function validatePanelCellCollisions(face: PanelFace): readonly ValidationIssue[] {
949
- const issues: ValidationIssue[] = [];
950
- const occupied = new Map<string, PanelElementPlacement>();
951
-
743
+ function validatePanelCellCollisions(face) {
744
+ const issues = [];
745
+ const occupied = new Map();
952
746
  for (const element of face.elements) {
953
747
  const rowSpan = element.grid.rowSpan ?? 1;
954
748
  const columnSpan = element.grid.columnSpan ?? 1;
@@ -970,11 +764,313 @@ function validatePanelCellCollisions(face: PanelFace): readonly ValidationIssue[
970
764
  }
971
765
  }
972
766
  }
973
-
974
767
  return issues;
975
768
  }
976
-
977
- function missingPropertyIssue(component: Component, rule: PropertyRule): ValidationIssue {
769
+ function validateV3BuildMetadata(doc, componentIds) {
770
+ if (!hasV3BuildMetadata(doc)) {
771
+ return [];
772
+ }
773
+ const issues = [];
774
+ const boards = doc.boards ?? [];
775
+ const boardsById = new Map(boards.map((board) => [board.id, board]));
776
+ const componentsById = new Map(doc.components.map((component) => [component.id, component]));
777
+ const panelElementIds = collectPanelElementIds(doc);
778
+ const controlIds = new Set(doc.deviceInterface?.controls.map((control) => control.id) ?? []);
779
+ const boardNetsByBoardId = new Map(boards.map((board) => [
780
+ board.id,
781
+ new Set(board.netlist?.nets.map((net) => net.id) ?? []),
782
+ ]));
783
+ const boardTerminalsByBoardId = new Map(boards.map((board) => [
784
+ board.id,
785
+ new Set(board.edgeTerminals.map((terminal) => terminal.id)),
786
+ ]));
787
+ const selectedBoardId = doc.build?.selectedBoardId;
788
+ if (selectedBoardId !== undefined && !boardsById.has(selectedBoardId)) {
789
+ issues.push(unresolvedIssue('build-board-unresolved', 'error', `Build selectedBoardId references missing board "${selectedBoardId}"`, selectedBoardId, 'selectedBoardId'));
790
+ }
791
+ for (const boardId of doc.build?.alternateBoardIds ?? []) {
792
+ if (!boardsById.has(boardId)) {
793
+ issues.push(unresolvedIssue('build-board-unresolved', 'warning', `Build alternateBoardIds references missing board "${boardId}"`, boardId, 'alternateBoardIds'));
794
+ }
795
+ }
796
+ const preferredBoardId = dataString(doc.mechanical?.internalBoard, 'preferredBoardId');
797
+ if (preferredBoardId !== undefined && !boardsById.has(preferredBoardId)) {
798
+ issues.push(unresolvedIssue('build-board-unresolved', 'warning', `Mechanical internalBoard.preferredBoardId references missing board "${preferredBoardId}"`, preferredBoardId, 'mechanical.internalBoard.preferredBoardId'));
799
+ }
800
+ const harnessesById = new Map(doc.offBoardWiring?.harnesses.map((harness) => [harness.id, harness]) ?? []);
801
+ for (const harnessId of doc.build?.selectedOffBoardWiringHarnessIds ?? []) {
802
+ if (!harnessesById.has(harnessId)) {
803
+ issues.push(unresolvedIssue('build-harness-unresolved', 'error', `Build selectedOffBoardWiringHarnessIds references missing harness "${harnessId}"`, harnessId, 'selectedOffBoardWiringHarnessIds'));
804
+ }
805
+ }
806
+ for (const item of doc.bom?.items ?? []) {
807
+ for (const ref of item.refs) {
808
+ const issue = validateBomRef(ref, componentIds, controlIds, panelElementIds, boardsById, item.id);
809
+ if (issue !== undefined) {
810
+ issues.push(issue);
811
+ }
812
+ }
813
+ }
814
+ for (const board of boards) {
815
+ issues.push(...validateBoardRealization(board, componentsById, boardNetsByBoardId));
816
+ }
817
+ if (doc.offBoardWiring !== undefined) {
818
+ issues.push(...validateOffBoardWiring(doc, componentsById, panelElementIds, boardTerminalsByBoardId, boardNetsByBoardId));
819
+ }
820
+ if (doc.build?.completeness === 'complete-selected-build' && selectedBoardId !== undefined) {
821
+ const selectedBoard = boardsById.get(selectedBoardId);
822
+ if (selectedBoard !== undefined) {
823
+ issues.push(...validateCompleteSelectedBoardRoutes(selectedBoard));
824
+ }
825
+ }
826
+ return issues;
827
+ }
828
+ function hasV3BuildMetadata(doc) {
829
+ return doc.mechanical !== undefined ||
830
+ doc.build !== undefined ||
831
+ doc.bom !== undefined ||
832
+ doc.partProfiles !== undefined ||
833
+ doc.footprints !== undefined ||
834
+ doc.offBoardWiring !== undefined ||
835
+ doc.boards !== undefined ||
836
+ doc.panel?.faces.some((face) => face.geometry !== undefined ||
837
+ face.elements.some((element) => element.id !== undefined || element.physical !== undefined)) === true;
838
+ }
839
+ function validateBomRef(ref, componentIds, controlIds, panelElementIds, boardsById, itemId) {
840
+ if (ref.kind === 'component' && (ref.componentId === undefined || !componentIds.has(ref.componentId))) {
841
+ return unresolvedIssue('bom-ref-unresolved', 'warning', `BOM item "${itemId}" references missing component "${ref.componentId ?? ''}"`, itemId, 'refs.componentId');
842
+ }
843
+ if (ref.kind === 'device-interface-control' &&
844
+ (ref.controlId === undefined || !controlIds.has(ref.controlId))) {
845
+ return unresolvedIssue('bom-ref-unresolved', 'warning', `BOM item "${itemId}" references missing device interface control "${ref.controlId ?? ''}"`, itemId, 'refs.controlId');
846
+ }
847
+ if (ref.kind === 'panel-element' && (ref.panelElementId === undefined || !panelElementIds.has(ref.panelElementId))) {
848
+ return unresolvedIssue('bom-ref-unresolved', 'warning', `BOM item "${itemId}" references missing panel element "${ref.panelElementId ?? ''}"`, itemId, 'refs.panelElementId');
849
+ }
850
+ if (ref.kind === 'board' && (ref.boardId === undefined || !boardsById.has(ref.boardId))) {
851
+ return unresolvedIssue('bom-ref-unresolved', 'warning', `BOM item "${itemId}" references missing board "${ref.boardId ?? ''}"`, itemId, 'refs.boardId');
852
+ }
853
+ return undefined;
854
+ }
855
+ function validateBoardRealization(board, componentsById, boardNetsByBoardId) {
856
+ const issues = [];
857
+ if (board.sourceCircuit !== undefined && !isDigestShapedSourceHash(board.sourceCircuit.hash)) {
858
+ issues.push({
859
+ code: 'board-source-hash-invalid',
860
+ severity: 'error',
861
+ message: `Board "${board.id}" sourceCircuit.hash must be sha256:<64 hex chars>`,
862
+ componentId: board.id,
863
+ property: 'sourceCircuit.hash',
864
+ });
865
+ }
866
+ for (const terminal of board.edgeTerminals) {
867
+ if (terminal.terminalRef !== undefined && !componentTerminalExists(componentsById, terminal.terminalRef)) {
868
+ issues.push(unresolvedIssue('board-terminal-unresolved', 'warning', `Board "${board.id}" edge terminal "${terminal.id}" references missing component terminal`, board.id, terminal.id));
869
+ }
870
+ }
871
+ for (const placement of board.footprintPlacements) {
872
+ if (!componentsById.has(placement.componentId)) {
873
+ issues.push(unresolvedIssue('board-terminal-unresolved', 'warning', `Board "${board.id}" places missing component "${placement.componentId}"`, board.id, placement.componentId));
874
+ continue;
875
+ }
876
+ for (const pad of placement.pads) {
877
+ if (pad.terminalName !== undefined &&
878
+ !componentHasTerminal(componentsById, placement.componentId, pad.terminalName)) {
879
+ issues.push(unresolvedIssue('board-terminal-unresolved', 'warning', `Board "${board.id}" pad "${pad.padId}" references missing terminal "${pad.terminalName}"`, board.id, pad.padId));
880
+ }
881
+ }
882
+ }
883
+ for (const net of board.netlist?.nets ?? []) {
884
+ for (const member of net.members) {
885
+ if (!componentTerminalExists(componentsById, member)) {
886
+ issues.push(unresolvedIssue('board-terminal-unresolved', 'warning', `Board "${board.id}" net "${net.id}" references missing component terminal`, board.id, net.id));
887
+ }
888
+ }
889
+ }
890
+ for (const route of board.routes) {
891
+ if (route.zones !== undefined || route.drills !== undefined) {
892
+ issues.push({
893
+ code: 'board-route-feature-invalid',
894
+ severity: 'error',
895
+ message: `Board "${board.id}" route "${route.id}" contains board-level zones or drills`,
896
+ componentId: board.id,
897
+ property: route.id,
898
+ });
899
+ }
900
+ if (isBoardNetlistRef(route.netRef) && !boardNetRefExists(route.netRef, board.id, boardNetsByBoardId)) {
901
+ issues.push(unresolvedIssue('offboard-signal-unresolved', 'warning', `Board "${board.id}" route "${route.id}" references missing board net "${route.netRef.netId}"`, board.id, route.id));
902
+ }
903
+ }
904
+ return issues;
905
+ }
906
+ function validateOffBoardWiring(doc, componentsById, panelElementIds, boardTerminalsByBoardId, boardNetsByBoardId) {
907
+ const issues = [];
908
+ const endpointIds = new Set();
909
+ for (const harness of doc.offBoardWiring?.harnesses ?? []) {
910
+ const localEndpointIds = new Set();
911
+ for (const endpoint of harness.endpoints) {
912
+ endpointIds.add(endpoint.id);
913
+ localEndpointIds.add(endpoint.id);
914
+ const issue = validateOffBoardEndpoint(endpoint, componentsById, panelElementIds, boardTerminalsByBoardId);
915
+ if (issue !== undefined) {
916
+ issues.push(issue);
917
+ }
918
+ }
919
+ for (const connection of harness.connections) {
920
+ if (!localEndpointIds.has(connection.fromEndpointId)) {
921
+ issues.push(unresolvedIssue('offboard-endpoint-unresolved', 'error', `Harness "${harness.id}" connection "${connection.id}" references missing endpoint "${connection.fromEndpointId}"`, harness.id, connection.id));
922
+ }
923
+ if (!localEndpointIds.has(connection.toEndpointId)) {
924
+ issues.push(unresolvedIssue('offboard-endpoint-unresolved', 'error', `Harness "${harness.id}" connection "${connection.id}" references missing endpoint "${connection.toEndpointId}"`, harness.id, connection.id));
925
+ }
926
+ if (connection.signalRef !== undefined) {
927
+ const issue = validateOffBoardSignalRef(connection.signalRef, componentsById, boardNetsByBoardId, harness.id);
928
+ if (issue !== undefined) {
929
+ issues.push(issue);
930
+ }
931
+ }
932
+ }
933
+ }
934
+ for (const harnessId of doc.build?.selectedOffBoardWiringHarnessIds ?? []) {
935
+ const harness = doc.offBoardWiring?.harnesses.find((candidate) => candidate.id === harnessId);
936
+ if (harness === undefined) {
937
+ continue;
938
+ }
939
+ for (const connection of harness.connections) {
940
+ if (!endpointIds.has(connection.fromEndpointId) || !endpointIds.has(connection.toEndpointId)) {
941
+ issues.push(unresolvedIssue('offboard-endpoint-unresolved', 'error', `Selected harness "${harnessId}" contains an unresolved connection endpoint`, harnessId, connection.id));
942
+ }
943
+ }
944
+ }
945
+ return issues;
946
+ }
947
+ function validateOffBoardEndpoint(endpoint, componentsById, panelElementIds, boardTerminalsByBoardId) {
948
+ if (endpoint.kind === 'board-terminal') {
949
+ const terminalIds = endpoint.boardId === undefined ? undefined : boardTerminalsByBoardId.get(endpoint.boardId);
950
+ if (terminalIds === undefined || endpoint.terminalId === undefined || !terminalIds.has(endpoint.terminalId)) {
951
+ return unresolvedIssue('offboard-endpoint-unresolved', 'error', `Off-board endpoint "${endpoint.id}" references missing board terminal`, endpoint.id, 'terminalId');
952
+ }
953
+ return undefined;
954
+ }
955
+ if (endpoint.kind === 'panel-component-terminal' ||
956
+ endpoint.kind === 'power-terminal' ||
957
+ endpoint.kind === 'footswitch-terminal') {
958
+ if (endpoint.componentId === undefined ||
959
+ endpoint.terminalName === undefined ||
960
+ !componentHasTerminal(componentsById, endpoint.componentId, endpoint.terminalName)) {
961
+ return unresolvedIssue('offboard-endpoint-unresolved', 'error', `Off-board endpoint "${endpoint.id}" references missing component terminal`, endpoint.id, 'componentId');
962
+ }
963
+ if (endpoint.panelElementId !== undefined &&
964
+ endpoint.kind !== 'power-terminal' &&
965
+ !panelElementIds.has(endpoint.panelElementId)) {
966
+ return unresolvedIssue('offboard-endpoint-unresolved', 'warning', `Off-board endpoint "${endpoint.id}" references missing panel element "${endpoint.panelElementId}"`, endpoint.id, 'panelElementId');
967
+ }
968
+ }
969
+ return undefined;
970
+ }
971
+ function validateOffBoardSignalRef(signalRef, componentsById, boardNetsByBoardId, harnessId) {
972
+ if (isBoardNetlistRef(signalRef)) {
973
+ if (!boardNetRefExists(signalRef, signalRef.boardId, boardNetsByBoardId)) {
974
+ return unresolvedIssue('offboard-signal-unresolved', 'error', `Harness "${harnessId}" references missing board net "${signalRef.netId}"`, harnessId, 'signalRef');
975
+ }
976
+ return undefined;
977
+ }
978
+ const member = dataObject(signalRef, 'member');
979
+ const componentId = dataString(member, 'componentId');
980
+ const terminalName = dataString(member, 'terminalName');
981
+ if (dataString(signalRef, 'source') === 'canonical-circuit' && componentId !== undefined && terminalName !== undefined) {
982
+ if (!componentHasTerminal(componentsById, componentId, terminalName)) {
983
+ return unresolvedIssue('offboard-signal-unresolved', 'error', `Harness "${harnessId}" references missing canonical component terminal`, harnessId, 'signalRef');
984
+ }
985
+ }
986
+ return undefined;
987
+ }
988
+ function validateCompleteSelectedBoardRoutes(board) {
989
+ const issues = [];
990
+ const routedNetIds = new Set(board.routes
991
+ .filter((route) => isRouteForBoardNet(route, board.id))
992
+ .map((route) => dataString(route.netRef, 'netId'))
993
+ .filter((netId) => netId !== undefined));
994
+ for (const net of board.netlist?.nets ?? []) {
995
+ if (isSingleTerminalEdgeNet(net)) {
996
+ continue;
997
+ }
998
+ if (!routedNetIds.has(net.id)) {
999
+ issues.push({
1000
+ code: 'board-net-unrouted',
1001
+ severity: 'error',
1002
+ message: `Selected board "${board.id}" net "${net.id}" has multiple members but no route`,
1003
+ componentId: board.id,
1004
+ property: net.id,
1005
+ });
1006
+ }
1007
+ }
1008
+ return issues;
1009
+ }
1010
+ function isSingleTerminalEdgeNet(net) {
1011
+ return net.members.length <= 1;
1012
+ }
1013
+ function isRouteForBoardNet(route, boardId) {
1014
+ if (!isBoardNetlistRef(route.netRef)) {
1015
+ return false;
1016
+ }
1017
+ return route.netRef.boardId === undefined || route.netRef.boardId === boardId;
1018
+ }
1019
+ function isBoardNetlistRef(value) {
1020
+ return dataString(value, 'source') === 'board-netlist' && dataString(value, 'netId') !== undefined;
1021
+ }
1022
+ function boardNetRefExists(ref, fallbackBoardId, boardNetsByBoardId) {
1023
+ const boardId = ref.boardId ?? fallbackBoardId;
1024
+ if (boardId === undefined) {
1025
+ return Array.from(boardNetsByBoardId.values()).some((netIds) => netIds.has(ref.netId));
1026
+ }
1027
+ return boardNetsByBoardId.get(boardId)?.has(ref.netId) === true;
1028
+ }
1029
+ function collectPanelElementIds(doc) {
1030
+ const ids = new Set();
1031
+ for (const face of doc.panel?.faces ?? []) {
1032
+ for (const element of face.elements) {
1033
+ if (element.id !== undefined) {
1034
+ ids.add(element.id);
1035
+ }
1036
+ ids.add(element.bind.componentId);
1037
+ if (element.bind.controlId !== undefined) {
1038
+ ids.add(element.bind.controlId);
1039
+ }
1040
+ }
1041
+ }
1042
+ return ids;
1043
+ }
1044
+ function componentTerminalExists(componentsById, ref) {
1045
+ return componentHasTerminal(componentsById, ref.componentId, ref.terminalName);
1046
+ }
1047
+ function componentHasTerminal(componentsById, componentId, terminalName) {
1048
+ return componentsById.get(componentId)?.terminals.some((terminal) => terminal.name === terminalName) === true;
1049
+ }
1050
+ function isDigestShapedSourceHash(hash) {
1051
+ return /^sha256:[0-9a-f]{64}$/i.test(hash);
1052
+ }
1053
+ function dataString(object, key) {
1054
+ const value = object?.[key];
1055
+ return typeof value === 'string' ? value : undefined;
1056
+ }
1057
+ function dataObject(object, key) {
1058
+ const value = object?.[key];
1059
+ return isBuildDataObject(value) ? value : undefined;
1060
+ }
1061
+ function isBuildDataObject(value) {
1062
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
1063
+ }
1064
+ function unresolvedIssue(code, severity, message, componentId, property) {
1065
+ return {
1066
+ code,
1067
+ severity,
1068
+ message,
1069
+ componentId,
1070
+ property,
1071
+ };
1072
+ }
1073
+ function missingPropertyIssue(component, rule) {
978
1074
  return {
979
1075
  code: rule.kind === 'string' ? 'model-required' : 'value-required',
980
1076
  severity: 'error',
@@ -983,3 +1079,4 @@ function missingPropertyIssue(component: Component, rule: PropertyRule): Validat
983
1079
  property: rule.name,
984
1080
  };
985
1081
  }
1082
+ //# sourceMappingURL=validation.js.map