@morscherlab/mint-sdk 1.0.0-rc.2 → 1.0.0-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (443) hide show
  1. package/dist/__tests__/components/AppTopBar.navigation.test.d.ts +1 -0
  2. package/dist/__tests__/components/DoseCalculatorVolumeField.test.d.ts +1 -0
  3. package/dist/__tests__/components/PlateMapEditorToolbarInternal.test.d.ts +1 -0
  4. package/dist/__tests__/components/PluginWorkspaceView.controls.test.d.ts +1 -0
  5. package/dist/__tests__/components/PluginWorkspaceView.navigation.test.d.ts +1 -0
  6. package/dist/__tests__/components/PluginWorkspaceView.shell.test.d.ts +1 -0
  7. package/dist/__tests__/components/ProtocolStep.presentation.test.d.ts +1 -0
  8. package/dist/__tests__/components/ProtocolStepEditor.state.test.d.ts +1 -0
  9. package/dist/__tests__/components/ProtocolStepParameterField.test.d.ts +1 -0
  10. package/dist/__tests__/components/ReagentList.presentation.test.d.ts +1 -0
  11. package/dist/__tests__/components/SampleSelector.colors.test.d.ts +1 -0
  12. package/dist/__tests__/components/SampleSelector.drag.test.d.ts +1 -0
  13. package/dist/__tests__/components/SampleSelector.groups.test.d.ts +1 -0
  14. package/dist/__tests__/components/SampleSelector.selection.test.d.ts +1 -0
  15. package/dist/__tests__/components/SampleSelectorSampleRow.test.d.ts +1 -0
  16. package/dist/__tests__/components/ScheduleCalendar.test.d.ts +1 -0
  17. package/dist/__tests__/components/SettingsModal.schema.test.d.ts +1 -0
  18. package/dist/__tests__/components/WellPlate.colors.test.d.ts +1 -0
  19. package/dist/__tests__/components/WellPlate.conditions.test.d.ts +1 -0
  20. package/dist/__tests__/components/WellPlate.geometry.test.d.ts +1 -0
  21. package/dist/__tests__/components/WellPlate.interaction.test.d.ts +1 -0
  22. package/dist/__tests__/components/WellPlate.legend.test.d.ts +1 -0
  23. package/dist/__tests__/components/WellPlate.rendering.test.d.ts +1 -0
  24. package/dist/__tests__/components/WellPlate.sampleDrop.test.d.ts +1 -0
  25. package/dist/__tests__/composables/autoGroup/classify.test.d.ts +1 -0
  26. package/dist/__tests__/composables/autoGroup/columns.test.d.ts +1 -0
  27. package/dist/__tests__/composables/autoGroup/compose.test.d.ts +1 -0
  28. package/dist/__tests__/composables/autoGroup/cooccurrence.test.d.ts +1 -0
  29. package/dist/__tests__/composables/autoGroup/fingerprint.test.d.ts +1 -0
  30. package/dist/__tests__/composables/autoGroup/integration.test.d.ts +1 -0
  31. package/dist/__tests__/composables/autoGroup/template.test.d.ts +1 -0
  32. package/dist/__tests__/composables/autoGroup/tokenize.test.d.ts +1 -0
  33. package/dist/__tests__/composables/useAutoGroupInputSources.test.d.ts +1 -0
  34. package/dist/__tests__/composables/useScheduleCalendarLayout.test.d.ts +1 -0
  35. package/dist/__tests__/docs/extractDocsComponents.test.d.ts +1 -0
  36. package/dist/__tests__/docs/extractDocsExports.test.d.ts +1 -0
  37. package/dist/__tests__/docs/extractDocsParsing.test.d.ts +1 -0
  38. package/dist/__tests__/docs/extractDocsTemplates.test.d.ts +1 -0
  39. package/dist/__tests__/docs/extractDocsTheme.test.d.ts +1 -0
  40. package/dist/components/AppTopBar.navigation.d.ts +11 -0
  41. package/dist/components/BaseButton.vue.d.ts +1 -1
  42. package/dist/components/BaseCheckbox.vue.d.ts +1 -1
  43. package/dist/components/BaseInput.vue.d.ts +2 -2
  44. package/dist/components/BasePill.vue.d.ts +1 -1
  45. package/dist/components/BaseRadioGroup.vue.d.ts +1 -1
  46. package/dist/components/BaseSelect.vue.d.ts +1 -1
  47. package/dist/components/BaseSlider.vue.d.ts +2 -2
  48. package/dist/components/BaseTextarea.vue.d.ts +2 -2
  49. package/dist/components/BaseToggle.vue.d.ts +1 -1
  50. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
  51. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +1 -1
  52. package/dist/components/ColorSlider.vue.d.ts +2 -2
  53. package/dist/components/ConcentrationInput.vue.d.ts +2 -2
  54. package/dist/components/DatePicker.vue.d.ts +1 -1
  55. package/dist/components/DateTimePicker.vue.d.ts +2 -2
  56. package/dist/components/DoseCalculatorVolumeField.vue.d.ts +15 -0
  57. package/dist/components/DropdownButton.vue.d.ts +1 -1
  58. package/dist/components/FileUploader.vue.d.ts +2 -2
  59. package/dist/components/FormulaInput.vue.d.ts +2 -2
  60. package/dist/components/IconButton.vue.d.ts +1 -1
  61. package/dist/components/LoadingSpinner.vue.d.ts +1 -1
  62. package/dist/components/MoleculeInput.vue.d.ts +2 -2
  63. package/dist/components/MultiSelect.vue.d.ts +1 -1
  64. package/dist/components/NumberInput.vue.d.ts +1 -1
  65. package/dist/components/PlateMapEditor.vue.d.ts +6 -6
  66. package/dist/components/PluginWorkspaceView.controls.d.ts +28 -0
  67. package/dist/components/PluginWorkspaceView.navigation.d.ts +29 -0
  68. package/dist/components/PluginWorkspaceView.props.d.ts +151 -0
  69. package/dist/components/PluginWorkspaceView.shell.d.ts +19 -0
  70. package/dist/components/PluginWorkspaceView.vue.d.ts +46 -195
  71. package/dist/components/ProgressBar.vue.d.ts +1 -1
  72. package/dist/components/ProtocolStep.presentation.d.ts +4 -0
  73. package/dist/components/ProtocolStepEditor.state.d.ts +18 -0
  74. package/dist/components/ProtocolStepParameterField.vue.d.ts +12 -0
  75. package/dist/components/ReagentList.presentation.d.ts +16 -0
  76. package/dist/components/ResourceCard.vue.d.ts +1 -1
  77. package/dist/components/SampleSelector.colors.d.ts +13 -0
  78. package/dist/components/SampleSelector.drag.d.ts +24 -0
  79. package/dist/components/SampleSelector.groups.d.ts +15 -0
  80. package/dist/components/SampleSelector.selection.d.ts +26 -0
  81. package/dist/components/SampleSelector.vue.d.ts +4 -1
  82. package/dist/components/SampleSelectorSampleRow.vue.d.ts +21 -0
  83. package/dist/components/SegmentedControl.vue.d.ts +1 -1
  84. package/dist/components/SequenceInput.vue.d.ts +2 -2
  85. package/dist/components/SequenceProgressBar.vue.d.ts +1 -1
  86. package/dist/components/SettingsModal.schema.d.ts +9 -0
  87. package/dist/components/StatusIndicator.vue.d.ts +1 -1
  88. package/dist/components/TagsInput.vue.d.ts +2 -2
  89. package/dist/components/TimePicker.vue.d.ts +2 -2
  90. package/dist/components/TimeRangeInput.vue.d.ts +1 -1
  91. package/dist/components/UnitInput.vue.d.ts +2 -2
  92. package/dist/components/WellPlate.colors.d.ts +9 -0
  93. package/dist/components/WellPlate.conditions.d.ts +26 -0
  94. package/dist/components/WellPlate.geometry.d.ts +23 -0
  95. package/dist/components/WellPlate.interaction.d.ts +71 -0
  96. package/dist/components/WellPlate.legend.d.ts +2 -0
  97. package/dist/components/WellPlate.rendering.d.ts +24 -0
  98. package/dist/components/WellPlate.sampleDrop.d.ts +8 -0
  99. package/dist/components/WellPlate.vue.d.ts +1 -1
  100. package/dist/components/index.js +2 -2
  101. package/dist/components/internal/ActionItemInternal.vue.d.ts +1 -1
  102. package/dist/components/internal/PlateMapEditorToolbarInternal.vue.d.ts +28 -0
  103. package/dist/{components-BhK-dW99.js → components-DtHA2bgp.js} +3754 -2991
  104. package/dist/components-DtHA2bgp.js.map +1 -0
  105. package/dist/composables/autoGroup/classKey.d.ts +4 -0
  106. package/dist/composables/autoGroup/classify.d.ts +28 -0
  107. package/dist/composables/autoGroup/colors.d.ts +2 -0
  108. package/dist/composables/autoGroup/columns.d.ts +10 -0
  109. package/dist/composables/autoGroup/compose.d.ts +8 -0
  110. package/dist/composables/autoGroup/cooccurrence.d.ts +2 -0
  111. package/dist/composables/autoGroup/csv-shim.d.ts +2 -0
  112. package/dist/composables/autoGroup/fingerprint.d.ts +3 -0
  113. package/dist/composables/autoGroup/index.d.ts +16 -0
  114. package/dist/composables/autoGroup/replicatePreGroup.d.ts +38 -0
  115. package/dist/composables/autoGroup/template.d.ts +15 -0
  116. package/dist/composables/autoGroup/tokenize.d.ts +8 -0
  117. package/dist/composables/autoGroupConstants.d.ts +1 -0
  118. package/dist/composables/autoGroupGrouping.d.ts +3 -0
  119. package/dist/composables/controlComponentBindings.d.ts +7 -0
  120. package/dist/composables/controlSchemaAdapters.d.ts +20 -0
  121. package/dist/composables/controlSchemaDoseDesign.d.ts +11 -0
  122. package/dist/composables/controlSchemaFormFields.d.ts +3 -0
  123. package/dist/composables/controlSchemaLayout.d.ts +7 -0
  124. package/dist/composables/controlSchemaModel.d.ts +5 -0
  125. package/dist/composables/controlSchemaNormalize.d.ts +15 -0
  126. package/dist/composables/controlSchemaTypes.d.ts +305 -0
  127. package/dist/composables/controlSchemaUtils.d.ts +9 -0
  128. package/dist/composables/controlWorkspaceOptions.d.ts +2 -0
  129. package/dist/composables/formBuilderSchema.d.ts +18 -0
  130. package/dist/composables/index.js +3 -3
  131. package/dist/composables/pluginEndpointBuilder.d.ts +13 -0
  132. package/dist/composables/protocolTemplateCatalog.d.ts +26 -0
  133. package/dist/composables/useAutoGroup.d.ts +61 -74
  134. package/dist/composables/useAutoGroupInputSources.d.ts +32 -0
  135. package/dist/composables/useBioTemplateControls.d.ts +1 -1
  136. package/dist/composables/useBioTemplatePresetWorkspace.d.ts +1 -1
  137. package/dist/composables/useBioTemplateWorkspace.d.ts +1 -1
  138. package/dist/composables/useControlSchema.d.ts +8 -346
  139. package/dist/composables/useControlWorkspace.d.ts +5 -0
  140. package/dist/composables/useForm.d.ts +2 -33
  141. package/dist/composables/useFormBuilder.d.ts +2 -9
  142. package/dist/composables/useFormValidation.d.ts +34 -0
  143. package/dist/composables/usePluginClient.d.ts +1 -4
  144. package/dist/composables/useProtocolTemplates.d.ts +2 -24
  145. package/dist/composables/useScheduleCalendarLayout.d.ts +49 -0
  146. package/dist/{composables-Bg7CFuNz.js → composables-Dlg8jenH.js} +33 -31
  147. package/dist/composables-Dlg8jenH.js.map +1 -0
  148. package/dist/index.js +4 -4
  149. package/dist/install.js +2 -2
  150. package/dist/styles.css +547 -516
  151. package/dist/templates/adapters.d.ts +14 -47
  152. package/dist/templates/assayLookups.d.ts +3 -0
  153. package/dist/templates/assayMatrixAdapters.d.ts +6 -0
  154. package/dist/templates/assayMatrixBuilder.d.ts +2 -0
  155. package/dist/templates/assayNormalizers.d.ts +4 -0
  156. package/dist/templates/builderDefaults.d.ts +1 -0
  157. package/dist/templates/builderIdUtils.d.ts +4 -0
  158. package/dist/templates/builderPresetControls.d.ts +10 -0
  159. package/dist/templates/builderReadUtils.d.ts +8 -0
  160. package/dist/templates/builders.d.ts +26 -67
  161. package/dist/templates/calibrationCurveAdapters.d.ts +4 -0
  162. package/dist/templates/calibrationCurveBuilder.d.ts +2 -0
  163. package/dist/templates/calibrationNormalizers.d.ts +5 -0
  164. package/dist/templates/componentBindingCatalog.d.ts +25 -0
  165. package/dist/templates/componentBindingHelpers.d.ts +17 -0
  166. package/dist/templates/componentDoseResponseProps.d.ts +3 -0
  167. package/dist/templates/componentGenericProps.d.ts +8 -0
  168. package/dist/templates/componentPlateHelpers.d.ts +5 -0
  169. package/dist/templates/componentPlateMapProps.d.ts +3 -0
  170. package/dist/templates/componentPropsFactory.d.ts +3 -0
  171. package/dist/templates/componentQpcrPlateProps.d.ts +3 -0
  172. package/dist/templates/componentTargetResolvers.d.ts +5 -0
  173. package/dist/templates/componentTemplateProps.d.ts +3 -0
  174. package/dist/templates/controlSchemaClone.d.ts +4 -0
  175. package/dist/templates/controlSchemaConstants.d.ts +10 -0
  176. package/dist/templates/controlSchemaMerge.d.ts +3 -0
  177. package/dist/templates/controlSchemaTargets.d.ts +17 -0
  178. package/dist/templates/controlSchemaTypes.d.ts +4 -0
  179. package/dist/templates/controlSchemas.d.ts +4 -4
  180. package/dist/templates/defaultBioTemplateBuilder.d.ts +2 -0
  181. package/dist/templates/doseResponseAdapters.d.ts +4 -0
  182. package/dist/templates/doseResponseBuilder.d.ts +2 -0
  183. package/dist/templates/elisaAssayCollectionBuilder.d.ts +2 -0
  184. package/dist/templates/flowCytometryAssayCollectionBuilder.d.ts +2 -0
  185. package/dist/templates/flowCytometryPanelBuilder.d.ts +2 -0
  186. package/dist/templates/flowNormalizers.d.ts +8 -0
  187. package/dist/templates/flowPanelAdapters.d.ts +4 -0
  188. package/dist/templates/index.js +1 -1
  189. package/dist/templates/instrumentRunAdapterHelpers.d.ts +8 -0
  190. package/dist/templates/instrumentRunAdapters.d.ts +8 -0
  191. package/dist/templates/instrumentRunBuilder.d.ts +2 -0
  192. package/dist/templates/lcmsBatchCollectionBuilder.d.ts +2 -0
  193. package/dist/templates/plateGeometry.d.ts +4 -0
  194. package/dist/templates/plateMapAdapters.d.ts +3 -0
  195. package/dist/templates/plateMapBuilder.d.ts +2 -0
  196. package/dist/templates/presetControlSchemas.d.ts +534 -0
  197. package/dist/templates/protocolAdapters.d.ts +5 -0
  198. package/dist/templates/protocolNormalizers.d.ts +6 -0
  199. package/dist/templates/protocolStepsBuilder.d.ts +2 -0
  200. package/dist/templates/qpcrAdapters.d.ts +5 -0
  201. package/dist/templates/qpcrExpressionCollectionBuilder.d.ts +2 -0
  202. package/dist/templates/qpcrPlateBuilder.d.ts +2 -0
  203. package/dist/templates/reagentAdapters.d.ts +5 -0
  204. package/dist/templates/reagentListBuilder.d.ts +2 -0
  205. package/dist/templates/runNormalizers.d.ts +10 -0
  206. package/dist/templates/sampleNormalizers.d.ts +4 -0
  207. package/dist/templates/samplePrepAdapters.d.ts +4 -0
  208. package/dist/templates/samplePrepBuilder.d.ts +2 -0
  209. package/dist/templates/sampleSheetAdapters.d.ts +5 -0
  210. package/dist/templates/sampleSheetBuilder.d.ts +2 -0
  211. package/dist/templates/targetedMetabolomicsCollectionBuilder.d.ts +2 -0
  212. package/dist/templates/targetedMetabolomicsHelpers.d.ts +5 -0
  213. package/dist/templates/templateAdapterTypes.d.ts +48 -0
  214. package/dist/templates/templateControlSchemas.d.ts +400 -0
  215. package/dist/templates/templateCreateOptions.d.ts +165 -0
  216. package/dist/templates/templateEnvelopes.d.ts +9 -0
  217. package/dist/templates/templatePackCollectionBuilder.d.ts +2 -0
  218. package/dist/templates/templatePresetCollectionBuilder.d.ts +18 -0
  219. package/dist/templates/templateQpcrTypes.d.ts +42 -0
  220. package/dist/templates/templateValidators.d.ts +13 -0
  221. package/dist/templates/timeCourseAdapters.d.ts +5 -0
  222. package/dist/templates/timeCourseBuilder.d.ts +2 -0
  223. package/dist/templates/types.d.ts +5 -250
  224. package/dist/templates/wellPlateScreenCollectionBuilder.d.ts +2 -0
  225. package/dist/templates/westernBlotAssayCollectionBuilder.d.ts +2 -0
  226. package/dist/{templates-BorLR_7p.js → templates-DtdUvJ4c.js} +3565 -3411
  227. package/dist/templates-DtdUvJ4c.js.map +1 -0
  228. package/dist/types/auto-group.d.ts +79 -9
  229. package/dist/types/componentLabTypes.d.ts +161 -0
  230. package/dist/types/componentWorkflowTypes.d.ts +150 -0
  231. package/dist/types/components.d.ts +2 -311
  232. package/dist/{useProtocolTemplates-n6AJqSqv.js → useProtocolTemplates-Bm5vyH4_.js} +1220 -454
  233. package/dist/useProtocolTemplates-Bm5vyH4_.js.map +1 -0
  234. package/package.json +1 -1
  235. package/src/__tests__/components/AppTopBar.navigation.test.ts +70 -0
  236. package/src/__tests__/components/DoseCalculatorVolumeField.test.ts +53 -0
  237. package/src/__tests__/components/PlateMapEditorToolbarInternal.test.ts +54 -0
  238. package/src/__tests__/components/PluginWorkspaceView.controls.test.ts +156 -0
  239. package/src/__tests__/components/PluginWorkspaceView.navigation.test.ts +102 -0
  240. package/src/__tests__/components/PluginWorkspaceView.shell.test.ts +41 -0
  241. package/src/__tests__/components/ProtocolStep.presentation.test.ts +31 -0
  242. package/src/__tests__/components/ProtocolStepEditor.state.test.ts +165 -0
  243. package/src/__tests__/components/ProtocolStepParameterField.test.ts +44 -0
  244. package/src/__tests__/components/ReagentList.presentation.test.ts +68 -0
  245. package/src/__tests__/components/SampleSelector.colors.test.ts +49 -0
  246. package/src/__tests__/components/SampleSelector.drag.test.ts +100 -0
  247. package/src/__tests__/components/SampleSelector.groups.test.ts +81 -0
  248. package/src/__tests__/components/SampleSelector.selection.test.ts +70 -0
  249. package/src/__tests__/components/SampleSelector.test.ts +32 -0
  250. package/src/__tests__/components/SampleSelectorSampleRow.test.ts +37 -0
  251. package/src/__tests__/components/ScheduleCalendar.test.ts +44 -0
  252. package/src/__tests__/components/SettingsModal.schema.test.ts +97 -0
  253. package/src/__tests__/components/WellPlate.colors.test.ts +28 -0
  254. package/src/__tests__/components/WellPlate.conditions.test.ts +68 -0
  255. package/src/__tests__/components/WellPlate.geometry.test.ts +54 -0
  256. package/src/__tests__/components/WellPlate.interaction.test.ts +171 -0
  257. package/src/__tests__/components/WellPlate.legend.test.ts +13 -0
  258. package/src/__tests__/components/WellPlate.rendering.test.ts +122 -0
  259. package/src/__tests__/components/WellPlate.sampleDrop.test.ts +70 -0
  260. package/src/__tests__/composables/autoGroup/classify.test.ts +107 -0
  261. package/src/__tests__/composables/autoGroup/columns.test.ts +135 -0
  262. package/src/__tests__/composables/autoGroup/compose.test.ts +227 -0
  263. package/src/__tests__/composables/autoGroup/cooccurrence.test.ts +91 -0
  264. package/src/__tests__/composables/autoGroup/fingerprint.test.ts +50 -0
  265. package/src/__tests__/composables/autoGroup/integration.test.ts +79 -0
  266. package/src/__tests__/composables/autoGroup/template.test.ts +70 -0
  267. package/src/__tests__/composables/autoGroup/tokenize.test.ts +33 -0
  268. package/src/__tests__/composables/useAutoGroup.test.ts +129 -625
  269. package/src/__tests__/composables/useAutoGroupInputSources.test.ts +107 -0
  270. package/src/__tests__/composables/useControlSchema.test.ts +23 -0
  271. package/src/__tests__/composables/useScheduleCalendarLayout.test.ts +89 -0
  272. package/src/__tests__/docs/extractDocsComponents.test.ts +142 -0
  273. package/src/__tests__/docs/extractDocsExports.test.ts +77 -0
  274. package/src/__tests__/docs/extractDocsParsing.test.ts +69 -0
  275. package/src/__tests__/docs/extractDocsTemplates.test.ts +54 -0
  276. package/src/__tests__/docs/extractDocsTheme.test.ts +89 -0
  277. package/src/__tests__/docs/frontendDocsCatalog.test.ts +1 -1
  278. package/src/__tests__/fixtures/auto-group/mixed-lc-ms-batch.txt +187 -0
  279. package/src/components/AppSidebar.vue +2 -6
  280. package/src/components/AppTopBar.navigation.ts +62 -0
  281. package/src/components/AppTopBar.vue +17 -44
  282. package/src/components/AutoGroupModal.story.vue +50 -0
  283. package/src/components/AutoGroupModal.vue +441 -158
  284. package/src/components/ControlWorkspaceView.vue +2 -6
  285. package/src/components/DoseCalculator.vue +13 -73
  286. package/src/components/DoseCalculatorVolumeField.vue +61 -0
  287. package/src/components/ExperimentTimeline.vue +6 -31
  288. package/src/components/FormBuilder.vue +2 -7
  289. package/src/components/PlateMapEditor.vue +32 -106
  290. package/src/components/PluginWorkspaceView.controls.ts +182 -0
  291. package/src/components/PluginWorkspaceView.navigation.ts +106 -0
  292. package/src/components/PluginWorkspaceView.props.ts +174 -0
  293. package/src/components/PluginWorkspaceView.shell.ts +66 -0
  294. package/src/components/PluginWorkspaceView.vue +85 -404
  295. package/src/components/ProtocolStep.presentation.ts +31 -0
  296. package/src/components/ProtocolStepEditor.state.ts +104 -0
  297. package/src/components/ProtocolStepEditor.vue +48 -179
  298. package/src/components/ProtocolStepParameterField.vue +134 -0
  299. package/src/components/ReagentList.presentation.ts +105 -0
  300. package/src/components/ReagentList.vue +16 -79
  301. package/src/components/SampleSelector.colors.ts +43 -0
  302. package/src/components/SampleSelector.drag.ts +164 -0
  303. package/src/components/SampleSelector.groups.ts +109 -0
  304. package/src/components/SampleSelector.selection.ts +103 -0
  305. package/src/components/SampleSelector.vue +82 -349
  306. package/src/components/SampleSelectorSampleRow.vue +64 -0
  307. package/src/components/ScheduleCalendar.vue +44 -199
  308. package/src/components/SettingsModal.schema.ts +71 -0
  309. package/src/components/SettingsModal.vue +16 -46
  310. package/src/components/WellPlate.colors.ts +56 -0
  311. package/src/components/WellPlate.conditions.ts +100 -0
  312. package/src/components/WellPlate.geometry.ts +91 -0
  313. package/src/components/WellPlate.interaction.ts +272 -0
  314. package/src/components/WellPlate.legend.ts +8 -0
  315. package/src/components/WellPlate.rendering.ts +105 -0
  316. package/src/components/WellPlate.sampleDrop.ts +73 -0
  317. package/src/components/WellPlate.vue +102 -550
  318. package/src/components/internal/PlateMapEditorToolbarInternal.vue +128 -0
  319. package/src/composables/autoGroup/classKey.ts +5 -0
  320. package/src/composables/autoGroup/classify.ts +205 -0
  321. package/src/composables/autoGroup/colors.ts +6 -0
  322. package/src/composables/autoGroup/columns.ts +226 -0
  323. package/src/composables/autoGroup/compose.ts +156 -0
  324. package/src/composables/autoGroup/cooccurrence.ts +46 -0
  325. package/src/composables/autoGroup/csv-shim.ts +44 -0
  326. package/src/composables/autoGroup/fingerprint.ts +49 -0
  327. package/src/composables/autoGroup/index.ts +20 -0
  328. package/src/composables/autoGroup/replicatePreGroup.ts +90 -0
  329. package/src/composables/autoGroup/template.ts +126 -0
  330. package/src/composables/autoGroup/tokenize.ts +41 -0
  331. package/src/composables/autoGroup/vocab.json +67 -0
  332. package/src/composables/autoGroupConstants.ts +4 -0
  333. package/src/composables/autoGroupGrouping.ts +148 -0
  334. package/src/composables/controlComponentBindings.ts +80 -0
  335. package/src/composables/controlSchemaAdapters.ts +196 -0
  336. package/src/composables/controlSchemaDoseDesign.ts +215 -0
  337. package/src/composables/controlSchemaFormFields.ts +61 -0
  338. package/src/composables/controlSchemaLayout.ts +59 -0
  339. package/src/composables/controlSchemaModel.ts +163 -0
  340. package/src/composables/controlSchemaNormalize.ts +101 -0
  341. package/src/composables/controlSchemaTypes.ts +364 -0
  342. package/src/composables/controlSchemaUtils.ts +36 -0
  343. package/src/composables/controlWorkspaceOptions.ts +21 -0
  344. package/src/composables/formBuilderSchema.ts +153 -0
  345. package/src/composables/pluginEndpointBuilder.ts +203 -0
  346. package/src/composables/protocolTemplateCatalog.ts +325 -0
  347. package/src/composables/useAutoGroup.ts +395 -549
  348. package/src/composables/useAutoGroupInputSources.ts +147 -0
  349. package/src/composables/useBioTemplateControls.ts +1 -1
  350. package/src/composables/useBioTemplatePresetWorkspace.ts +1 -1
  351. package/src/composables/useBioTemplateWorkspace.ts +1 -1
  352. package/src/composables/useControlSchema.ts +64 -1312
  353. package/src/composables/useControlWorkspace.ts +201 -0
  354. package/src/composables/useForm.ts +5 -187
  355. package/src/composables/useFormBuilder.ts +11 -153
  356. package/src/composables/useFormValidation.ts +154 -0
  357. package/src/composables/usePluginClient.ts +10 -193
  358. package/src/composables/useProtocolTemplates.ts +10 -328
  359. package/src/composables/useScheduleCalendarLayout.ts +287 -0
  360. package/src/styles/components/auto-group-modal.css +248 -310
  361. package/src/templates/adapters.ts +89 -930
  362. package/src/templates/assayLookups.ts +33 -0
  363. package/src/templates/assayMatrixAdapters.ts +78 -0
  364. package/src/templates/assayMatrixBuilder.ts +59 -0
  365. package/src/templates/assayNormalizers.ts +34 -0
  366. package/src/templates/builderDefaults.ts +11 -0
  367. package/src/templates/builderIdUtils.ts +20 -0
  368. package/src/templates/builderPresetControls.ts +165 -0
  369. package/src/templates/builderReadUtils.ts +57 -0
  370. package/src/templates/builders.ts +122 -2350
  371. package/src/templates/calibrationCurveAdapters.ts +59 -0
  372. package/src/templates/calibrationCurveBuilder.ts +99 -0
  373. package/src/templates/calibrationNormalizers.ts +60 -0
  374. package/src/templates/componentBindingCatalog.ts +90 -0
  375. package/src/templates/componentBindingHelpers.ts +93 -0
  376. package/src/templates/componentBindings.ts +12 -461
  377. package/src/templates/componentDoseResponseProps.ts +42 -0
  378. package/src/templates/componentGenericProps.ts +77 -0
  379. package/src/templates/componentPlateHelpers.ts +29 -0
  380. package/src/templates/componentPlateMapProps.ts +32 -0
  381. package/src/templates/componentPropsFactory.ts +21 -0
  382. package/src/templates/componentQpcrPlateProps.ts +28 -0
  383. package/src/templates/componentTargetResolvers.ts +69 -0
  384. package/src/templates/componentTemplateProps.ts +78 -0
  385. package/src/templates/controlSchemaClone.ts +32 -0
  386. package/src/templates/controlSchemaConstants.ts +11 -0
  387. package/src/templates/controlSchemaMerge.ts +40 -0
  388. package/src/templates/controlSchemaTargets.ts +87 -0
  389. package/src/templates/controlSchemaTypes.ts +20 -0
  390. package/src/templates/controlSchemas.ts +22 -704
  391. package/src/templates/defaultBioTemplateBuilder.ts +124 -0
  392. package/src/templates/doseResponseAdapters.ts +45 -0
  393. package/src/templates/doseResponseBuilder.ts +44 -0
  394. package/src/templates/elisaAssayCollectionBuilder.ts +62 -0
  395. package/src/templates/flowCytometryAssayCollectionBuilder.ts +41 -0
  396. package/src/templates/flowCytometryPanelBuilder.ts +53 -0
  397. package/src/templates/flowNormalizers.ts +58 -0
  398. package/src/templates/flowPanelAdapters.ts +58 -0
  399. package/src/templates/instrumentRunAdapterHelpers.ts +94 -0
  400. package/src/templates/instrumentRunAdapters.ts +163 -0
  401. package/src/templates/instrumentRunBuilder.ts +97 -0
  402. package/src/templates/lcmsBatchCollectionBuilder.ts +38 -0
  403. package/src/templates/plateGeometry.ts +62 -0
  404. package/src/templates/plateMapAdapters.ts +36 -0
  405. package/src/templates/plateMapBuilder.ts +43 -0
  406. package/src/templates/presetControlSchemas.ts +258 -0
  407. package/src/templates/protocolAdapters.ts +69 -0
  408. package/src/templates/protocolNormalizers.ts +37 -0
  409. package/src/templates/protocolStepsBuilder.ts +36 -0
  410. package/src/templates/qpcrAdapters.ts +104 -0
  411. package/src/templates/qpcrExpressionCollectionBuilder.ts +33 -0
  412. package/src/templates/qpcrPlateBuilder.ts +96 -0
  413. package/src/templates/reagentAdapters.ts +77 -0
  414. package/src/templates/reagentListBuilder.ts +30 -0
  415. package/src/templates/runNormalizers.ts +63 -0
  416. package/src/templates/sampleNormalizers.ts +58 -0
  417. package/src/templates/samplePrepAdapters.ts +63 -0
  418. package/src/templates/samplePrepBuilder.ts +51 -0
  419. package/src/templates/sampleSheetAdapters.ts +75 -0
  420. package/src/templates/sampleSheetBuilder.ts +23 -0
  421. package/src/templates/targetedMetabolomicsCollectionBuilder.ts +79 -0
  422. package/src/templates/targetedMetabolomicsHelpers.ts +102 -0
  423. package/src/templates/templateAdapterTypes.ts +58 -0
  424. package/src/templates/templateControlSchemas.ts +320 -0
  425. package/src/templates/templateCreateOptions.ts +208 -0
  426. package/src/templates/templateEnvelopes.ts +137 -0
  427. package/src/templates/templatePackCollectionBuilder.ts +23 -0
  428. package/src/templates/templatePresetCollectionBuilder.ts +139 -0
  429. package/src/templates/templateQpcrTypes.ts +48 -0
  430. package/src/templates/templateValidators.ts +414 -0
  431. package/src/templates/timeCourseAdapters.ts +73 -0
  432. package/src/templates/timeCourseBuilder.ts +64 -0
  433. package/src/templates/types.ts +79 -275
  434. package/src/templates/wellPlateScreenCollectionBuilder.ts +36 -0
  435. package/src/templates/westernBlotAssayCollectionBuilder.ts +68 -0
  436. package/src/types/auto-group.ts +107 -9
  437. package/src/types/componentLabTypes.ts +235 -0
  438. package/src/types/componentWorkflowTypes.ts +190 -0
  439. package/src/types/components.ts +74 -424
  440. package/dist/components-BhK-dW99.js.map +0 -1
  441. package/dist/composables-Bg7CFuNz.js.map +0 -1
  442. package/dist/templates-BorLR_7p.js.map +0 -1
  443. package/dist/useProtocolTemplates-n6AJqSqv.js.map +0 -1
