@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 CHANGED
@@ -4,3 +4,13 @@ Headless Vessel DSP circuit, device, format conversion, and layout model APIs.
4
4
 
5
5
  This package has no React, DOM rendering, AudioContext, or AudioWorklet
6
6
  dependency.
7
+
8
+ `.vdsp` parsing supports `circuit-interchange/v2` and `circuit-interchange/v3`.
9
+ V3 documents preserve reviewed physical build metadata such as build scope,
10
+ mechanical envelopes, BOM rows, embedded part and footprint catalogs,
11
+ off-board wiring, panel drill placement, and board realizations for stripboard,
12
+ perfboard, breadboard-pattern protoboard, and fabricated PCB.
13
+
14
+ Conversions from v3 `.vdsp` to formats that cannot represent those physical
15
+ fields throw by default. Use `convertCircuitDocumentFileWithReport()` with
16
+ `lossPolicy: 'drop-with-diagnostics'` when intentional lossy export is needed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vessel-dsp/core",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Headless Vessel DSP circuit, device, format conversion, and layout model APIs.",
5
5
  "keywords": [
6
6
  "guitar-pedal",
@@ -38,6 +38,24 @@ export type ConvertCircuitDocumentFileOptions = Readonly<{
38
38
  outputFilename?: string;
39
39
  }>;
40
40
 
41
+ export type CircuitDocumentConversionLossPolicy = 'error' | 'drop-with-diagnostics';
42
+
43
+ export type CircuitDocumentConversionDiagnostic = Readonly<{
44
+ code: 'v3-data-dropped';
45
+ message: string;
46
+ field: string;
47
+ }>;
48
+
49
+ export type ConvertCircuitDocumentFileWithReportOptions = ConvertCircuitDocumentFileOptions & Readonly<{
50
+ lossPolicy?: CircuitDocumentConversionLossPolicy;
51
+ }>;
52
+
53
+ export type CircuitDocumentFileConversionReport = Readonly<{
54
+ output: string;
55
+ diagnostics: readonly CircuitDocumentConversionDiagnostic[];
56
+ droppedFields: readonly string[];
57
+ }>;
58
+
41
59
  export type VdspSchemaValidationIssue = Readonly<{
42
60
  code: 'vdsp-schema-invalid';
43
61
  message: string;
@@ -190,6 +208,8 @@ export function serializeCircuitDocumentFile(
190
208
  document: CircuitDocument,
191
209
  options: SerializeCircuitDocumentFileOptions,
192
210
  ): string {
211
+ assertNoV3OnlyDataDrop(document, options.format);
212
+
193
213
  switch (options.format) {
194
214
  case 'vdsp':
195
215
  case 'yaml':
@@ -212,12 +232,45 @@ export function convertCircuitDocumentFile(
212
232
  source: string,
213
233
  options: ConvertCircuitDocumentFileOptions,
214
234
  ): string {
215
- return serializeCircuitDocumentFile(parseCircuitDocumentFile(source, {
235
+ return convertCircuitDocumentFileWithReport(source, options).output;
236
+ }
237
+
238
+ export function convertCircuitDocumentFileWithReport(
239
+ source: string,
240
+ options: ConvertCircuitDocumentFileWithReportOptions,
241
+ ): CircuitDocumentFileConversionReport {
242
+ const document = parseCircuitDocumentFile(source, {
216
243
  filename: options.inputFilename,
217
- }), {
244
+ });
245
+ const droppedFields = v3OnlyDroppedFields(document);
246
+ const targetOptions: SerializeCircuitDocumentFileOptions = {
218
247
  format: options.outputFormat,
219
248
  ...(options.outputFilename === undefined ? {} : { filename: options.outputFilename }),
220
- });
249
+ };
250
+
251
+ if (isVdspOutputFormat(options.outputFormat) || droppedFields.length === 0) {
252
+ return {
253
+ output: serializeCircuitDocumentFile(document, targetOptions),
254
+ diagnostics: [],
255
+ droppedFields: [],
256
+ };
257
+ }
258
+
259
+ if (options.lossPolicy !== 'drop-with-diagnostics') {
260
+ throw new Error(v3DropMessage(options.outputFormat, droppedFields));
261
+ }
262
+
263
+ const diagnostics = droppedFields.map((field): CircuitDocumentConversionDiagnostic => ({
264
+ code: 'v3-data-dropped',
265
+ field,
266
+ message: `Dropped v3-only field "${field}" while converting to ${options.outputFormat}`,
267
+ }));
268
+
269
+ return {
270
+ output: serializeCircuitDocumentFile(stripV3OnlyData(document), targetOptions),
271
+ diagnostics,
272
+ droppedFields,
273
+ };
221
274
  }
222
275
 
223
276
  export function serializeVdspCircuitDocument(
@@ -261,6 +314,95 @@ function normalizeVdspFilename(filename: string | undefined, fallbackName: strin
261
314
  return `${withoutExtension || 'untitled-preset'}${vdspFileExtension}`;
262
315
  }
263
316
 
317
+ function assertNoV3OnlyDataDrop(document: CircuitDocument, format: CircuitDocumentFileFormat): void {
318
+ if (isVdspOutputFormat(format)) {
319
+ return;
320
+ }
321
+ const droppedFields = v3OnlyDroppedFields(document);
322
+ if (droppedFields.length > 0) {
323
+ throw new Error(v3DropMessage(format, droppedFields));
324
+ }
325
+ }
326
+
327
+ function v3DropMessage(format: CircuitDocumentFileFormat, droppedFields: readonly string[]): string {
328
+ return `Converting to ${format} would drop v3-only fields: ${droppedFields.join(', ')}`;
329
+ }
330
+
331
+ function isVdspOutputFormat(format: CircuitDocumentFileFormat): boolean {
332
+ return format === 'vdsp' || format === 'yaml';
333
+ }
334
+
335
+ function v3OnlyDroppedFields(document: CircuitDocument): readonly string[] {
336
+ const fields: string[] = [];
337
+ if (document.mechanical !== undefined) {
338
+ fields.push('mechanical');
339
+ }
340
+ if (document.build !== undefined) {
341
+ fields.push('build');
342
+ }
343
+ if (document.bom !== undefined) {
344
+ fields.push('bom');
345
+ }
346
+ if (document.partProfiles !== undefined) {
347
+ fields.push('partProfiles');
348
+ }
349
+ if (document.footprints !== undefined) {
350
+ fields.push('footprints');
351
+ }
352
+ if (document.offBoardWiring !== undefined) {
353
+ fields.push('offBoardWiring');
354
+ }
355
+ if (document.panel?.faces.some((face) => face.geometry !== undefined) === true) {
356
+ fields.push('panel.faces[].geometry');
357
+ }
358
+ if (document.panel?.faces.some((face) => face.elements.some((element) => element.id !== undefined)) === true) {
359
+ fields.push('panel.faces[].elements[].id');
360
+ }
361
+ if (document.panel?.faces.some((face) => face.elements.some((element) => element.physical !== undefined)) === true) {
362
+ fields.push('panel.faces[].elements[].physical');
363
+ }
364
+ if (document.boards !== undefined) {
365
+ fields.push('boards');
366
+ }
367
+ return fields;
368
+ }
369
+
370
+ function stripV3OnlyData(document: CircuitDocument): CircuitDocument {
371
+ return {
372
+ metadata: document.metadata,
373
+ ...(document.source === undefined ? {} : { source: document.source }),
374
+ ...(document.device === undefined ? {} : { device: document.device }),
375
+ ...(document.controlGroups === undefined ? {} : { controlGroups: document.controlGroups }),
376
+ ...(document.controlContexts === undefined ? {} : { controlContexts: document.controlContexts }),
377
+ ...(document.deviceInterface === undefined ? {} : { deviceInterface: document.deviceInterface }),
378
+ ...(document.panel === undefined ? {} : {
379
+ panel: {
380
+ faces: document.panel.faces.map((face) => ({
381
+ id: face.id,
382
+ ...(face.label === undefined ? {} : { label: face.label }),
383
+ layout: face.layout,
384
+ elements: face.elements.map((element) => ({
385
+ bind: element.bind,
386
+ kind: element.kind,
387
+ ...(element.label === undefined ? {} : { label: element.label }),
388
+ ...(element.interfaceControlId === undefined
389
+ ? {}
390
+ : { interfaceControlId: element.interfaceControlId }),
391
+ grid: element.grid,
392
+ })),
393
+ })),
394
+ },
395
+ }),
396
+ ...(document.controlInterfaces === undefined ? {} : { controlInterfaces: document.controlInterfaces }),
397
+ ...(document.controlOutputs === undefined ? {} : { controlOutputs: document.controlOutputs }),
398
+ components: document.components,
399
+ wires: document.wires,
400
+ directives: document.directives,
401
+ warnings: document.warnings,
402
+ rawAttributes: document.rawAttributes,
403
+ };
404
+ }
405
+
264
406
  function pathFromSchemaMessage(message: string): string | undefined {
265
407
  const colonIndex = message.indexOf(':');
266
408
  if (colonIndex <= 0) {