@vessel-dsp/core 0.5.0 → 0.6.0
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/package.json +1 -1
- package/src/formats/document.ts +145 -3
- package/src/formats/interchange/parser.ts +667 -9
- package/src/formats/interchange/serializer.ts +96 -1
- package/src/index.ts +51 -1
- package/src/model/types.ts +302 -1
- package/src/model/validation.ts +579 -1
package/src/model/validation.ts
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import { propertyQuantityValue, propertyStringValue } from './properties';
|
|
2
2
|
import { extractPanel } from '../panel/extract';
|
|
3
3
|
import type {
|
|
4
|
+
BoardNet,
|
|
5
|
+
BoardRealization,
|
|
6
|
+
BoardRoute,
|
|
7
|
+
BuildBomRef,
|
|
4
8
|
CircuitDocument,
|
|
5
9
|
Component,
|
|
6
10
|
ComponentKind,
|
|
7
11
|
DeviceInterfaceBinding,
|
|
8
12
|
DeviceInterfaceControl,
|
|
13
|
+
OffBoardSignalRef,
|
|
14
|
+
OffBoardWiringEndpoint,
|
|
9
15
|
PanelControlKind,
|
|
10
16
|
PanelElementPlacement,
|
|
11
17
|
PanelFace,
|
|
12
18
|
ParsedQuantity,
|
|
13
19
|
PropertyValue,
|
|
20
|
+
VdspBuildDataObject,
|
|
14
21
|
} from './types';
|
|
15
22
|
|
|
16
23
|
export type ValidationSeverity = 'error' | 'warning';
|
|
@@ -39,6 +46,15 @@ export type ValidationCode =
|
|
|
39
46
|
| 'panel-control-unresolved'
|
|
40
47
|
| 'panel-kind-mismatch'
|
|
41
48
|
| 'panel-cell-collision'
|
|
49
|
+
| 'build-board-unresolved'
|
|
50
|
+
| 'build-harness-unresolved'
|
|
51
|
+
| 'bom-ref-unresolved'
|
|
52
|
+
| 'offboard-endpoint-unresolved'
|
|
53
|
+
| 'offboard-signal-unresolved'
|
|
54
|
+
| 'board-source-hash-invalid'
|
|
55
|
+
| 'board-terminal-unresolved'
|
|
56
|
+
| 'board-route-feature-invalid'
|
|
57
|
+
| 'board-net-unrouted'
|
|
42
58
|
| 'duplicate-id'
|
|
43
59
|
| 'degenerate-wire';
|
|
44
60
|
|
|
@@ -301,6 +317,10 @@ export function validateDocument(doc: CircuitDocument): readonly ValidationIssue
|
|
|
301
317
|
issues.push(issue);
|
|
302
318
|
}
|
|
303
319
|
|
|
320
|
+
for (const issue of validateV3BuildMetadata(doc, seen)) {
|
|
321
|
+
issues.push(issue);
|
|
322
|
+
}
|
|
323
|
+
|
|
304
324
|
return issues;
|
|
305
325
|
}
|
|
306
326
|
|
|
@@ -520,6 +540,10 @@ function isRecognizedJackInterface(value: string): boolean {
|
|
|
520
540
|
'audio-port',
|
|
521
541
|
'control',
|
|
522
542
|
'control-port',
|
|
543
|
+
'power',
|
|
544
|
+
'power-port',
|
|
545
|
+
'dc-power',
|
|
546
|
+
'dc-power-input',
|
|
523
547
|
'tap-tempo-input',
|
|
524
548
|
].includes(normalized);
|
|
525
549
|
}
|
|
@@ -864,7 +888,7 @@ function validatePanel(
|
|
|
864
888
|
continue;
|
|
865
889
|
}
|
|
866
890
|
|
|
867
|
-
if (resolved !== undefined &&
|
|
891
|
+
if (resolved !== undefined && !panelKindsCompatible(element.kind, resolved.kind)) {
|
|
868
892
|
issues.push({
|
|
869
893
|
code: 'panel-kind-mismatch',
|
|
870
894
|
severity: 'warning',
|
|
@@ -882,6 +906,13 @@ function validatePanel(
|
|
|
882
906
|
return issues;
|
|
883
907
|
}
|
|
884
908
|
|
|
909
|
+
function panelKindsCompatible(declared: PanelControlKind, resolved: PanelControlKind): boolean {
|
|
910
|
+
if (declared === resolved) {
|
|
911
|
+
return true;
|
|
912
|
+
}
|
|
913
|
+
return resolved === 'switch' && (declared === 'selector' || declared === 'footswitch');
|
|
914
|
+
}
|
|
915
|
+
|
|
885
916
|
function resolvePanelElements(doc: CircuitDocument): readonly ResolvedPanelElement[] {
|
|
886
917
|
const panel = extractPanel(doc);
|
|
887
918
|
const resolved: ResolvedPanelElement[] = [];
|
|
@@ -974,6 +1005,553 @@ function validatePanelCellCollisions(face: PanelFace): readonly ValidationIssue[
|
|
|
974
1005
|
return issues;
|
|
975
1006
|
}
|
|
976
1007
|
|
|
1008
|
+
function validateV3BuildMetadata(
|
|
1009
|
+
doc: CircuitDocument,
|
|
1010
|
+
componentIds: ReadonlySet<string>,
|
|
1011
|
+
): readonly ValidationIssue[] {
|
|
1012
|
+
if (!hasV3BuildMetadata(doc)) {
|
|
1013
|
+
return [];
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const issues: ValidationIssue[] = [];
|
|
1017
|
+
const boards = doc.boards ?? [];
|
|
1018
|
+
const boardsById = new Map(boards.map((board) => [board.id, board]));
|
|
1019
|
+
const componentsById = new Map(doc.components.map((component) => [component.id, component]));
|
|
1020
|
+
const panelElementIds = collectPanelElementIds(doc);
|
|
1021
|
+
const controlIds = new Set(doc.deviceInterface?.controls.map((control) => control.id) ?? []);
|
|
1022
|
+
const boardNetsByBoardId = new Map(boards.map((board) => [
|
|
1023
|
+
board.id,
|
|
1024
|
+
new Set(board.netlist?.nets.map((net) => net.id) ?? []),
|
|
1025
|
+
]));
|
|
1026
|
+
const boardTerminalsByBoardId = new Map(boards.map((board) => [
|
|
1027
|
+
board.id,
|
|
1028
|
+
new Set(board.edgeTerminals.map((terminal) => terminal.id)),
|
|
1029
|
+
]));
|
|
1030
|
+
|
|
1031
|
+
const selectedBoardId = doc.build?.selectedBoardId;
|
|
1032
|
+
if (selectedBoardId !== undefined && !boardsById.has(selectedBoardId)) {
|
|
1033
|
+
issues.push(unresolvedIssue(
|
|
1034
|
+
'build-board-unresolved',
|
|
1035
|
+
'error',
|
|
1036
|
+
`Build selectedBoardId references missing board "${selectedBoardId}"`,
|
|
1037
|
+
selectedBoardId,
|
|
1038
|
+
'selectedBoardId',
|
|
1039
|
+
));
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
for (const boardId of doc.build?.alternateBoardIds ?? []) {
|
|
1043
|
+
if (!boardsById.has(boardId)) {
|
|
1044
|
+
issues.push(unresolvedIssue(
|
|
1045
|
+
'build-board-unresolved',
|
|
1046
|
+
'warning',
|
|
1047
|
+
`Build alternateBoardIds references missing board "${boardId}"`,
|
|
1048
|
+
boardId,
|
|
1049
|
+
'alternateBoardIds',
|
|
1050
|
+
));
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const preferredBoardId = dataString(doc.mechanical?.internalBoard, 'preferredBoardId');
|
|
1055
|
+
if (preferredBoardId !== undefined && !boardsById.has(preferredBoardId)) {
|
|
1056
|
+
issues.push(unresolvedIssue(
|
|
1057
|
+
'build-board-unresolved',
|
|
1058
|
+
'warning',
|
|
1059
|
+
`Mechanical internalBoard.preferredBoardId references missing board "${preferredBoardId}"`,
|
|
1060
|
+
preferredBoardId,
|
|
1061
|
+
'mechanical.internalBoard.preferredBoardId',
|
|
1062
|
+
));
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
const harnessesById = new Map(doc.offBoardWiring?.harnesses.map((harness) => [harness.id, harness]) ?? []);
|
|
1066
|
+
for (const harnessId of doc.build?.selectedOffBoardWiringHarnessIds ?? []) {
|
|
1067
|
+
if (!harnessesById.has(harnessId)) {
|
|
1068
|
+
issues.push(unresolvedIssue(
|
|
1069
|
+
'build-harness-unresolved',
|
|
1070
|
+
'error',
|
|
1071
|
+
`Build selectedOffBoardWiringHarnessIds references missing harness "${harnessId}"`,
|
|
1072
|
+
harnessId,
|
|
1073
|
+
'selectedOffBoardWiringHarnessIds',
|
|
1074
|
+
));
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
for (const item of doc.bom?.items ?? []) {
|
|
1079
|
+
for (const ref of item.refs) {
|
|
1080
|
+
const issue = validateBomRef(ref, componentIds, controlIds, panelElementIds, boardsById, item.id);
|
|
1081
|
+
if (issue !== undefined) {
|
|
1082
|
+
issues.push(issue);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
for (const board of boards) {
|
|
1088
|
+
issues.push(...validateBoardRealization(board, componentsById, boardNetsByBoardId));
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
if (doc.offBoardWiring !== undefined) {
|
|
1092
|
+
issues.push(...validateOffBoardWiring(
|
|
1093
|
+
doc,
|
|
1094
|
+
componentsById,
|
|
1095
|
+
panelElementIds,
|
|
1096
|
+
boardTerminalsByBoardId,
|
|
1097
|
+
boardNetsByBoardId,
|
|
1098
|
+
));
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
if (doc.build?.completeness === 'complete-selected-build' && selectedBoardId !== undefined) {
|
|
1102
|
+
const selectedBoard = boardsById.get(selectedBoardId);
|
|
1103
|
+
if (selectedBoard !== undefined) {
|
|
1104
|
+
issues.push(...validateCompleteSelectedBoardRoutes(selectedBoard));
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
return issues;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function hasV3BuildMetadata(doc: CircuitDocument): boolean {
|
|
1112
|
+
return doc.mechanical !== undefined ||
|
|
1113
|
+
doc.build !== undefined ||
|
|
1114
|
+
doc.bom !== undefined ||
|
|
1115
|
+
doc.partProfiles !== undefined ||
|
|
1116
|
+
doc.footprints !== undefined ||
|
|
1117
|
+
doc.offBoardWiring !== undefined ||
|
|
1118
|
+
doc.boards !== undefined ||
|
|
1119
|
+
doc.panel?.faces.some((face) =>
|
|
1120
|
+
face.geometry !== undefined ||
|
|
1121
|
+
face.elements.some((element) => element.id !== undefined || element.physical !== undefined)
|
|
1122
|
+
) === true;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function validateBomRef(
|
|
1126
|
+
ref: BuildBomRef,
|
|
1127
|
+
componentIds: ReadonlySet<string>,
|
|
1128
|
+
controlIds: ReadonlySet<string>,
|
|
1129
|
+
panelElementIds: ReadonlySet<string>,
|
|
1130
|
+
boardsById: ReadonlyMap<string, BoardRealization>,
|
|
1131
|
+
itemId: string,
|
|
1132
|
+
): ValidationIssue | undefined {
|
|
1133
|
+
if (ref.kind === 'component' && (ref.componentId === undefined || !componentIds.has(ref.componentId))) {
|
|
1134
|
+
return unresolvedIssue(
|
|
1135
|
+
'bom-ref-unresolved',
|
|
1136
|
+
'warning',
|
|
1137
|
+
`BOM item "${itemId}" references missing component "${ref.componentId ?? ''}"`,
|
|
1138
|
+
itemId,
|
|
1139
|
+
'refs.componentId',
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
if (
|
|
1143
|
+
ref.kind === 'device-interface-control' &&
|
|
1144
|
+
(ref.controlId === undefined || !controlIds.has(ref.controlId))
|
|
1145
|
+
) {
|
|
1146
|
+
return unresolvedIssue(
|
|
1147
|
+
'bom-ref-unresolved',
|
|
1148
|
+
'warning',
|
|
1149
|
+
`BOM item "${itemId}" references missing device interface control "${ref.controlId ?? ''}"`,
|
|
1150
|
+
itemId,
|
|
1151
|
+
'refs.controlId',
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
if (ref.kind === 'panel-element' && (ref.panelElementId === undefined || !panelElementIds.has(ref.panelElementId))) {
|
|
1155
|
+
return unresolvedIssue(
|
|
1156
|
+
'bom-ref-unresolved',
|
|
1157
|
+
'warning',
|
|
1158
|
+
`BOM item "${itemId}" references missing panel element "${ref.panelElementId ?? ''}"`,
|
|
1159
|
+
itemId,
|
|
1160
|
+
'refs.panelElementId',
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
if (ref.kind === 'board' && (ref.boardId === undefined || !boardsById.has(ref.boardId))) {
|
|
1164
|
+
return unresolvedIssue(
|
|
1165
|
+
'bom-ref-unresolved',
|
|
1166
|
+
'warning',
|
|
1167
|
+
`BOM item "${itemId}" references missing board "${ref.boardId ?? ''}"`,
|
|
1168
|
+
itemId,
|
|
1169
|
+
'refs.boardId',
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
return undefined;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function validateBoardRealization(
|
|
1176
|
+
board: BoardRealization,
|
|
1177
|
+
componentsById: ReadonlyMap<string, Component>,
|
|
1178
|
+
boardNetsByBoardId: ReadonlyMap<string, ReadonlySet<string>>,
|
|
1179
|
+
): readonly ValidationIssue[] {
|
|
1180
|
+
const issues: ValidationIssue[] = [];
|
|
1181
|
+
|
|
1182
|
+
if (board.sourceCircuit !== undefined && !isDigestShapedSourceHash(board.sourceCircuit.hash)) {
|
|
1183
|
+
issues.push({
|
|
1184
|
+
code: 'board-source-hash-invalid',
|
|
1185
|
+
severity: 'error',
|
|
1186
|
+
message: `Board "${board.id}" sourceCircuit.hash must be sha256:<64 hex chars>`,
|
|
1187
|
+
componentId: board.id,
|
|
1188
|
+
property: 'sourceCircuit.hash',
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
for (const terminal of board.edgeTerminals) {
|
|
1193
|
+
if (terminal.terminalRef !== undefined && !componentTerminalExists(componentsById, terminal.terminalRef)) {
|
|
1194
|
+
issues.push(unresolvedIssue(
|
|
1195
|
+
'board-terminal-unresolved',
|
|
1196
|
+
'warning',
|
|
1197
|
+
`Board "${board.id}" edge terminal "${terminal.id}" references missing component terminal`,
|
|
1198
|
+
board.id,
|
|
1199
|
+
terminal.id,
|
|
1200
|
+
));
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
for (const placement of board.footprintPlacements) {
|
|
1205
|
+
if (!componentsById.has(placement.componentId)) {
|
|
1206
|
+
issues.push(unresolvedIssue(
|
|
1207
|
+
'board-terminal-unresolved',
|
|
1208
|
+
'warning',
|
|
1209
|
+
`Board "${board.id}" places missing component "${placement.componentId}"`,
|
|
1210
|
+
board.id,
|
|
1211
|
+
placement.componentId,
|
|
1212
|
+
));
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
for (const pad of placement.pads) {
|
|
1216
|
+
if (
|
|
1217
|
+
pad.terminalName !== undefined &&
|
|
1218
|
+
!componentHasTerminal(componentsById, placement.componentId, pad.terminalName)
|
|
1219
|
+
) {
|
|
1220
|
+
issues.push(unresolvedIssue(
|
|
1221
|
+
'board-terminal-unresolved',
|
|
1222
|
+
'warning',
|
|
1223
|
+
`Board "${board.id}" pad "${pad.padId}" references missing terminal "${pad.terminalName}"`,
|
|
1224
|
+
board.id,
|
|
1225
|
+
pad.padId,
|
|
1226
|
+
));
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
for (const net of board.netlist?.nets ?? []) {
|
|
1232
|
+
for (const member of net.members) {
|
|
1233
|
+
if (!componentTerminalExists(componentsById, member)) {
|
|
1234
|
+
issues.push(unresolvedIssue(
|
|
1235
|
+
'board-terminal-unresolved',
|
|
1236
|
+
'warning',
|
|
1237
|
+
`Board "${board.id}" net "${net.id}" references missing component terminal`,
|
|
1238
|
+
board.id,
|
|
1239
|
+
net.id,
|
|
1240
|
+
));
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
for (const route of board.routes) {
|
|
1246
|
+
if (route.zones !== undefined || route.drills !== undefined) {
|
|
1247
|
+
issues.push({
|
|
1248
|
+
code: 'board-route-feature-invalid',
|
|
1249
|
+
severity: 'error',
|
|
1250
|
+
message: `Board "${board.id}" route "${route.id}" contains board-level zones or drills`,
|
|
1251
|
+
componentId: board.id,
|
|
1252
|
+
property: route.id,
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
if (isBoardNetlistRef(route.netRef) && !boardNetRefExists(route.netRef, board.id, boardNetsByBoardId)) {
|
|
1256
|
+
issues.push(unresolvedIssue(
|
|
1257
|
+
'offboard-signal-unresolved',
|
|
1258
|
+
'warning',
|
|
1259
|
+
`Board "${board.id}" route "${route.id}" references missing board net "${route.netRef.netId}"`,
|
|
1260
|
+
board.id,
|
|
1261
|
+
route.id,
|
|
1262
|
+
));
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
return issues;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function validateOffBoardWiring(
|
|
1270
|
+
doc: CircuitDocument,
|
|
1271
|
+
componentsById: ReadonlyMap<string, Component>,
|
|
1272
|
+
panelElementIds: ReadonlySet<string>,
|
|
1273
|
+
boardTerminalsByBoardId: ReadonlyMap<string, ReadonlySet<string>>,
|
|
1274
|
+
boardNetsByBoardId: ReadonlyMap<string, ReadonlySet<string>>,
|
|
1275
|
+
): readonly ValidationIssue[] {
|
|
1276
|
+
const issues: ValidationIssue[] = [];
|
|
1277
|
+
const endpointIds = new Set<string>();
|
|
1278
|
+
|
|
1279
|
+
for (const harness of doc.offBoardWiring?.harnesses ?? []) {
|
|
1280
|
+
const localEndpointIds = new Set<string>();
|
|
1281
|
+
for (const endpoint of harness.endpoints) {
|
|
1282
|
+
endpointIds.add(endpoint.id);
|
|
1283
|
+
localEndpointIds.add(endpoint.id);
|
|
1284
|
+
const issue = validateOffBoardEndpoint(endpoint, componentsById, panelElementIds, boardTerminalsByBoardId);
|
|
1285
|
+
if (issue !== undefined) {
|
|
1286
|
+
issues.push(issue);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
for (const connection of harness.connections) {
|
|
1291
|
+
if (!localEndpointIds.has(connection.fromEndpointId)) {
|
|
1292
|
+
issues.push(unresolvedIssue(
|
|
1293
|
+
'offboard-endpoint-unresolved',
|
|
1294
|
+
'error',
|
|
1295
|
+
`Harness "${harness.id}" connection "${connection.id}" references missing endpoint "${connection.fromEndpointId}"`,
|
|
1296
|
+
harness.id,
|
|
1297
|
+
connection.id,
|
|
1298
|
+
));
|
|
1299
|
+
}
|
|
1300
|
+
if (!localEndpointIds.has(connection.toEndpointId)) {
|
|
1301
|
+
issues.push(unresolvedIssue(
|
|
1302
|
+
'offboard-endpoint-unresolved',
|
|
1303
|
+
'error',
|
|
1304
|
+
`Harness "${harness.id}" connection "${connection.id}" references missing endpoint "${connection.toEndpointId}"`,
|
|
1305
|
+
harness.id,
|
|
1306
|
+
connection.id,
|
|
1307
|
+
));
|
|
1308
|
+
}
|
|
1309
|
+
if (connection.signalRef !== undefined) {
|
|
1310
|
+
const issue = validateOffBoardSignalRef(connection.signalRef, componentsById, boardNetsByBoardId, harness.id);
|
|
1311
|
+
if (issue !== undefined) {
|
|
1312
|
+
issues.push(issue);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
for (const harnessId of doc.build?.selectedOffBoardWiringHarnessIds ?? []) {
|
|
1319
|
+
const harness = doc.offBoardWiring?.harnesses.find((candidate) => candidate.id === harnessId);
|
|
1320
|
+
if (harness === undefined) {
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
for (const connection of harness.connections) {
|
|
1324
|
+
if (!endpointIds.has(connection.fromEndpointId) || !endpointIds.has(connection.toEndpointId)) {
|
|
1325
|
+
issues.push(unresolvedIssue(
|
|
1326
|
+
'offboard-endpoint-unresolved',
|
|
1327
|
+
'error',
|
|
1328
|
+
`Selected harness "${harnessId}" contains an unresolved connection endpoint`,
|
|
1329
|
+
harnessId,
|
|
1330
|
+
connection.id,
|
|
1331
|
+
));
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
return issues;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function validateOffBoardEndpoint(
|
|
1340
|
+
endpoint: OffBoardWiringEndpoint,
|
|
1341
|
+
componentsById: ReadonlyMap<string, Component>,
|
|
1342
|
+
panelElementIds: ReadonlySet<string>,
|
|
1343
|
+
boardTerminalsByBoardId: ReadonlyMap<string, ReadonlySet<string>>,
|
|
1344
|
+
): ValidationIssue | undefined {
|
|
1345
|
+
if (endpoint.kind === 'board-terminal') {
|
|
1346
|
+
const terminalIds = endpoint.boardId === undefined ? undefined : boardTerminalsByBoardId.get(endpoint.boardId);
|
|
1347
|
+
if (terminalIds === undefined || endpoint.terminalId === undefined || !terminalIds.has(endpoint.terminalId)) {
|
|
1348
|
+
return unresolvedIssue(
|
|
1349
|
+
'offboard-endpoint-unresolved',
|
|
1350
|
+
'error',
|
|
1351
|
+
`Off-board endpoint "${endpoint.id}" references missing board terminal`,
|
|
1352
|
+
endpoint.id,
|
|
1353
|
+
'terminalId',
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
return undefined;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
if (
|
|
1360
|
+
endpoint.kind === 'panel-component-terminal' ||
|
|
1361
|
+
endpoint.kind === 'power-terminal' ||
|
|
1362
|
+
endpoint.kind === 'footswitch-terminal'
|
|
1363
|
+
) {
|
|
1364
|
+
if (
|
|
1365
|
+
endpoint.componentId === undefined ||
|
|
1366
|
+
endpoint.terminalName === undefined ||
|
|
1367
|
+
!componentHasTerminal(componentsById, endpoint.componentId, endpoint.terminalName)
|
|
1368
|
+
) {
|
|
1369
|
+
return unresolvedIssue(
|
|
1370
|
+
'offboard-endpoint-unresolved',
|
|
1371
|
+
'error',
|
|
1372
|
+
`Off-board endpoint "${endpoint.id}" references missing component terminal`,
|
|
1373
|
+
endpoint.id,
|
|
1374
|
+
'componentId',
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
if (
|
|
1378
|
+
endpoint.panelElementId !== undefined &&
|
|
1379
|
+
endpoint.kind !== 'power-terminal' &&
|
|
1380
|
+
!panelElementIds.has(endpoint.panelElementId)
|
|
1381
|
+
) {
|
|
1382
|
+
return unresolvedIssue(
|
|
1383
|
+
'offboard-endpoint-unresolved',
|
|
1384
|
+
'warning',
|
|
1385
|
+
`Off-board endpoint "${endpoint.id}" references missing panel element "${endpoint.panelElementId}"`,
|
|
1386
|
+
endpoint.id,
|
|
1387
|
+
'panelElementId',
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
return undefined;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function validateOffBoardSignalRef(
|
|
1396
|
+
signalRef: OffBoardSignalRef,
|
|
1397
|
+
componentsById: ReadonlyMap<string, Component>,
|
|
1398
|
+
boardNetsByBoardId: ReadonlyMap<string, ReadonlySet<string>>,
|
|
1399
|
+
harnessId: string,
|
|
1400
|
+
): ValidationIssue | undefined {
|
|
1401
|
+
if (isBoardNetlistRef(signalRef)) {
|
|
1402
|
+
if (!boardNetRefExists(signalRef, signalRef.boardId, boardNetsByBoardId)) {
|
|
1403
|
+
return unresolvedIssue(
|
|
1404
|
+
'offboard-signal-unresolved',
|
|
1405
|
+
'error',
|
|
1406
|
+
`Harness "${harnessId}" references missing board net "${signalRef.netId}"`,
|
|
1407
|
+
harnessId,
|
|
1408
|
+
'signalRef',
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
return undefined;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
const member = dataObject(signalRef, 'member');
|
|
1415
|
+
const componentId = dataString(member, 'componentId');
|
|
1416
|
+
const terminalName = dataString(member, 'terminalName');
|
|
1417
|
+
if (dataString(signalRef, 'source') === 'canonical-circuit' && componentId !== undefined && terminalName !== undefined) {
|
|
1418
|
+
if (!componentHasTerminal(componentsById, componentId, terminalName)) {
|
|
1419
|
+
return unresolvedIssue(
|
|
1420
|
+
'offboard-signal-unresolved',
|
|
1421
|
+
'error',
|
|
1422
|
+
`Harness "${harnessId}" references missing canonical component terminal`,
|
|
1423
|
+
harnessId,
|
|
1424
|
+
'signalRef',
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
return undefined;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
function validateCompleteSelectedBoardRoutes(board: BoardRealization): readonly ValidationIssue[] {
|
|
1433
|
+
const issues: ValidationIssue[] = [];
|
|
1434
|
+
const routedNetIds = new Set(
|
|
1435
|
+
board.routes
|
|
1436
|
+
.filter((route) => isRouteForBoardNet(route, board.id))
|
|
1437
|
+
.map((route) => dataString(route.netRef, 'netId'))
|
|
1438
|
+
.filter((netId): netId is string => netId !== undefined),
|
|
1439
|
+
);
|
|
1440
|
+
|
|
1441
|
+
for (const net of board.netlist?.nets ?? []) {
|
|
1442
|
+
if (isSingleTerminalEdgeNet(net)) {
|
|
1443
|
+
continue;
|
|
1444
|
+
}
|
|
1445
|
+
if (!routedNetIds.has(net.id)) {
|
|
1446
|
+
issues.push({
|
|
1447
|
+
code: 'board-net-unrouted',
|
|
1448
|
+
severity: 'error',
|
|
1449
|
+
message: `Selected board "${board.id}" net "${net.id}" has multiple members but no route`,
|
|
1450
|
+
componentId: board.id,
|
|
1451
|
+
property: net.id,
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
return issues;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
function isSingleTerminalEdgeNet(net: BoardNet): boolean {
|
|
1460
|
+
return net.members.length <= 1;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
function isRouteForBoardNet(route: BoardRoute, boardId: string): boolean {
|
|
1464
|
+
if (!isBoardNetlistRef(route.netRef)) {
|
|
1465
|
+
return false;
|
|
1466
|
+
}
|
|
1467
|
+
return route.netRef.boardId === undefined || route.netRef.boardId === boardId;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
function isBoardNetlistRef(value: VdspBuildDataObject | undefined): value is VdspBuildDataObject & {
|
|
1471
|
+
source: 'board-netlist';
|
|
1472
|
+
boardId?: string;
|
|
1473
|
+
netId: string;
|
|
1474
|
+
} {
|
|
1475
|
+
return dataString(value, 'source') === 'board-netlist' && dataString(value, 'netId') !== undefined;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
function boardNetRefExists(
|
|
1479
|
+
ref: VdspBuildDataObject & { netId: string; boardId?: string },
|
|
1480
|
+
fallbackBoardId: string | undefined,
|
|
1481
|
+
boardNetsByBoardId: ReadonlyMap<string, ReadonlySet<string>>,
|
|
1482
|
+
): boolean {
|
|
1483
|
+
const boardId = ref.boardId ?? fallbackBoardId;
|
|
1484
|
+
if (boardId === undefined) {
|
|
1485
|
+
return Array.from(boardNetsByBoardId.values()).some((netIds) => netIds.has(ref.netId));
|
|
1486
|
+
}
|
|
1487
|
+
return boardNetsByBoardId.get(boardId)?.has(ref.netId) === true;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
function collectPanelElementIds(doc: CircuitDocument): ReadonlySet<string> {
|
|
1491
|
+
const ids = new Set<string>();
|
|
1492
|
+
for (const face of doc.panel?.faces ?? []) {
|
|
1493
|
+
for (const element of face.elements) {
|
|
1494
|
+
if (element.id !== undefined) {
|
|
1495
|
+
ids.add(element.id);
|
|
1496
|
+
}
|
|
1497
|
+
ids.add(element.bind.componentId);
|
|
1498
|
+
if (element.bind.controlId !== undefined) {
|
|
1499
|
+
ids.add(element.bind.controlId);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
return ids;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function componentTerminalExists(
|
|
1507
|
+
componentsById: ReadonlyMap<string, Component>,
|
|
1508
|
+
ref: Readonly<{ componentId: string; terminalName: string }>,
|
|
1509
|
+
): boolean {
|
|
1510
|
+
return componentHasTerminal(componentsById, ref.componentId, ref.terminalName);
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function componentHasTerminal(
|
|
1514
|
+
componentsById: ReadonlyMap<string, Component>,
|
|
1515
|
+
componentId: string,
|
|
1516
|
+
terminalName: string,
|
|
1517
|
+
): boolean {
|
|
1518
|
+
return componentsById.get(componentId)?.terminals.some((terminal) => terminal.name === terminalName) === true;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function isDigestShapedSourceHash(hash: string): boolean {
|
|
1522
|
+
return /^sha256:[0-9a-f]{64}$/i.test(hash);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
function dataString(object: VdspBuildDataObject | undefined, key: string): string | undefined {
|
|
1526
|
+
const value = object?.[key];
|
|
1527
|
+
return typeof value === 'string' ? value : undefined;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function dataObject(object: VdspBuildDataObject | undefined, key: string): VdspBuildDataObject | undefined {
|
|
1531
|
+
const value = object?.[key];
|
|
1532
|
+
return isBuildDataObject(value) ? value : undefined;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
function isBuildDataObject(value: unknown): value is VdspBuildDataObject {
|
|
1536
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
function unresolvedIssue(
|
|
1540
|
+
code: ValidationCode,
|
|
1541
|
+
severity: ValidationSeverity,
|
|
1542
|
+
message: string,
|
|
1543
|
+
componentId: string,
|
|
1544
|
+
property: string,
|
|
1545
|
+
): ValidationIssue {
|
|
1546
|
+
return {
|
|
1547
|
+
code,
|
|
1548
|
+
severity,
|
|
1549
|
+
message,
|
|
1550
|
+
componentId,
|
|
1551
|
+
property,
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
|
|
977
1555
|
function missingPropertyIssue(component: Component, rule: PropertyRule): ValidationIssue {
|
|
978
1556
|
return {
|
|
979
1557
|
code: rule.kind === 'string' ? 'model-required' : 'value-required',
|