@@ -1,229 +1,9 @@
1
- import { describe, it, expect } from 'vitest'
1
+ import { describe, it, expect, vi } from 'vitest'
2
2
  import {
3
- analyzeDelimiter,
4
- detectOutliers,
5
- classifyOutlierAction,
6
- extractColumns,
7
3
  parseCSV,
8
- parseCSVLine,
9
- computeGroups,
10
- computeGroupsFromCsv,
11
4
  extractSamplesFromDesignData,
12
- DEFAULT_COLORS,
13
5
  useAutoGroup,
14
6
  } from '../../composables/useAutoGroup'
15
- import type { OutlierAction } from '../../types/auto-group'
16
-
17
- describe('analyzeDelimiter', () => {
18
- it('should detect underscore delimiter', () => {
19
- const lines = ['Ctrl_WT_1', 'Ctrl_WT_2', 'Treat_KO_1', 'Treat_KO_2']
20
- const result = analyzeDelimiter(lines)
21
- expect(result.delimiter).toBe('_')
22
- expect(result.dominantFieldCount).toBe(3)
23
- expect(result.consistency).toBe(1)
24
- })
25
-
26
- it('should detect hyphen delimiter', () => {
27
- const lines = ['Ctrl-WT-1', 'Ctrl-WT-2', 'Treat-KO-1']
28
- const result = analyzeDelimiter(lines)
29
- expect(result.delimiter).toBe('-')
30
- expect(result.dominantFieldCount).toBe(3)
31
- })
32
-
33
- it('should detect dot delimiter', () => {
34
- const lines = ['Ctrl.WT.1', 'Ctrl.WT.2', 'Treat.KO.1']
35
- const result = analyzeDelimiter(lines)
36
- expect(result.delimiter).toBe('.')
37
- expect(result.dominantFieldCount).toBe(3)
38
- })
39
-
40
- it('should pick most consistent delimiter with mixed usage', () => {
41
- // Underscores used consistently (3 fields), hyphens appear but less consistently
42
- const lines = ['Ctrl_WT_1', 'Ctrl_WT_2', 'Treat_KO-1', 'Treat_KO_2']
43
- const result = analyzeDelimiter(lines)
44
- expect(result.delimiter).toBe('_')
45
- })
46
-
47
- it('should prefer underscore when tied', () => {
48
- // Both _ and - produce identical consistency
49
- const lines = ['A_B-C']
50
- const result = analyzeDelimiter(lines)
51
- expect(result.delimiter).toBe('_')
52
- })
53
-
54
- it('should handle single-segment samples (no delimiter)', () => {
55
- const lines = ['SampleA', 'SampleB', 'SampleC']
56
- const result = analyzeDelimiter(lines)
57
- expect(result.dominantFieldCount).toBe(1)
58
- expect(result.minFieldCount).toBe(1)
59
- })
60
-
61
- it('should handle empty input', () => {
62
- const result = analyzeDelimiter([])
63
- expect(result.delimiter).toBe('_')
64
- expect(result.dominantFieldCount).toBe(1)
65
- expect(result.minFieldCount).toBe(1)
66
- expect(result.consistency).toBe(0)
67
- })
68
-
69
- it('should compute correct consistency ratio', () => {
70
- // 3 out of 4 samples have 3 fields with underscore
71
- const lines = ['A_B_C', 'D_E_F', 'G_H_I', 'JK']
72
- const result = analyzeDelimiter(lines)
73
- expect(result.delimiter).toBe('_')
74
- expect(result.dominantFieldCount).toBe(3)
75
- expect(result.consistency).toBe(0.75)
76
- })
77
-
78
- it('should return correct minFieldCount with mixed field counts', () => {
79
- const lines = ['Control_Rep1', 'Treatment_Low_Rep1', 'Vehicle_Rep1']
80
- const result = analyzeDelimiter(lines)
81
- expect(result.delimiter).toBe('_')
82
- expect(result.minFieldCount).toBe(2)
83
- expect(result.dominantFieldCount).toBe(2)
84
- })
85
-
86
- it('should return minFieldCount different from dominantFieldCount when needed', () => {
87
- const lines = [
88
- 'Control_Rep1', 'Control_Rep2',
89
- 'Treatment_Low_Rep1', 'Treatment_Low_Rep2', 'Treatment_Low_Rep3',
90
- 'Treatment_High_Rep1', 'Treatment_High_Rep2', 'Treatment_High_Rep3',
91
- ]
92
- const result = analyzeDelimiter(lines)
93
- expect(result.delimiter).toBe('_')
94
- // Mode is 3 (6 samples have 3 fields vs 2 samples with 2 fields)
95
- expect(result.dominantFieldCount).toBe(3)
96
- // Min of multi-field counts is 2
97
- expect(result.minFieldCount).toBe(2)
98
- })
99
- })
100
-
101
- describe('detectOutliers', () => {
102
- it('should flag samples with fewer fields than minFieldCount', () => {
103
- const lines = ['Ctrl_WT_1', 'Ctrl_WT_2', 'QC_Pool', 'Treat_KO_1']
104
- const outliers = detectOutliers(lines, '_', 3)
105
- expect(outliers).toHaveLength(1)
106
- expect(outliers[0].sample).toBe('QC_Pool')
107
- expect(outliers[0].index).toBe(2)
108
- expect(outliers[0].fieldCount).toBe(2)
109
- expect(outliers[0].action).toBe('include')
110
- })
111
-
112
- it('should return empty array when all meet minFieldCount', () => {
113
- const lines = ['A_B_C', 'D_E_F', 'G_H_I']
114
- const outliers = detectOutliers(lines, '_', 3)
115
- expect(outliers).toHaveLength(0)
116
- })
117
-
118
- it('should preserve original line index', () => {
119
- const lines = ['A_B', 'C', 'D_E', 'F']
120
- const outliers = detectOutliers(lines, '_', 2)
121
- expect(outliers).toHaveLength(2)
122
- expect(outliers[0]).toEqual({ sample: 'C', index: 1, fieldCount: 1, action: 'include' })
123
- expect(outliers[1]).toEqual({ sample: 'F', index: 3, fieldCount: 1, action: 'include' })
124
- })
125
-
126
- it('should NOT flag samples with more fields than minFieldCount', () => {
127
- const lines = ['Ctrl_Rep1', 'Treat_Low_Rep1']
128
- const outliers = detectOutliers(lines, '_', 2)
129
- expect(outliers).toHaveLength(0)
130
- })
131
-
132
- it('should flag single-segment samples when minFieldCount requires delimiter', () => {
133
- const lines = ['Ctrl_Rep1', 'QCPool']
134
- const outliers = detectOutliers(lines, '_', 2)
135
- expect(outliers).toHaveLength(1)
136
- expect(outliers[0].sample).toBe('QCPool')
137
- expect(outliers[0].fieldCount).toBe(1)
138
- })
139
-
140
- it('should not flag any sample when minFieldCount is 1', () => {
141
- const lines = ['ABC', 'DEF', 'G_H_I']
142
- const outliers = detectOutliers(lines, '_', 1)
143
- expect(outliers).toHaveLength(0)
144
- })
145
- })
146
-
147
- describe('extractColumns', () => {
148
- it('should produce correct column count and unique values via right alignment', () => {
149
- const samples = ['Ctrl_Liver_1', 'Ctrl_Brain_2', 'Treat_Liver_1']
150
- const columns = extractColumns(samples, '_', 3)
151
- expect(columns).toHaveLength(3)
152
- expect(columns[0].uniqueValues).toEqual(['Ctrl', 'Treat'])
153
- expect(columns[1].uniqueValues).toEqual(['Liver', 'Brain'])
154
- expect(columns[2].uniqueValues).toEqual(['1', '2'])
155
- })
156
-
157
- it('should compute correct cardinality', () => {
158
- const samples = ['A_X_1', 'A_Y_2', 'B_X_1', 'B_Y_2']
159
- const columns = extractColumns(samples, '_', 3)
160
- expect(columns[0].cardinality).toBe(2) // A, B
161
- expect(columns[1].cardinality).toBe(2) // X, Y
162
- expect(columns[2].cardinality).toBe(2) // 1, 2
163
- })
164
-
165
- it('should assign Condition as first column name and Field N for rest', () => {
166
- const samples = ['A_B_C']
167
- const columns = extractColumns(samples, '_', 3)
168
- expect(columns[0].name).toBe('Condition')
169
- expect(columns[1].name).toBe('Field 2')
170
- expect(columns[2].name).toBe('Field 3')
171
- })
172
-
173
- it('should set correct column indices', () => {
174
- const samples = ['A_B_C_D']
175
- const columns = extractColumns(samples, '_', 4)
176
- expect(columns.map(c => c.index)).toEqual([0, 1, 2, 3])
177
- })
178
-
179
- it('should handle empty input', () => {
180
- const columns = extractColumns([], '_', 2)
181
- expect(columns).toHaveLength(0)
182
- })
183
-
184
- it('should handle variable-length prefixes via right alignment', () => {
185
- const samples = ['Control_Rep1', 'Treatment_Low_Rep1', 'Vehicle_Rep1']
186
- const columns = extractColumns(samples, '_', 2)
187
- expect(columns).toHaveLength(2)
188
- expect(columns[0].uniqueValues).toEqual(['Control', 'Treatment_Low', 'Vehicle'])
189
- expect(columns[1].uniqueValues).toEqual(['Rep1'])
190
- })
191
-
192
- it('should right-align with 3+ suffix fields', () => {
193
- const samples = ['A_X_1', 'B_C_X_1']
194
- const columns = extractColumns(samples, '_', 3)
195
- expect(columns).toHaveLength(3)
196
- expect(columns[0].uniqueValues).toEqual(['A', 'B_C'])
197
- expect(columns[1].uniqueValues).toEqual(['X'])
198
- expect(columns[2].uniqueValues).toEqual(['1'])
199
- })
200
-
201
- it('should mark column types (prefix vs suffix)', () => {
202
- const samples = ['A_B_C']
203
- const columns = extractColumns(samples, '_', 3)
204
- expect(columns[0].type).toBe('prefix')
205
- expect(columns[1].type).toBe('suffix')
206
- expect(columns[2].type).toBe('suffix')
207
- })
208
- })
209
-
210
- describe('parseCSVLine', () => {
211
- it('should parse simple comma-separated line', () => {
212
- expect(parseCSVLine('A,B,C')).toEqual(['A', 'B', 'C'])
213
- })
214
-
215
- it('should handle quoted fields with commas', () => {
216
- expect(parseCSVLine('A,"B,C",D')).toEqual(['A', 'B,C', 'D'])
217
- })
218
-
219
- it('should trim whitespace from fields', () => {
220
- expect(parseCSVLine(' A , B , C ')).toEqual(['A', 'B', 'C'])
221
- })
222
-
223
- it('should parse tab-separated line when delimiter is tab', () => {
224
- expect(parseCSVLine('A\tB\tC', '\t')).toEqual(['A', 'B', 'C'])
225
- })
226
- })
227
7
 
