import type { AssayMatrixTemplateData, CalibrationCurveTemplateData, DoseResponseTemplateData, FlowCytometryPanelTemplateData, InstrumentRunTemplateData, PlateMapTemplateData, ProtocolStepsTemplateData, QpcrPlateTemplateData, ReagentListTemplateData, SamplePrepTemplateData, SampleSheetTemplateData, TimeCourseTemplateData, } from './types' import { assertWellInFormat } from '../components/WellPlate.geometry' /** @deprecated Scheduled for removal from the core SDK in MINT 1.4; the biology templates move to a dedicated package or the owning plugins. */ export function validatePlateMapData(data: PlateMapTemplateData): void { if (!Array.isArray(data.plates) || data.plates.length === 0) { throw new Error('Plate-map template requires at least one plate.') } const sampleIds = new Set(data.samples.map(sample => sample.id)) if (sampleIds.size !== data.samples.length) { throw new Error('Plate-map sample ids must be unique.') } for (const plate of data.plates) { for (const [wellId, well] of Object.entries(plate.wells)) { if (well.id !== wellId) { throw new Error(`Well key '${wellId}' does not match well id '${well.id}'.`) } if (well.sampleType && !sampleIds.has(well.sampleType)) { throw new Error(`Well '${well.id}' references unknown sample '${well.sampleType}'.`) } } } } /** @deprecated Scheduled for removal from the core SDK in MINT 1.4; the biology templates move to a dedicated package or the owning plugins. */ export function validateSampleSheetData(data: SampleSheetTemplateData): void { if (!Array.isArray(data.samples) || data.samples.length === 0) { throw new Error('Sample-sheet template requires at least one sample.') } const sampleIds = new Set(data.samples.map(sample => sample.sampleId)) if (sampleIds.size !== data.samples.length) { throw new Error('Sample-sheet sample ids must be unique.') } for (const group of data.groups) { for (const sampleId of group.samples) { if (!sampleIds.has(sampleId)) { throw new Error(`Group '${group.name}' references unknown sample '${sampleId}'.`) } } } } /** @deprecated Scheduled for removal from the core SDK in MINT 1.4; the biology templates move to a dedicated package or the owning plugins. */ export function validateSamplePrepData(data: SamplePrepTemplateData): void { if (!data.protocolName) { throw new Error('Sample-prep template requires protocolName.') } if (!Array.isArray(data.steps) || data.steps.length === 0) { throw new Error('Sample-prep template requires at least one step.') } const stepIds = new Set(data.steps.map(step => step.id)) if (stepIds.size !== data.steps.length) { throw new Error('Sample-prep step ids must be unique.') } const orders = new Set(data.steps.map(step => step.order)) if (orders.size !== data.steps.length) { throw new Error('Sample-prep step orders must be unique.') } for (const step of data.steps) { if (!step.id || !step.name) { throw new Error('Sample-prep steps require id and name.') } if (step.order < 0) { throw new Error(`Sample-prep step '${step.id}' has a negative order.`) } if (!step.volumeUnit) { throw new Error(`Sample-prep step '${step.id}' requires volumeUnit.`) } if (step.inputVolume !== undefined && step.inputVolume < 0) { throw new Error(`Sample-prep step '${step.id}' has a negative input volume.`) } if (step.outputVolume !== undefined && step.outputVolume < 0) { throw new Error(`Sample-prep step '${step.id}' has a negative output volume.`) } if (step.durationMin !== undefined && step.durationMin < 0) { throw new Error(`Sample-prep step '${step.id}' has a negative duration.`) } if (!step.sourceSampleId && !step.destinationSampleId && !step.reagentId) { throw new Error(`Sample-prep step '${step.id}' requires a source, destination, or reagent.`) } } } /** @deprecated Scheduled for removal from the core SDK in MINT 1.4; the biology templates move to a dedicated package or the owning plugins. */ export function validateDoseResponseData(data: DoseResponseTemplateData): void { if (!Array.isArray(data.compounds) || data.compounds.length === 0) { throw new Error('Dose-response template requires at least one compound.') } if (!data.unit) { throw new Error('Dose-response template requires a unit.') } if (data.replicates < 1) { throw new Error('Dose-response replicates must be at least 1.') } for (const compound of data.compounds) { if (compound.concentrations.length === 0) { throw new Error(`Compound '${compound.name}' requires at least one concentration.`) } if (compound.concentrations.some(value => value < 0)) { throw new Error(`Compound '${compound.name}' has a negative concentration.`) } } } /** @deprecated Scheduled for removal from the core SDK in MINT 1.4; the biology templates move to a dedicated package or the owning plugins. */ export function validateCalibrationCurveData(data: CalibrationCurveTemplateData): void { if (!data.analyte) { throw new Error('Calibration-curve template requires an analyte.') } if (!data.xUnit) { throw new Error('Calibration-curve template requires xUnit.') } if (!data.responseUnit) { throw new Error('Calibration-curve template requires responseUnit.') } if (!Array.isArray(data.points) || data.points.length === 0) { throw new Error('Calibration-curve template requires at least one point.') } if (data.acceptance.minRSquared < 0 || data.acceptance.minRSquared > 1) { throw new Error('Calibration-curve minRSquared must be between 0 and 1.') } if (data.acceptance.standardTolerancePercent < 0 || data.acceptance.qcTolerancePercent < 0) { throw new Error('Calibration-curve tolerances cannot be negative.') } const pointIds = new Set(data.points.map(point => point.id)) if (pointIds.size !== data.points.length) { throw new Error('Calibration-curve point ids must be unique.') } const orders = new Set(data.points.map(point => point.order)) if (orders.size !== data.points.length) { throw new Error('Calibration-curve point orders must be unique.') } let standards = 0 for (const point of data.points) { if (!point.id || !point.level) { throw new Error('Calibration-curve points require id and level.') } if (!point.unit) { throw new Error(`Calibration-curve point '${point.id}' requires unit.`) } if (point.order < 0) { throw new Error(`Calibration-curve point '${point.id}' has a negative order.`) } if (point.concentration < 0) { throw new Error(`Calibration-curve point '${point.id}' has a negative concentration.`) } if ((point.role === 'standard' || point.role === 'qc') && point.concentration <= 0) { throw new Error(`Calibration-curve point '${point.id}' requires positive concentration.`) } if (point.replicate < 1) { throw new Error(`Calibration-curve point '${point.id}' replicate must be at least 1.`) } if (point.role === 'standard' && point.include) { standards += 1 } } if (standards < 2) { throw new Error('Calibration-curve template requires at least two included standards.') } if (data.acceptance.requireBlank && !data.points.some(point => point.role === 'blank')) { throw new Error('Calibration-curve template requires a blank point when requireBlank is true.') } } /** @deprecated Scheduled for removal from the core SDK in MINT 1.4; the biology templates move to a dedicated package or the owning plugins. */ export function validateTimeCourseData(data: TimeCourseTemplateData): void { if (!Array.isArray(data.timepoints) || data.timepoints.length === 0) { throw new Error('Time-course template requires at least one time point.') } if (!Array.isArray(data.conditions) || data.conditions.length === 0) { throw new Error('Time-course template requires at least one condition.') } const timepointIds = new Set(data.timepoints.map(timepoint => timepoint.id)) if (timepointIds.size !== data.timepoints.length) { throw new Error('Time-course time point ids must be unique.') } const conditionIds = new Set(data.conditions.map(condition => condition.id)) if (conditionIds.size !== data.conditions.length) { throw new Error('Time-course condition ids must be unique.') } const sampleIds = new Set(data.samples.map(sample => sample.sampleId)) if (sampleIds.size !== data.samples.length) { throw new Error('Time-course sample ids must be unique.') } for (const sample of data.samples) { if (!timepointIds.has(sample.timepointId)) { throw new Error(`Sample '${sample.sampleId}' references unknown time point.`) } if (!conditionIds.has(sample.conditionId)) { throw new Error(`Sample '${sample.sampleId}' references unknown condition.`) } } } /** @deprecated Scheduled for removal from the core SDK in MINT 1.4; the biology templates move to a dedicated package or the owning plugins. */ export function validateProtocolStepsData(data: ProtocolStepsTemplateData): void { if (!Array.isArray(data.steps) || data.steps.length === 0) { throw new Error('Protocol-steps template requires at least one step.') } const stepIds = new Set(data.steps.map(step => step.id)) if (stepIds.size !== data.steps.length) { throw new Error('Protocol-steps ids must be unique.') } const orders = new Set(data.steps.map(step => step.order)) if (orders.size !== data.steps.length) { throw new Error('Protocol-steps orders must be unique.') } for (const step of data.steps) { if (!step.id) { throw new Error('Protocol step id is required.') } if (!step.name) { throw new Error(`Protocol step '${step.id}' requires a name.`) } if (step.duration !== undefined && step.duration < 0) { throw new Error(`Protocol step '${step.id}' has a negative duration.`) } if (step.order < 0) { throw new Error(`Protocol step '${step.id}' has a negative order.`) } } } /** @deprecated Scheduled for removal from the core SDK in MINT 1.4; the biology templates move to a dedicated package or the owning plugins. */ export function validateAssayMatrixData(data: AssayMatrixTemplateData): void { if (!Array.isArray(data.samples) || data.samples.length === 0) { throw new Error('Assay-matrix template requires at least one sample.') } if (!Array.isArray(data.features) || data.features.length === 0) { throw new Error('Assay-matrix template requires at least one feature.') } const sampleIds = new Set(data.samples.map(sample => sample.sampleId)) if (sampleIds.size !== data.samples.length) { throw new Error('Assay-matrix sample ids must be unique.') } const featureIds = new Set(data.features.map(feature => feature.id)) if (featureIds.size !== data.features.length) { throw new Error('Assay-matrix feature ids must be unique.') } const measurementIds = new Set() for (const measurement of data.measurements) { const key = `${measurement.sampleId}:${measurement.featureId}` if (measurementIds.has(key)) { throw new Error(`Duplicate assay measurement for '${key}'.`) } measurementIds.add(key) if (!sampleIds.has(measurement.sampleId)) { throw new Error(`Measurement references unknown sample '${measurement.sampleId}'.`) } if (!featureIds.has(measurement.featureId)) { throw new Error(`Measurement references unknown feature '${measurement.featureId}'.`) } } } /** @deprecated Scheduled for removal from the core SDK in MINT 1.4; the biology templates move to a dedicated package or the owning plugins. */ export function validateReagentListData(data: ReagentListTemplateData): void { if (!Array.isArray(data.reagents) || data.reagents.length === 0) { throw new Error('Reagent-list template requires at least one reagent.') } const reagentIds = new Set(data.reagents.map(reagent => reagent.id)) if (reagentIds.size !== data.reagents.length) { throw new Error('Reagent-list reagent ids must be unique.') } for (const reagent of data.reagents) { if (!reagent.id) { throw new Error('Reagent-list reagent id is required.') } if (!reagent.name) { throw new Error(`Reagent '${reagent.id}' requires a name.`) } if (reagent.stockLevel !== undefined && reagent.stockLevel < 0) { throw new Error(`Reagent '${reagent.id}' has a negative stock level.`) } } } /** @deprecated Scheduled for removal from the core SDK in MINT 1.4; the biology templates move to a dedicated package or the owning plugins. */ export function validateFlowCytometryPanelData(data: FlowCytometryPanelTemplateData): void { if (!Array.isArray(data.markers) || data.markers.length === 0) { throw new Error('Flow-cytometry panel template requires at least one marker.') } const markerIds = new Set(data.markers.map(marker => marker.id)) if (markerIds.size !== data.markers.length) { throw new Error('Flow-cytometry panel marker ids must be unique.') } const controlIds = new Set(data.controls.map(control => control.id)) if (controlIds.size !== data.controls.length) { throw new Error('Flow-cytometry panel control ids must be unique.') } for (const marker of data.markers) { if (!marker.id) { throw new Error('Flow-cytometry panel marker id is required.') } if (!marker.marker) { throw new Error(`Flow-cytometry panel marker '${marker.id}' requires a marker name.`) } } for (const control of data.controls) { if (!control.id) { throw new Error('Flow-cytometry panel control id is required.') } if (!control.name) { throw new Error(`Flow-cytometry panel control '${control.id}' requires a name.`) } if (control.markerId && !markerIds.has(control.markerId)) { throw new Error(`Flow-cytometry panel control '${control.id}' references unknown marker '${control.markerId}'.`) } } } /** @deprecated Scheduled for removal from the core SDK in MINT 1.4; the biology templates move to a dedicated package or the owning plugins. */ export function validateInstrumentRunData(data: InstrumentRunTemplateData): void { if (!Array.isArray(data.methods) || data.methods.length === 0) { throw new Error('Instrument-run template requires at least one method.') } if (!Array.isArray(data.items) || data.items.length === 0) { throw new Error('Instrument-run template requires at least one run item.') } const methodIds = new Set(data.methods.map(method => method.id)) if (methodIds.size !== data.methods.length) { throw new Error('Instrument-run method ids must be unique.') } for (const method of data.methods) { if (!method.id || !method.name) { throw new Error('Instrument-run methods require id and name.') } } const itemIds = new Set(data.items.map(item => item.id)) if (itemIds.size !== data.items.length) { throw new Error('Instrument-run item ids must be unique.') } const orders = new Set(data.items.map(item => item.order)) if (orders.size !== data.items.length) { throw new Error('Instrument-run item orders must be unique.') } for (const item of data.items) { if (!item.id) { throw new Error('Instrument-run items require id.') } if (!methodIds.has(item.methodId)) { throw new Error(`Instrument-run item '${item.id}' references unknown method '${item.methodId}'.`) } if (item.kind === 'sample' && !item.sampleId) { throw new Error(`Instrument-run sample item '${item.id}' requires sampleId.`) } if (item.order < 0) { throw new Error(`Instrument-run item '${item.id}' has a negative order.`) } if (item.injectionVolume !== undefined && item.injectionVolume < 0) { throw new Error(`Instrument-run item '${item.id}' has a negative injection volume.`) } if (item.expectedDurationMin !== undefined && item.expectedDurationMin < 0) { throw new Error(`Instrument-run item '${item.id}' has a negative expected duration.`) } } } /** @deprecated Scheduled for removal from the core SDK in MINT 1.4; the biology templates move to a dedicated package or the owning plugins. */ export function validateQpcrPlateData(data: QpcrPlateTemplateData): void { if (!Array.isArray(data.samples) || data.samples.length === 0) { throw new Error('qPCR plate template requires at least one sample.') } if (!Array.isArray(data.targets) || data.targets.length === 0) { throw new Error('qPCR plate template requires at least one target.') } if (!Array.isArray(data.reactions) || data.reactions.length === 0) { throw new Error('qPCR plate template requires at least one reaction.') } const sampleIds = new Set(data.samples.map(sample => sample.sampleId)) if (sampleIds.size !== data.samples.length) { throw new Error('qPCR plate sample ids must be unique.') } const targetIds = new Set(data.targets.map(target => target.id)) if (targetIds.size !== data.targets.length) { throw new Error('qPCR plate target ids must be unique.') } const reactionIds = new Set(data.reactions.map(reaction => reaction.id)) if (reactionIds.size !== data.reactions.length) { throw new Error('qPCR plate reaction ids must be unique.') } const wellIds = new Set(data.reactions.map(reaction => reaction.wellId)) if (wellIds.size !== data.reactions.length) { throw new Error('qPCR plate reaction wells must be unique.') } for (const reaction of data.reactions) { assertWellInFormat(reaction.wellId, data.format) if (!reaction.id) { throw new Error('qPCR reaction id is required.') } if (!reaction.targetId || !targetIds.has(reaction.targetId)) { throw new Error(`qPCR reaction '${reaction.id}' references unknown target '${reaction.targetId}'.`) } if (reaction.sampleId && !sampleIds.has(reaction.sampleId)) { throw new Error(`qPCR reaction '${reaction.id}' references unknown sample '${reaction.sampleId}'.`) } if (reaction.controlKind === 'sample' && !reaction.sampleId) { throw new Error(`qPCR sample reaction '${reaction.id}' requires sampleId.`) } if (reaction.replicate < 1) { throw new Error(`qPCR reaction '${reaction.id}' replicate must be at least 1.`) } if (reaction.cq !== undefined && reaction.cq !== null && reaction.cq < 0) { throw new Error(`qPCR reaction '${reaction.id}' has a negative Cq.`) } if (reaction.quantity !== undefined && reaction.quantity !== null && reaction.quantity < 0) { throw new Error(`qPCR reaction '${reaction.id}' has a negative quantity.`) } } }