@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.
- package/README.md +10 -0
- package/dist/editor/commands.d.ts +48 -0
- package/dist/editor/commands.d.ts.map +1 -0
- package/{src/editor/commands.ts → dist/editor/commands.js} +44 -91
- package/dist/editor/commands.js.map +1 -0
- package/dist/editor/factory.d.ts +10 -0
- package/dist/editor/factory.d.ts.map +1 -0
- package/{src/editor/factory.ts → dist/editor/factory.js} +11 -27
- package/dist/editor/factory.js.map +1 -0
- package/dist/editor/history.d.ts +29 -0
- package/dist/editor/history.d.ts.map +1 -0
- package/{src/editor/history.ts → dist/editor/history.js} +12 -42
- package/dist/editor/history.js.map +1 -0
- package/{src/editor/index.ts → dist/editor/index.d.ts} +1 -3
- package/dist/editor/index.d.ts.map +1 -0
- package/dist/editor/index.js +5 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/editor/layout.d.ts +8 -0
- package/dist/editor/layout.d.ts.map +1 -0
- package/{src/editor/layout.ts → dist/editor/layout.js} +36 -90
- package/dist/editor/layout.js.map +1 -0
- package/dist/formats/circuit-json/serializer.d.ts +86 -0
- package/dist/formats/circuit-json/serializer.d.ts.map +1 -0
- package/{src/formats/circuit-json/serializer.ts → dist/formats/circuit-json/serializer.js} +114 -414
- package/dist/formats/circuit-json/serializer.js.map +1 -0
- package/dist/formats/document.d.ts +64 -0
- package/dist/formats/document.d.ts.map +1 -0
- package/dist/formats/document.js +300 -0
- package/dist/formats/document.js.map +1 -0
- package/dist/formats/interchange/parser.d.ts +3 -0
- package/dist/formats/interchange/parser.d.ts.map +1 -0
- package/{src/formats/interchange/parser.ts → dist/formats/interchange/parser.js} +651 -299
- package/dist/formats/interchange/parser.js.map +1 -0
- package/dist/formats/interchange/serializer.d.ts +9 -0
- package/dist/formats/interchange/serializer.d.ts.map +1 -0
- package/{src/formats/interchange/serializer.ts → dist/formats/interchange/serializer.js} +151 -158
- package/dist/formats/interchange/serializer.js.map +1 -0
- package/dist/formats/ltspice/catalog.d.ts +19 -0
- package/dist/formats/ltspice/catalog.d.ts.map +1 -0
- package/{src/formats/ltspice/catalog.ts → dist/formats/ltspice/catalog.js} +18 -52
- package/dist/formats/ltspice/catalog.js.map +1 -0
- package/dist/formats/ltspice/encoding.d.ts +2 -0
- package/dist/formats/ltspice/encoding.d.ts.map +1 -0
- package/{src/formats/ltspice/encoding.ts → dist/formats/ltspice/encoding.js} +17 -41
- package/dist/formats/ltspice/encoding.js.map +1 -0
- package/dist/formats/ltspice/parser.d.ts +3 -0
- package/dist/formats/ltspice/parser.d.ts.map +1 -0
- package/{src/formats/ltspice/parser.ts → dist/formats/ltspice/parser.js} +39 -141
- package/dist/formats/ltspice/parser.js.map +1 -0
- package/dist/formats/ltspice/serializer.d.ts +7 -0
- package/dist/formats/ltspice/serializer.d.ts.map +1 -0
- package/{src/formats/ltspice/serializer.ts → dist/formats/ltspice/serializer.js} +18 -45
- package/dist/formats/ltspice/serializer.js.map +1 -0
- package/dist/formats/schx/catalog.d.ts +19 -0
- package/dist/formats/schx/catalog.d.ts.map +1 -0
- package/{src/formats/schx/catalog.ts → dist/formats/schx/catalog.js} +48 -101
- package/dist/formats/schx/catalog.js.map +1 -0
- package/dist/formats/schx/parser.d.ts +3 -0
- package/dist/formats/schx/parser.d.ts.map +1 -0
- package/{src/formats/schx/parser.ts → dist/formats/schx/parser.js} +31 -86
- package/dist/formats/schx/parser.js.map +1 -0
- package/dist/formats/schx/runtime-descriptors.d.ts +3 -0
- package/dist/formats/schx/runtime-descriptors.d.ts.map +1 -0
- package/{src/formats/schx/runtime-descriptors.ts → dist/formats/schx/runtime-descriptors.js} +36 -123
- package/dist/formats/schx/runtime-descriptors.js.map +1 -0
- package/dist/formats/schx/serializer.d.ts +5 -0
- package/dist/formats/schx/serializer.d.ts.map +1 -0
- package/{src/formats/schx/serializer.ts → dist/formats/schx/serializer.js} +17 -42
- package/dist/formats/schx/serializer.js.map +1 -0
- package/dist/formats/schx/transforms.d.ts +4 -0
- package/dist/formats/schx/transforms.d.ts.map +1 -0
- package/{src/formats/schx/transforms.ts → dist/formats/schx/transforms.js} +6 -10
- package/dist/formats/schx/transforms.js.map +1 -0
- package/dist/formats/spice/parser.d.ts +3 -0
- package/dist/formats/spice/parser.d.ts.map +1 -0
- package/{src/formats/spice/parser.ts → dist/formats/spice/parser.js} +50 -96
- package/dist/formats/spice/parser.js.map +1 -0
- package/dist/formats/spice/serializer.d.ts +3 -0
- package/dist/formats/spice/serializer.d.ts.map +1 -0
- package/{src/formats/spice/serializer.ts → dist/formats/spice/serializer.js} +8 -13
- package/dist/formats/spice/serializer.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/model/connectivity.d.ts +16 -0
- package/dist/model/connectivity.d.ts.map +1 -0
- package/{src/model/connectivity.ts → dist/model/connectivity.js} +28 -63
- package/dist/model/connectivity.js.map +1 -0
- package/dist/model/netlist.d.ts +24 -0
- package/dist/model/netlist.d.ts.map +1 -0
- package/{src/model/netlist.ts → dist/model/netlist.js} +42 -110
- package/dist/model/netlist.js.map +1 -0
- package/dist/model/properties.d.ts +9 -0
- package/dist/model/properties.d.ts.map +1 -0
- package/{src/model/properties.ts → dist/model/properties.js} +10 -18
- package/dist/model/properties.js.map +1 -0
- package/dist/model/quantity.d.ts +3 -0
- package/dist/model/quantity.d.ts.map +1 -0
- package/{src/model/quantity.ts → dist/model/quantity.js} +7 -30
- package/dist/model/quantity.js.map +1 -0
- package/dist/model/types.d.ts +431 -0
- package/dist/model/types.d.ts.map +1 -0
- package/dist/model/types.js +10 -0
- package/dist/model/types.js.map +1 -0
- package/dist/model/validation.d.ts +32 -0
- package/dist/model/validation.d.ts.map +1 -0
- package/{src/model/validation.ts → dist/model/validation.js} +420 -323
- package/dist/model/validation.js.map +1 -0
- package/dist/model/wires.d.ts +3 -0
- package/dist/model/wires.d.ts.map +1 -0
- package/{src/model/wires.ts → dist/model/wires.js} +10 -16
- package/dist/model/wires.js.map +1 -0
- package/dist/panel/extract.d.ts +5 -0
- package/dist/panel/extract.d.ts.map +1 -0
- package/{src/panel/extract.ts → dist/panel/extract.js} +146 -235
- package/dist/panel/extract.js.map +1 -0
- package/dist/panel/index.d.ts +6 -0
- package/dist/panel/index.d.ts.map +1 -0
- package/dist/panel/index.js +5 -0
- package/dist/panel/index.js.map +1 -0
- package/dist/panel/knobs.d.ts +7 -0
- package/dist/panel/knobs.d.ts.map +1 -0
- package/{src/panel/knobs.ts → dist/panel/knobs.js} +7 -18
- package/dist/panel/knobs.js.map +1 -0
- package/dist/panel/protocol.d.ts +9 -0
- package/dist/panel/protocol.d.ts.map +1 -0
- package/{src/panel/protocol.ts → dist/panel/protocol.js} +10 -26
- package/dist/panel/protocol.js.map +1 -0
- package/{src/panel/types.ts → dist/panel/types.d.ts} +50 -89
- package/dist/panel/types.d.ts.map +1 -0
- package/dist/panel/types.js +2 -0
- package/dist/panel/types.js.map +1 -0
- package/dist/preview/bounds.d.ts +12 -0
- package/dist/preview/bounds.d.ts.map +1 -0
- package/{src/preview/bounds.ts → dist/preview/bounds.js} +15 -29
- package/dist/preview/bounds.js.map +1 -0
- package/dist/preview/box-layout.d.ts +4 -0
- package/dist/preview/box-layout.d.ts.map +1 -0
- package/{src/preview/box-layout.ts → dist/preview/box-layout.js} +2 -6
- package/dist/preview/box-layout.js.map +1 -0
- package/dist/preview/colors.d.ts +3 -0
- package/dist/preview/colors.d.ts.map +1 -0
- package/{src/preview/colors.ts → dist/preview/colors.js} +3 -5
- package/dist/preview/colors.js.map +1 -0
- package/dist/preview/hanging.d.ts +8 -0
- package/dist/preview/hanging.d.ts.map +1 -0
- package/{src/preview/hanging.ts → dist/preview/hanging.js} +9 -28
- package/dist/preview/hanging.js.map +1 -0
- package/dist/preview/junctions.d.ts +3 -0
- package/dist/preview/junctions.d.ts.map +1 -0
- package/{src/preview/junctions.ts → dist/preview/junctions.js} +9 -24
- package/dist/preview/junctions.js.map +1 -0
- package/dist/preview/label-layout.d.ts +12 -0
- package/dist/preview/label-layout.d.ts.map +1 -0
- package/{src/preview/label-layout.ts → dist/preview/label-layout.js} +15 -36
- package/dist/preview/label-layout.js.map +1 -0
- package/dist/preview/ports.d.ts +17 -0
- package/dist/preview/ports.d.ts.map +1 -0
- package/{src/preview/ports.ts → dist/preview/ports.js} +10 -37
- package/dist/preview/ports.js.map +1 -0
- package/dist/preview/renderable-wires.d.ts +3 -0
- package/dist/preview/renderable-wires.d.ts.map +1 -0
- package/{src/preview/renderable-wires.ts → dist/preview/renderable-wires.js} +12 -29
- package/dist/preview/renderable-wires.js.map +1 -0
- package/dist/preview/routing.d.ts +4 -0
- package/dist/preview/routing.d.ts.map +1 -0
- package/dist/preview/routing.js +13 -0
- package/dist/preview/routing.js.map +1 -0
- package/dist/preview/snap.d.ts +9 -0
- package/dist/preview/snap.d.ts.map +1 -0
- package/{src/preview/snap.ts → dist/preview/snap.js} +9 -31
- package/dist/preview/snap.js.map +1 -0
- package/dist/preview/symbols/svg-content.d.ts +7 -0
- package/dist/preview/symbols/svg-content.d.ts.map +1 -0
- package/{src/preview/symbols/svg-content.ts → dist/preview/symbols/svg-content.js} +3 -6
- package/dist/preview/symbols/svg-content.js.map +1 -0
- package/dist/preview/symbols.d.ts +7 -0
- package/dist/preview/symbols.d.ts.map +1 -0
- package/{src/preview/symbols.ts → dist/preview/symbols.js} +18 -43
- package/dist/preview/symbols.js.map +1 -0
- package/dist/preview/wire-chains.d.ts +4 -0
- package/dist/preview/wire-chains.d.ts.map +1 -0
- package/{src/preview/wire-chains.ts → dist/preview/wire-chains.js} +37 -37
- package/dist/preview/wire-chains.js.map +1 -0
- package/package.json +3 -3
- package/src/formats/document.ts +0 -274
- package/src/index.ts +0 -205
- package/src/model/types.ts +0 -309
- package/src/panel/index.ts +0 -39
- package/src/preview/routing.ts +0 -15
- package/src/preview/symbols/analog-switch.svg +0 -17
- package/src/preview/symbols/battery.svg +0 -16
- package/src/preview/symbols/bbd.svg +0 -21
- package/src/preview/symbols/bjt-npn.svg +0 -16
- package/src/preview/symbols/bjt-pnp.svg +0 -17
- package/src/preview/symbols/capacitor-electrolytic.svg +0 -13
- package/src/preview/symbols/capacitor.svg +0 -12
- package/src/preview/symbols/current-source.svg +0 -14
- package/src/preview/symbols/delay-ic.svg +0 -22
- package/src/preview/symbols/diode-schottky.svg +0 -12
- package/src/preview/symbols/diode-zener.svg +0 -12
- package/src/preview/symbols/diode.svg +0 -13
- package/src/preview/symbols/flipflop.svg +0 -20
- package/src/preview/symbols/ground.svg +0 -12
- package/src/preview/symbols/ic-block.svg +0 -20
- package/src/preview/symbols/ic.svg +0 -19
- package/src/preview/symbols/inductor.svg +0 -11
- package/src/preview/symbols/jack-input.svg +0 -16
- package/src/preview/symbols/jack-output.svg +0 -16
- package/src/preview/symbols/jfet-junction-n.svg +0 -17
- package/src/preview/symbols/jfet-n.svg +0 -17
- package/src/preview/symbols/jfet-p.svg +0 -17
- package/src/preview/symbols/label.svg +0 -8
- package/src/preview/symbols/led.svg +0 -18
- package/src/preview/symbols/mosfet-n.svg +0 -21
- package/src/preview/symbols/mosfet-p.svg +0 -21
- package/src/preview/symbols/named-wire.svg +0 -11
- package/src/preview/symbols/opamp.svg +0 -21
- package/src/preview/symbols/optocoupler.svg +0 -30
- package/src/preview/symbols/ota.svg +0 -20
- package/src/preview/symbols/pentode.svg +0 -25
- package/src/preview/symbols/photoresistor.svg +0 -19
- package/src/preview/symbols/port.svg +0 -8
- package/src/preview/symbols/potentiometer.svg +0 -15
- package/src/preview/symbols/power-amp.svg +0 -20
- package/src/preview/symbols/rail.svg +0 -11
- package/src/preview/symbols/regulator.svg +0 -13
- package/src/preview/symbols/relay.svg +0 -20
- package/src/preview/symbols/resistor.svg +0 -11
- package/src/preview/symbols/switch-3pdt.svg +0 -32
- package/src/preview/symbols/switch-rotary.svg +0 -23
- package/src/preview/symbols/switch-spdt.svg +0 -16
- package/src/preview/symbols/switch-spst.svg +0 -14
- package/src/preview/symbols/switch-toggle.svg +0 -14
- package/src/preview/symbols/transformer.svg +0 -17
- package/src/preview/symbols/triode.svg +0 -17
- package/src/preview/symbols/tube-diode.svg +0 -13
- package/src/preview/symbols/unsupported.svg +0 -8
- package/src/preview/symbols/variable-resistor.svg +0 -13
- 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
|
-
|
|
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
|
|
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
|
|
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
|
-
]
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
57
|
+
kind: 'quantity', name: 'V', required: true, unit: 'V',
|
|
58
|
+
aliases: ['Voltage', 'voltage', 'v'],
|
|
59
|
+
}],
|
|
140
60
|
'current-source': [{
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
61
|
+
kind: 'quantity', name: 'I', required: true, unit: 'A',
|
|
62
|
+
aliases: ['Current', 'current', 'i'],
|
|
63
|
+
}],
|
|
144
64
|
battery: [{
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
65
|
+
kind: 'quantity', name: 'V', required: true, unit: 'V',
|
|
66
|
+
aliases: ['Voltage', 'voltage', 'v'],
|
|
67
|
+
}],
|
|
148
68
|
rail: [{
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
358
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
785
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
949
|
-
const
|
|
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
|
-
|
|
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
|