228
8
  describe('parseCSV', () => {
229
9
  it('should parse simple CSV with headers', () => {
@@ -285,356 +65,6 @@ describe('parseCSV', () => {
285
65
  })
286
66
  })
287
67
 
288
- describe('computeGroups', () => {
289
- it('should group by single column', () => {
290
- const samples = ['Ctrl_Liver_1', 'Ctrl_Brain_2', 'Treat_Liver_1']
291
- const columns = extractColumns(samples, '_', 3)
292
- const enabledFields = new Set([0]) // group by first column only
293
-
294
- const result = computeGroups(samples, columns, enabledFields, new Map(), '_', 3)
295
- expect(result.groups).toHaveLength(2)
296
-
297
- const ctrlGroup = result.groups.find(g => g.name === 'Ctrl')
298
- const treatGroup = result.groups.find(g => g.name === 'Treat')
299
- expect(ctrlGroup?.samples).toEqual(['Ctrl_Liver_1', 'Ctrl_Brain_2'])
300
- expect(treatGroup?.samples).toEqual(['Treat_Liver_1'])
301
- })
302
-
303
- it('should group by multiple columns joined with " / "', () => {
304
- const samples = ['Ctrl_Liver_1', 'Ctrl_Brain_2', 'Treat_Liver_1']
305
- const columns = extractColumns(samples, '_', 3)
306
- const enabledFields = new Set([0, 1])
307
-
308
- const result = computeGroups(samples, columns, enabledFields, new Map(), '_', 3)
309
- expect(result.groups).toHaveLength(3)
310
-
311
- const names = result.groups.map(g => g.name).sort()
312
- expect(names).toEqual(['Ctrl / Brain', 'Ctrl / Liver', 'Treat / Liver'])
313
- })
314
-
315
- it('should cycle colors from DEFAULT_COLORS', () => {
316
- const samples = Array.from({ length: 12 }, (_, i) => `G${i}_X`)
317
- const columns = extractColumns(samples, '_', 2)
318
- const enabledFields = new Set([0])
319
-
320
- const result = computeGroups(samples, columns, enabledFields, new Map(), '_', 2)
321
- // 12 unique groups should cycle through 10 colors
322
- expect(result.groups[0].color).toBe(DEFAULT_COLORS[0])
323
- expect(result.groups[10].color).toBe(DEFAULT_COLORS[0])
324
- expect(result.groups[11].color).toBe(DEFAULT_COLORS[1])
325
- })
326
-
327
- it('should exclude outliers marked as "exclude"', () => {
328
- const samples = ['Ctrl_WT_1', 'Ctrl_WT_2', 'QC_Pool']
329
- const columns = extractColumns(['Ctrl_WT_1', 'Ctrl_WT_2'], '_', 3)
330
- const enabledFields = new Set([0])
331
- const outlierActions = new Map<number, OutlierAction>([[2, 'exclude']])
332
-
333
- const result = computeGroups(samples, columns, enabledFields, outlierActions, '_', 3)
334
- expect(result.excludedSamples).toContain('QC_Pool')
335
- const allGroupedSamples = result.groups.flatMap(g => g.samples)
336
- expect(allGroupedSamples).not.toContain('QC_Pool')
337
- })
338
-
339
- it('should put QC-marked outliers in a QC group', () => {
340
- const samples = ['Ctrl_WT_1', 'Ctrl_WT_2', 'QC_Pool']
341
- const columns = extractColumns(['Ctrl_WT_1', 'Ctrl_WT_2'], '_', 3)
342
- const enabledFields = new Set([0])
343
- const outlierActions = new Map<number, OutlierAction>([[2, 'qc']])
344
-
345
- const result = computeGroups(samples, columns, enabledFields, outlierActions, '_', 3)
346
- const qcGroup = result.groups.find(g => g.name === 'QC')
347
- expect(qcGroup).toBeDefined()
348
- expect(qcGroup?.samples).toContain('QC_Pool')
349
- expect(qcGroup?.color).toBe('#6B7280')
350
- })
351
-
352
- it('should generate metadata rows', () => {
353
- const samples = ['Ctrl_Liver_1', 'Treat_Brain_2']
354
- const columns = extractColumns(samples, '_', 3)
355
- // Rename columns for metadata
356
- columns[0].name = 'Condition'
357
- columns[1].name = 'Tissue'
358
- columns[2].name = 'Replicate'
359
- const enabledFields = new Set([0, 1])
360
-
361
- const result = computeGroups(samples, columns, enabledFields, new Map(), '_', 3)
362
- expect(result.metadata).toHaveLength(2)
363
- expect(result.metadata[0]).toEqual({
364
- sampleName: 'Ctrl_Liver_1',
365
- fields: { Condition: 'Ctrl', Tissue: 'Liver', Replicate: '1' },
366
- group: 'Ctrl / Liver',
367
- })
368
- })
369
-
370
- it('should handle single column without separator in group name', () => {
371
- const samples = ['A_1', 'B_2', 'A_3']
372
- const columns = extractColumns(samples, '_', 2)
373
- const enabledFields = new Set([0])
374
-
375
- const result = computeGroups(samples, columns, enabledFields, new Map(), '_', 2)
376
- const names = result.groups.map(g => g.name)
377
- expect(names).toContain('A')
378
- expect(names).toContain('B')
379
- // Should NOT contain " / " separator
380
- expect(names.every(n => !n.includes(' / '))).toBe(true)
381
- })
382
-
383
- it('should correctly group variable-length prefixes', () => {
384
- const samples = [
385
- 'Control_Rep1', 'Control_Rep2', 'Control_Rep3',
386
- 'Treatment_Low_Rep1', 'Treatment_Low_Rep2', 'Treatment_Low_Rep3',
387
- ]
388
- const columns = extractColumns(samples, '_', 2)
389
- const enabledFields = new Set([0])
390
-
391
- const result = computeGroups(samples, columns, enabledFields, new Map(), '_', 2)
392
- expect(result.groups).toHaveLength(2)
393
-
394
- const ctrlGroup = result.groups.find(g => g.name === 'Control')
395
- const treatGroup = result.groups.find(g => g.name === 'Treatment_Low')
396
- expect(ctrlGroup?.samples).toHaveLength(3)
397
- expect(treatGroup?.samples).toHaveLength(3)
398
- })
399
- })
400
-
401
- describe('classifyOutlierAction', () => {
402
- it('should return qc for samples containing QC keywords', () => {
403
- expect(classifyOutlierAction('LT_13102025_EQC_Jurkat_1_2', '_')).toBe('qc')
404
- expect(classifyOutlierAction('LT_13102025_IQC_Pool_1', '_')).toBe('qc')
405
- expect(classifyOutlierAction('Blank_001', '_')).toBe('qc')
406
- expect(classifyOutlierAction('STD_Low_1', '_')).toBe('qc')
407
- expect(classifyOutlierAction('test_EQC_Jurkat_02', '_')).toBe('qc')
408
- })
409
-
410
- it('should match case-insensitively', () => {
411
- expect(classifyOutlierAction('LT_eqc_Pool', '_')).toBe('qc')
412
- expect(classifyOutlierAction('LT_EQC_Pool', '_')).toBe('qc')
413
- expect(classifyOutlierAction('LT_Eqc_Pool', '_')).toBe('qc')
414
- expect(classifyOutlierAction('BLANK_001', '_')).toBe('qc')
415
- expect(classifyOutlierAction('Test_Sample', '_')).toBe('qc')
416
- })
417
-
418
- it('should match against individual segments only', () => {
419
- // "eqc" embedded inside a segment should NOT match
420
- expect(classifyOutlierAction('LT_SEQC123_Pool', '_')).toBe('include')
421
- // "std" as standalone segment should match
422
- expect(classifyOutlierAction('LT_std_Pool', '_')).toBe('qc')
423
- })
424
-
425
- it('should return include for regular experimental samples', () => {
426
- expect(classifyOutlierAction('LT_13102025_212_WT_Glu', '_')).toBe('include')
427
- expect(classifyOutlierAction('Control_Rep1', '_')).toBe('include')
428
- expect(classifyOutlierAction('Treatment_High_Rep3', '_')).toBe('include')
429
- })
430
-
431
- it('should work with different delimiters', () => {
432
- expect(classifyOutlierAction('LT-EQC-Pool', '-')).toBe('qc')
433
- expect(classifyOutlierAction('blank.001', '.')).toBe('qc')
434
- expect(classifyOutlierAction('Control-Rep1', '-')).toBe('include')
435
- })
436
- })
437
-
438
- describe('computeGroupsFromCsv', () => {
439
- it('should group samples by CSV column values', () => {
440
- const csvData = parseCSV(
441
- 'File Name\tTreatment Group\n' +
442
- 'Sample_001\tCtrl\n' +
443
- 'Sample_002\tCtrl\n' +
444
- 'Sample_003\tTreat\n' +
445
- 'Sample_004\tTreat\n'
446
- )
447
- const columns = [
448
- { index: 0, name: 'Treatment Group', uniqueValues: ['Ctrl', 'Treat'], cardinality: 2 },
449
- ]
450
- const result = computeGroupsFromCsv(csvData, columns, new Set([0]))
451
- expect(result.groups).toHaveLength(2)
452
-
453
- const ctrl = result.groups.find(g => g.name === 'Ctrl')
454
- const treat = result.groups.find(g => g.name === 'Treat')
455
- expect(ctrl?.samples).toEqual(['Sample_001', 'Sample_002'])
456
- expect(treat?.samples).toEqual(['Sample_003', 'Sample_004'])
457
- expect(result.excludedSamples).toEqual([])
458
- })
459
-
460
- it('should group by multiple enabled CSV columns', () => {
461
- const csvData = parseCSV(
462
- 'Sample,Condition,Tissue\nS1,Ctrl,Liver\nS2,Ctrl,Brain\nS3,Treat,Liver'
463
- )
464
- const columns = [
465
- { index: 0, name: 'Condition', uniqueValues: ['Ctrl', 'Treat'], cardinality: 2 },
466
- { index: 1, name: 'Tissue', uniqueValues: ['Liver', 'Brain'], cardinality: 2 },
467
- ]
468
- const result = computeGroupsFromCsv(csvData, columns, new Set([0, 1]))
469
- expect(result.groups).toHaveLength(3)
470
-
471
- const names = result.groups.map(g => g.name).sort()
472
- expect(names).toEqual(['Ctrl / Brain', 'Ctrl / Liver', 'Treat / Liver'])
473
- })
474
-
475
- it('should generate metadata rows from CSV data', () => {
476
- const csvData = parseCSV(
477
- 'Sample,Condition,Tissue\nS1,Ctrl,Liver\nS2,Treat,Brain'
478
- )
479
- const columns = [
480
- { index: 0, name: 'Condition', uniqueValues: ['Ctrl', 'Treat'], cardinality: 2 },
481
- { index: 1, name: 'Tissue', uniqueValues: ['Liver', 'Brain'], cardinality: 2 },
482
- ]
483
- const result = computeGroupsFromCsv(csvData, columns, new Set([0]))
484
- expect(result.metadata).toHaveLength(2)
485
- expect(result.metadata[0]).toEqual({
486
- sampleName: 'S1',
487
- fields: { Condition: 'Ctrl', Tissue: 'Liver' },
488
- group: 'Ctrl',
489
- })
490
- })
491
-
492
- it('should still group correctly after user renames a field', () => {
493
- const csvData = parseCSV(
494
- 'Sample,Condition,Tissue\nS1,Ctrl,Liver\nS2,Treat,Brain'
495
- )
496
- // Simulate user renaming "Condition" to "Treatment" in the Fields step
497
- const columns = [
498
- { index: 0, name: 'Treatment', originalName: 'Condition', uniqueValues: ['Ctrl', 'Treat'], cardinality: 2 },
499
- { index: 1, name: 'Organ', originalName: 'Tissue', uniqueValues: ['Liver', 'Brain'], cardinality: 2 },
500
- ]
501
- const result = computeGroupsFromCsv(csvData, columns, new Set([0]))
502
- expect(result.groups).toHaveLength(2)
503
- expect(result.groups.find(g => g.name === 'Ctrl')?.samples).toEqual(['S1'])
504
- expect(result.groups.find(g => g.name === 'Treat')?.samples).toEqual(['S2'])
505
-
506
- // Metadata should use the display name as key
507
- expect(result.metadata[0].fields['Treatment']).toBe('Ctrl')
508
- expect(result.metadata[0].fields['Organ']).toBe('Liver')
509
- })
510
-
511
- it('should handle the real-world TSV format from the bug report', () => {
512
- const text = [
513
- 'File Name\tTreatment Group',
514
- 'exp275_260401_YS_RNA_blank_001\tBlank',
515
- 'exp275_260401_YS_RNA_SplusA1_002\tS+A',
516
- 'exp275_260401_YS_RNA_A2_004\tA',
517
- 'exp275_260401_YS_RNA_NC_005\tDigControl',
518
- 'exp275_260401_YS_RNA_R2_007\tR',
519
- 'exp275_260401_YS_RNA_S2_009\tS',
520
- 'exp275_260401_YS_RNA_C1_010\tC',
521
- 'exp275_260401_YS_RNA_RplusA1_011\tR+A',
522
- ].join('\n')
523
- const csvData = parseCSV(text)
524
-
525
- expect(csvData.sampleColumn).toBe('File Name')
526
- expect(csvData.columns).toEqual(['File Name', 'Treatment Group'])
527
- expect(csvData.rows).toHaveLength(8)
528
-
529
- const columns = [
530
- { index: 0, name: 'Treatment Group', uniqueValues: ['Blank', 'S+A', 'A', 'DigControl', 'R', 'S', 'C', 'R+A'], cardinality: 8 },
531
- ]
532
- const result = computeGroupsFromCsv(csvData, columns, new Set([0]))
533
- expect(result.groups).toHaveLength(8)
534
-
535
- const blankGroup = result.groups.find(g => g.name === 'Blank')
536
- expect(blankGroup?.samples).toEqual(['exp275_260401_YS_RNA_blank_001'])
537
-
538
- const saGroup = result.groups.find(g => g.name === 'S+A')
539
- expect(saGroup?.samples).toEqual(['exp275_260401_YS_RNA_SplusA1_002'])
540
- })
541
- })
542
-
543
- describe('integration: mixed experimental + QC + test samples', () => {
544
- // Simulates the real dataset pattern:
545
- // 154 experimental (11 fields), ~15 QC (6-7 fields), 3 test (4 fields)
546
- const experimental = [
547
- 'LT_13102025_212_WT_Glu_3_20_S_091123_T1_33',
548
- 'LT_13102025_213_WT_Glu_3_20_S_091123_T1_34',
549
- 'LT_13102025_214_KI_Glu_3_20_S_091123_T1_35',
550
- 'LT_13102025_215_KI_Glu_6_40_L_091123_T1_36',
551
- 'LT_13102025_216_WT_Glu_6_40_L_091123_T1_37',
552
- 'LT_13102025_217_WT_Glu_6_40_L_091123_T2_38',
553
- 'LT_13102025_218_KI_Glu_3_20_S_091123_T2_39',
554
- ]
555
- const qcSamples = [
556
- 'LT_13102025_EQC_Jurkat_1_2',
557
- 'LT_13102025_IQC_Pool_3',
558
- ]
559
- const testSamples = [
560
- 'test_EQC_Jurkat_02',
561
- ]
562
- const allLines = [...experimental, ...qcSamples, ...testSamples]
563
-
564
- it('should detect dominantFieldCount=11 and flag QC/test as outliers', () => {
565
- const analysis = analyzeDelimiter(allLines)
566
- expect(analysis.delimiter).toBe('_')
567
- expect(analysis.dominantFieldCount).toBe(11)
568
-
569
- const outliers = detectOutliers(allLines, '_', analysis.dominantFieldCount)
570
- // QC samples (6 fields) and test samples (4 fields) are outliers
571
- expect(outliers).toHaveLength(qcSamples.length + testSamples.length)
572
-
573
- // Experimental samples should NOT be flagged
574
- const outlierIndices = new Set(outliers.map(o => o.index))
575
- for (let i = 0; i < experimental.length; i++) {
576
- expect(outlierIndices.has(i)).toBe(false)
577
- }
578
- })
579
-
580
- it('should auto-classify QC/test outliers with smart defaults', () => {
581
- const analysis = analyzeDelimiter(allLines)
582
- const outliers = detectOutliers(allLines, '_', analysis.dominantFieldCount)
583
-
584
- for (const outlier of outliers) {
585
- outlier.action = classifyOutlierAction(outlier.sample, '_')
586
- }
587
-
588
- // All QC and test samples should be classified as 'qc'
589
- expect(outliers.every(o => o.action === 'qc')).toBe(true)
590
- })
591
-
592
- it('should produce 11 columns from conforming samples', () => {
593
- const analysis = analyzeDelimiter(allLines)
594
- const outliers = detectOutliers(allLines, '_', analysis.dominantFieldCount)
595
- const conforming = allLines.filter(
596
- (_, i) => !outliers.some(o => o.index === i)
597
- )
598
-
599
- const conformingFieldCounts = conforming.map(s => s.split('_').length)
600
- const effectiveMinFieldCount = Math.min(...conformingFieldCounts)
601
-
602
- const columns = extractColumns(conforming, '_', effectiveMinFieldCount)
603
- expect(columns).toHaveLength(11)
604
- })
605
-
606
- it('should auto-disable constant columns (cardinality 1)', () => {
607
- const analysis = analyzeDelimiter(allLines)
608
- const outliers = detectOutliers(allLines, '_', analysis.dominantFieldCount)
609
- const conforming = allLines.filter(
610
- (_, i) => !outliers.some(o => o.index === i)
611
- )
612
-
613
- const conformingFieldCounts = conforming.map(s => s.split('_').length)
614
- const effectiveMinFieldCount = Math.min(...conformingFieldCounts)
615
-
616
- const columns = extractColumns(conforming, '_', effectiveMinFieldCount)
617
-
618
- // Auto-disable: only enable columns with cardinality > 1
619
- const enabled = new Set(
620
- columns.filter(f => f.cardinality > 1).map(f => f.index)
621
- )
622
-
623
- // Constant columns (LT, 13102025, Glu, 091123) should be disabled
624
- const constantCols = columns.filter(c => c.cardinality === 1)
625
- for (const col of constantCols) {
626
- expect(enabled.has(col.index)).toBe(false)
627
- }
628
-
629
- // Variable columns should be enabled
630
- const variableCols = columns.filter(c => c.cardinality > 1)
631
- for (const col of variableCols) {
632
- expect(enabled.has(col.index)).toBe(true)
633
- }
634
- expect(variableCols.length).toBeGreaterThan(0)
635
- })
636
- })
637
-
638
68
  describe('extractSamplesFromDesignData', () => {
639
69
  it('should extract samples with conditions into ParsedCsvData', () => {
640
70
  const data = {
@@ -648,12 +78,12 @@ describe('extractSamplesFromDesignData', () => {
648
78
  const result = extractSamplesFromDesignData(data)
649
79
  expect(result).not.toBeNull()
650
80
  expect(result!.sampleColumn).toBe('sample_name')
651
- expect(result!.columns).toEqual(['sample_name', 'Cell Line', 'Treatment'])
81
+ expect(result!.columns).toEqual(['sample_name', 'Cell Line', 'Treatment', 'sample_type'])
652
82
  expect(result!.rows).toHaveLength(3)
653
- expect(result!.rows[0]).toEqual({ sample_name: 'HeLa_DrugA_1', 'Cell Line': 'HeLa', Treatment: 'Drug A' })
83
+ expect(result!.rows[0]).toEqual({ sample_name: 'HeLa_DrugA_1', 'Cell Line': 'HeLa', Treatment: 'Drug A', sample_type: 'sample' })
654
84
  })
655
85
 
656
- it('should filter out QC and blank samples by sample_type', () => {
86
+ it('should retain QC and blank samples and surface their type in sampleTypeHints', () => {
657
87
  const data = {
658
88
  samples: [
659
89
  { sample_name: 'Sample_1', sample_type: 'sample', conditions: { Treatment: 'Drug A' } },
@@ -664,8 +94,11 @@ describe('extractSamplesFromDesignData', () => {
664
94
  }
665
95
  const result = extractSamplesFromDesignData(data)
666
96
  expect(result).not.toBeNull()
667
- expect(result!.rows).toHaveLength(2)
668
- expect(result!.rows.map(r => r.sample_name)).toEqual(['Sample_1', 'Sample_2'])
97
+ // All 4 samples are retained — QC/blank are no longer filtered here.
98
+ expect(result!.rows).toHaveLength(4)
99
+ expect(result!.rows.map(r => r.sample_name)).toEqual(['Sample_1', 'QC_Pool', 'Blank_1', 'Sample_2'])
100
+ // sampleTypeHints parallels the rows array.
101
+ expect(result!.sampleTypeHints).toEqual(['sample', 'qc', 'blank', 'sample'])
669
102
  })
670
103
 
671
104
  it('should return null when no samples array exists', () => {
@@ -677,14 +110,18 @@ describe('extractSamplesFromDesignData', () => {
677
110
  expect(extractSamplesFromDesignData({ samples: [] })).toBeNull()
678
111
  })
679
112
 
680
- it('should return null when all samples are QC/blank', () => {
113
+ it('should retain all-QC/blank batches and set sampleTypeHints accordingly', () => {
114
+ // QC_1 has conditions so allConditionKeys is non-empty; both samples are retained.
681
115
  const data = {
682
116
  samples: [
683
117
  { sample_name: 'QC_1', sample_type: 'qc', conditions: { Treatment: 'QC' } },
684
118
  { sample_name: 'Blank', sample_type: 'blank', conditions: {} },
685
119
  ],
686
120
  }
687
- expect(extractSamplesFromDesignData(data)).toBeNull()
121
+ const result = extractSamplesFromDesignData(data)
122
+ expect(result).not.toBeNull()
123
+ expect(result!.rows).toHaveLength(2)
124
+ expect(result!.sampleTypeHints).toEqual(['qc', 'blank'])
688
125
  })
689
126
 
690
127
  it('should return null when no samples have conditions', () => {
@@ -706,11 +143,11 @@ describe('extractSamplesFromDesignData', () => {
706
143
  }
707
144
  const result = extractSamplesFromDesignData(data)
708
145
  expect(result).not.toBeNull()
709
- expect(result!.columns).toEqual(['sample_name', 'Cell Line', 'Treatment', 'Timepoint'])
146
+ expect(result!.columns).toEqual(['sample_name', 'Cell Line', 'Treatment', 'Timepoint', 'sample_type'])
710
147
  // S1 has no Timepoint → empty string
711
- expect(result!.rows[0]).toEqual({ sample_name: 'S1', 'Cell Line': 'HeLa', Treatment: 'Drug A', Timepoint: '' })
148
+ expect(result!.rows[0]).toEqual({ sample_name: 'S1', 'Cell Line': 'HeLa', Treatment: 'Drug A', Timepoint: '', sample_type: 'sample' })
712
149
  // S2 has no Treatment → empty string
713
- expect(result!.rows[1]).toEqual({ sample_name: 'S2', 'Cell Line': 'MCF7', Treatment: '', Timepoint: '24h' })
150
+ expect(result!.rows[1]).toEqual({ sample_name: 'S2', 'Cell Line': 'MCF7', Treatment: '', Timepoint: '24h', sample_type: 'sample' })
714
151
  })
715
152
 
716
153
  it('should preserve column insertion order from first occurrence', () => {
@@ -721,8 +158,8 @@ describe('extractSamplesFromDesignData', () => {
721
158
  ],
722
159
  }
723
160
  const result = extractSamplesFromDesignData(data)
724
- // Timepoint seen first in S1, then Treatment, then Cell Line from S2
725
- expect(result!.columns).toEqual(['sample_name', 'Timepoint', 'Treatment', 'Cell Line'])
161
+ // Timepoint seen first in S1, then Treatment, then Cell Line from S2; sample_type appended last.
162
+ expect(result!.columns).toEqual(['sample_name', 'Timepoint', 'Treatment', 'Cell Line', 'sample_type'])
726
163
  })
727
164
 
728
165
  it('should handle samples without sample_type (defaults to "sample")', () => {
@@ -737,7 +174,7 @@ describe('extractSamplesFromDesignData', () => {
737
174
  expect(result!.rows).toHaveLength(2)
738
175
  })
739
176
 
740
- it('should handle case-insensitive sample_type filtering', () => {
177
+ it('should lower-case sample_type in sampleTypeHints regardless of input casing', () => {
741
178
  const data = {
742
179
  samples: [
743
180
  { sample_name: 'S1', sample_type: 'SAMPLE', conditions: { Treatment: 'Drug' } },
@@ -747,8 +184,9 @@ describe('extractSamplesFromDesignData', () => {
747
184
  }
748
185
  const result = extractSamplesFromDesignData(data)
749
186
  expect(result).not.toBeNull()
750
- expect(result!.rows).toHaveLength(1)
751
- expect(result!.rows[0].sample_name).toBe('S1')
187
+ // All 3 samples retained; hints are lower-cased.
188
+ expect(result!.rows).toHaveLength(3)
189
+ expect(result!.sampleTypeHints).toEqual(['sample', 'qc', 'blank'])
752
190
  })
753
191
 
754
192
  it('should work with a realistic MS Designer design_data structure', () => {
@@ -770,30 +208,20 @@ describe('extractSamplesFromDesignData', () => {
770
208
  }
771
209
  const result = extractSamplesFromDesignData(data)
772
210
  expect(result).not.toBeNull()
773
- expect(result!.rows).toHaveLength(3) // Only experimental samples
774
- expect(result!.columns).toEqual(['sample_name', 'Cell Line', 'Treatment', 'Timepoint'])
775
-
776
- // Verify it's compatible with computeGroupsFromCsv
777
- const columns = result!.columns
778
- .filter(c => c !== 'sample_name')
779
- .map((col, i) => ({
780
- index: i,
781
- name: col,
782
- originalName: col,
783
- uniqueValues: [...new Set(result!.rows.map(r => r[col]))],
784
- cardinality: new Set(result!.rows.map(r => r[col])).size,
785
- }))
786
-
787
- // Group by Treatment only
788
- const groups = computeGroupsFromCsv(result!, columns, new Set([1])) // Treatment is index 1
789
- expect(groups.groups).toHaveLength(2)
790
- expect(groups.groups.find(g => g.name === 'Drug A')?.samples).toHaveLength(2)
791
- expect(groups.groups.find(g => g.name === 'Control')?.samples).toHaveLength(1)
211
+ // All 5 samples are retained (including QC and blank).
212
+ expect(result!.rows).toHaveLength(5)
213
+ expect(result!.columns).toEqual(['sample_name', 'Cell Line', 'Treatment', 'Timepoint', 'sample_type'])
214
+ expect(result!.sampleTypeHints).toEqual(['sample', 'sample', 'sample', 'qc', 'blank'])
792
215
  })
793
216
  })
794
217
 
795
218
  describe('useAutoGroup auto-disable degenerate columns', () => {
796
- it('auto-disables unique-per-row columns in paste mode', () => {
219
+ it('collapses unique-per-row injection numbers via replicate pre-grouping', () => {
220
+ // The replicate pre-grouping pass strips the trailing 3-digit run-order
221
+ // tokens before tokenisation, so 5 inputs collapse to 2 base names
222
+ // (Ctrl_WT and Treat_WT). This supersedes the old "auto-disable
223
+ // unique-per-row column" mechanism — there IS no unique-per-row column
224
+ // for the schema to see, because pre-grouping already absorbed it.
797
225
  const auto = useAutoGroup()
798
226
  auto.rawText.value = [
799
227
  'Ctrl_WT_001',
@@ -804,30 +232,35 @@ describe('useAutoGroup auto-disable degenerate columns', () => {
804
232
  ].join('\n')
805
233
  auto.parseInput()
806
234
 
807
- const lastCol = auto.fields.value.find(f => f.cardinality === 5)
808
- expect(lastCol, 'expected a unique-per-row column').toBeTruthy()
809
- expect(auto.enabledFields.value.has(lastCol!.index)).toBe(false)
810
- // With the unique column auto-disabled, samples aggregate by Condition only
811
- // (WT is constant, also auto-disabled): Ctrl=3 samples, Treat=2 samples.
812
235
  expect(auto.groups.value).toHaveLength(2)
236
+ // Ctrl has 3 samples, Treat has 2 — replicates expanded back from the
237
+ // base names by expandGroupsWithReplicates in the result computed.
238
+ const ctrl = auto.groups.value.find(g => g.name === 'Ctrl')
239
+ const treat = auto.groups.value.find(g => g.name === 'Treat')
240
+ expect(ctrl?.samples).toHaveLength(3)
241
+ expect(treat?.samples).toHaveLength(2)
813
242
  expect(auto.groups.value.every(g => g.samples.length > 1)).toBe(true)
814
243
  })
815
244
 
816
- it('auto-disables unique-per-row columns in CSV mode', () => {
245
+ it('the schema never sees a degenerate unique-per-row column', () => {
246
+ // Same setup, asserting the contract from the other side: after the
247
+ // pre-grouping pass, the active schema's columns reflect the BASE
248
+ // tokens only. There's no column with cardinality === sample-count.
817
249
  const auto = useAutoGroup()
818
- auto.inputMode.value = 'csv'
819
- auto.csvData.value = parseCSV(
820
- 'sample,condition,uid\n' +
821
- 'S1,Ctrl,a1\n' +
822
- 'S2,Ctrl,a2\n' +
823
- 'S3,Treat,a3\n' +
824
- 'S4,Treat,a4\n',
825
- )
250
+ auto.rawText.value = [
251
+ 'Ctrl_WT_001',
252
+ 'Ctrl_WT_002',
253
+ 'Ctrl_WT_003',
254
+ 'Treat_WT_004',
255
+ 'Treat_WT_005',
256
+ ].join('\n')
826
257
  auto.parseInput()
827
258
 
828
- const uidField = auto.fields.value.find(f => f.name === 'uid')
829
- expect(uidField, 'expected uid column').toBeTruthy()
830
- expect(auto.enabledFields.value.has(uidField!.index)).toBe(false)
259
+ // 5 originals 2 base names → 2 schema columns: condition (Ctrl/Treat)
260
+ // and the constant WT. Neither has cardinality 5.
261
+ const schema = auto.activeSchema.value
262
+ expect(schema).toBeTruthy()
263
+ expect(schema!.columns.every(c => c.cardinality < 5)).toBe(true)
831
264
  })
832
265
 
833
266
  it('keeps useful columns (cardinality between 2 and N-1) enabled', () => {
@@ -850,11 +283,82 @@ describe('useAutoGroup auto-disable degenerate columns', () => {
850
283
 
851
284
  it('exposes allSingletons=true when every group has exactly one sample', () => {
852
285
  const auto = useAutoGroup()
286
+ // All-unique samples: each is classified as unknown, all 3 columns are factor by default
853
287
  auto.rawText.value = ['A_x_1', 'B_y_2', 'C_z_3'].join('\n')
854
288
  auto.parseInput()
855
- auto.enabledFields.value = new Set(auto.fields.value.map(f => f.index))
856
-
289
+ // With all 3 factor columns in groupBy by default, each unique combination = singleton
857
290
  expect(auto.groups.value.length).toBe(3)
858
291
  expect(auto.allSingletons.value).toBe(true)
859
292
  })
860
293
  })
294
+
295
+ describe('useAutoGroup — class-first orchestration', () => {
296
+ it('detects classes after parseInput runs', async () => {
297
+ const ag = useAutoGroup()
298
+ ag.rawText.value = [
299
+ 'exp281_260415_SAM_SAH_Tissues_Kidney_M1_001',
300
+ 'exp281_260415_SAM_SAH_Tissues_Liver_M1_002',
301
+ 'exp281_260415_SAM_SAH_Tissues_IQC_003',
302
+ ].join('\n')
303
+ ag.parseInput()
304
+ const kinds = ag.classes.value.map(c => c.kind).sort()
305
+ expect(kinds).toContain('biological')
306
+ expect(kinds).toContain('iqc')
307
+ })
308
+
309
+ it('switches active schema when activeClassKey changes', () => {
310
+ const ag = useAutoGroup()
311
+ ag.rawText.value = [
312
+ 'a_Kidney_M1_001',
313
+ 'b_Liver_M1_002',
314
+ 'c_IQC_003',
315
+ ].join('\n')
316
+ ag.parseInput()
317
+ const firstKey = ag.classes.value[0].subKind
318
+ ? `${ag.classes.value[0].kind}:${ag.classes.value[0].subKind}`
319
+ : ag.classes.value[0].kind
320
+ ag.activeClassKey.value = firstKey
321
+ expect(ag.activeSchema.value!.classKind).toBe(ag.classes.value[0].kind)
322
+ })
323
+
324
+ it('setClassDisposition rewrites disposition and re-derives qcGroups', () => {
325
+ const ag = useAutoGroup()
326
+ ag.rawText.value = [
327
+ 'exp_Plasma_M1_001',
328
+ 'exp_Plasma_M2_002',
329
+ 'exp_IQC_003',
330
+ ].join('\n')
331
+ ag.parseInput()
332
+ ag.setClassDisposition('iqc', 'group')
333
+ expect(ag.classes.value.find(c => c.kind === 'iqc')!.disposition).toBe('group')
334
+ })
335
+
336
+ it('legacy setOutlierAction is a no-op deprecation shim', () => {
337
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
338
+ const ag = useAutoGroup()
339
+ ag.rawText.value = ['a_b_c'].join('\n')
340
+ ag.parseInput()
341
+ ag.setOutlierAction(0, 'exclude')
342
+ expect(warn).toHaveBeenCalled()
343
+ warn.mockRestore()
344
+ })
345
+
346
+ it('preserves legacy public refs after redesign', () => {
347
+ const ag = useAutoGroup()
348
+ ag.rawText.value = 'a_b_c\nd_e_f'
349
+ ag.parseInput()
350
+ expect(ag.delimiter.value).toBeDefined()
351
+ expect(ag.fields.value).toBeDefined()
352
+ expect(ag.enabledFields.value).toBeDefined()
353
+ expect(ag.effectiveColumns.value).toBeDefined()
354
+ expect(ag.allSingletons.value).toBeDefined()
355
+ })
356
+
357
+ it('downloadTemplate composes blank template before classifier ran', () => {
358
+ const ag = useAutoGroup()
359
+ ag.rawText.value = 'a_b\nc_d'
360
+ ag.parseInput()
361
+ const { content } = ag.composeTemplateForTest({ mode: 'blank', format: 'csv' })
362
+ expect(content.split('\n')[0]).toContain('sample_name')
363
+ })
364
+ })