@morscherlab/mint-sdk 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/__tests__/components/AutoGroupModal.anatomy.test.d.ts +1 -0
  2. package/dist/__tests__/components/AutoGroupModal.wiring.test.d.ts +1 -0
  3. package/dist/__tests__/composables/autoGroup/fieldEdits.test.d.ts +1 -0
  4. package/dist/__tests__/composables/autoGroup/golden.test.d.ts +1 -0
  5. package/dist/components/AutoGroupModal.adapter.d.ts +27 -4
  6. package/dist/components/SmartGroup.types.d.ts +22 -0
  7. package/dist/components/SmartGroupFieldRecipe.groups.d.ts +10 -2
  8. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +3 -0
  9. package/dist/components/index.js +1 -1
  10. package/dist/{components-vHfxV2PB.js → components-bbanLd1u.js} +499 -263
  11. package/dist/components-bbanLd1u.js.map +1 -0
  12. package/dist/composables/autoGroup/classify.d.ts +7 -1
  13. package/dist/composables/autoGroup/columns.d.ts +16 -1
  14. package/dist/composables/autoGroup/index.d.ts +5 -7
  15. package/dist/composables/autoGroup/tokenize.d.ts +36 -1
  16. package/dist/composables/index.js +2 -2
  17. package/dist/composables/useAutoGroup.d.ts +13 -2
  18. package/dist/{composables-D6uqp0UF.js → composables-BbHozlA7.js} +2 -2
  19. package/dist/{composables-D6uqp0UF.js.map → composables-BbHozlA7.js.map} +1 -1
  20. package/dist/index.js +3 -3
  21. package/dist/install.js +1 -1
  22. package/dist/permissions.js +2 -2
  23. package/dist/permissions.js.map +1 -1
  24. package/dist/styles.css +71 -82
  25. package/dist/types/auto-group.d.ts +1 -1
  26. package/dist/{validation-Dmp9Q5W0.js → validation-CukfLjy-.js} +323 -187
  27. package/dist/validation-CukfLjy-.js.map +1 -0
  28. package/package.json +2 -2
  29. package/src/__tests__/components/AutoGroupModal.anatomy.test.ts +86 -0
  30. package/src/__tests__/components/AutoGroupModal.wiring.test.ts +53 -0
  31. package/src/__tests__/components/SampleSelector.test.ts +14 -16
  32. package/src/__tests__/components/SmartGroupFieldRecipe.groups.test.ts +23 -0
  33. package/src/__tests__/components/SmartGroupFieldRecipe.test.ts +176 -0
  34. package/src/__tests__/composables/autoGroup/columns.test.ts +100 -11
  35. package/src/__tests__/composables/autoGroup/fieldEdits.test.ts +74 -0
  36. package/src/__tests__/composables/autoGroup/fingerprint.test.ts +9 -0
  37. package/src/__tests__/composables/autoGroup/golden.test.ts +187 -0
  38. package/src/__tests__/composables/autoGroup/groupByOrder.test.ts +7 -7
  39. package/src/__tests__/composables/autoGroup/tokenLength.test.ts +7 -0
  40. package/src/__tests__/composables/autoGroup/tokenize.test.ts +78 -1
  41. package/src/__tests__/composables/useAutoGroup.test.ts +2 -1
  42. package/src/__tests__/fixtures/auto-group/share-exp021.txt +103 -0
  43. package/src/__tests__/fixtures/auto-group/share-exp031.txt +39 -0
  44. package/src/__tests__/fixtures/auto-group/share-exp114.txt +123 -0
  45. package/src/__tests__/fixtures/auto-group/share-exp214.txt +63 -0
  46. package/src/__tests__/utils/permissions.test.ts +15 -0
  47. package/src/components/AutoGroupModal.adapter.ts +80 -9
  48. package/src/components/AutoGroupModal.vue +41 -8
  49. package/src/components/SmartGroup.types.ts +23 -0
  50. package/src/components/SmartGroupFieldRecipe.groups.ts +14 -2
  51. package/src/components/SmartGroupFieldRecipe.vue +289 -169
  52. package/src/composables/autoGroup/classify.ts +26 -7
  53. package/src/composables/autoGroup/columns.ts +144 -36
  54. package/src/composables/autoGroup/compose.ts +8 -10
  55. package/src/composables/autoGroup/fingerprint.ts +1 -1
  56. package/src/composables/autoGroup/index.ts +5 -12
  57. package/src/composables/autoGroup/tokenLength.ts +8 -0
  58. package/src/composables/autoGroup/tokenize.ts +148 -1
  59. package/src/composables/autoGroup/vocab.json +5 -11
  60. package/src/composables/useAutoGroup.ts +92 -79
  61. package/src/permissions.ts +3 -2
  62. package/src/styles/components/smart-group.css +71 -82
  63. package/src/types/auto-group.ts +1 -0
  64. package/dist/components-vHfxV2PB.js.map +0 -1
  65. package/dist/composables/autoGroup/replicatePreGroup.d.ts +0 -36
  66. package/dist/validation-Dmp9Q5W0.js.map +0 -1
  67. package/src/composables/autoGroup/replicatePreGroup.ts +0 -124
@@ -0,0 +1,187 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { readFileSync } from 'node:fs'
3
+ import { resolve } from 'node:path'
4
+ import { useAutoGroup } from '../../../composables/useAutoGroup'
5
+
6
+ // End-to-end defaults of the paste path: sample names in, group names and sizes
7
+ // out. Fixtures `share-*.txt` are real batches from the lab's mass-spec share.
8
+
9
+ function engineFor(samples: string[]) {
10
+ const engine = useAutoGroup()
11
+ engine.rawText.value = samples.join('\n')
12
+ engine.parseInput()
13
+ return engine
14
+ }
15
+
16
+ function sizes(groups: { name: string; samples: string[] }[] | undefined): Record<string, number> {
17
+ return Object.fromEntries((groups ?? []).map(group => [group.name, group.samples.length]))
18
+ }
19
+
20
+ const experimental = (samples: string[]) => sizes(engineFor(samples).result.value.experimentalGroups)
21
+ const qc = (samples: string[]) => sizes(engineFor(samples).result.value.qcGroups)
22
+
23
+ function fixture(name: string): string[] {
24
+ return readFileSync(resolve(__dirname, `../../fixtures/auto-group/${name}.txt`), 'utf-8').trim().split('\n')
25
+ }
26
+
27
+ const cross = (a: string[], b: string[], c: string[], sep: string): string[] =>
28
+ a.flatMap(x => b.flatMap(y => c.map(z => [x, y, z].join(sep))))
29
+
30
+ const liverBatch = [
31
+ 'Ctrl_Liver_D1_Rep1', 'Ctrl_Liver_D1_Rep2',
32
+ 'Treat_Liver_D3_Rep1', 'Treat_Liver_D3_Rep2',
33
+ 'Batch_IQC_Rep1', 'Batch_IQC_Rep2', 'Batch_IQC_Rep3',
34
+ ]
35
+
36
+ const tracingBatch = [
37
+ 'LT_13102025_212_WT_Glu_3_20_S_091123_T1_33', 'LT_13102025_213_WT_Glu_3_20_S_091123_T1_34',
38
+ 'LT_13102025_214_KI_Glu_3_20_S_091123_T1_35', 'LT_13102025_215_KI_Glu_6_40_L_091123_T1_36',
39
+ 'LT_13102025_216_WT_Glu_6_40_L_091123_T1_37', 'LT_13102025_217_WT_Glu_6_40_L_091123_T2_38',
40
+ 'LT_13102025_218_KI_Glu_3_20_S_091123_T2_39', 'LT_13102025_219_KI_Glu_6_40_L_091123_T2_40',
41
+ 'LT_13102025_220_WT_Glu_3_20_S_091123_T2_41', 'LT_13102025_221_WT_Glu_6_40_L_091123_T2_42',
42
+ 'LT_13102025_EQC_Jurkat_1_2', 'LT_13102025_EQC_Jurkat_2_8', 'LT_13102025_IQC_Pool_1_5',
43
+ 'test_EQC_Jurkat_02', 'test_Blank_Run_01',
44
+ ]
45
+
46
+ const twoMatrices = [
47
+ 'Ctrl_Liver_D1_Rep1', 'Ctrl_Liver_D1_Rep2', 'Treat_Liver_D3_Rep1', 'Treat_Liver_D3_Rep2',
48
+ 'QC_Pool', 'Blank', 'ISTD_Mix',
49
+ 'Ctrl_Brain_D1_Rep1', 'Ctrl_Brain_D1_Rep2',
50
+ ]
51
+
52
+ describe('auto-group defaults: replicates', () => {
53
+ it('should keep a trailing counter out of the group key when it repeats', () => {
54
+ expect(experimental(cross(['WT', 'KO'], ['Vehicle', 'Drug'], ['1', '2', '3'], '-'))).toEqual({
55
+ 'WT / Vehicle': 3, 'WT / Drug': 3, 'KO / Vehicle': 3, 'KO / Drug': 3,
56
+ })
57
+ })
58
+
59
+ it('should keep a trailing letter counter out of the group key when it repeats', () => {
60
+ expect(experimental(cross(['WT', 'KO'], ['Vehicle', 'Drug'], ['A', 'B', 'C'], '_'))).toEqual({
61
+ 'WT / Vehicle': 3, 'WT / Drug': 3, 'KO / Vehicle': 3, 'KO / Drug': 3,
62
+ })
63
+ })
64
+
65
+ it('should keep sample numbers, replicate tokens and injection numbers out of the group key when a name carries all three', () => {
66
+ expect(experimental(tracingBatch)).toEqual({
67
+ 'WT / 3 / 20 / S': 3, 'KI / 3 / 20 / S': 2, 'KI / 6 / 40 / L': 2, 'WT / 6 / 40 / L': 3,
68
+ })
69
+ })
70
+
71
+ it('should report original sample names in metadata when replicates collapse', () => {
72
+ expect(engineFor(liverBatch).result.value.metadata.map(row => row.sampleName)).toEqual(liverBatch)
73
+ })
74
+
75
+ it('should report original sample names as excluded when a class is excluded', () => {
76
+ const engine = engineFor(liverBatch)
77
+ engine.setClassDispositions(c => c.kind === 'iqc', 'exclude')
78
+ expect(engine.result.value.excludedSamples).toEqual(['Batch_IQC_Rep1', 'Batch_IQC_Rep2', 'Batch_IQC_Rep3'])
79
+ })
80
+
81
+ it('should count each QC replicate as a class member when QC names differ only by replicate', () => {
82
+ const iqc = engineFor(liverBatch).classes.value.find(c => c.kind === 'iqc')
83
+ expect(iqc?.members.length).toBe(3)
84
+ })
85
+
86
+ it('should fill each pre-filled template row with the tokens of its own sample when replicates collapse', () => {
87
+ const { content } = engineFor(liverBatch).composeTemplateForTest({ mode: 'prefilled', format: 'csv' })
88
+ const row = content.split('\n').find(line => line.startsWith('Treat_Liver_D3_Rep1,'))
89
+ expect(row).toContain('Treat / D3')
90
+ })
91
+ })
92
+
93
+ describe('auto-group defaults: classes', () => {
94
+ it('should route bare QC and ISTD tokens to overlay classes when a name carries them', () => {
95
+ expect(Object.keys(qc(twoMatrices)).sort()).toEqual(['Blank', 'IQC', 'Standard'])
96
+ })
97
+
98
+ it('should start each group name with the class layer when the batch has two matrices', () => {
99
+ expect(experimental(twoMatrices)).toEqual({ 'Liver / Ctrl / D1': 2, 'Liver / Treat / D3': 2, 'Brain': 2 })
100
+ })
101
+
102
+ it('should keep one overlay group when a QC class has two token lengths', () => {
103
+ expect(qc(tracingBatch)).toEqual({ EQC: 3, IQC: 1, Blank: 1 })
104
+ })
105
+
106
+ it('should not name a column Username when it holds two genotypes', () => {
107
+ const engine = engineFor(cross(['WT', 'KO'], ['Vehicle', 'Drug'], ['1', '2', '3'], '-'))
108
+ expect(engine.activeSchema.value?.columns.map(column => column.name)).not.toContain('Username')
109
+ })
110
+ })
111
+
112
+ describe('auto-group defaults: tokenizer', () => {
113
+ const pairs = { WT: 2, KO: 2 }
114
+
115
+ it('should drop a file extension when all names share it', () => {
116
+ expect(experimental(['WT_Ctrl_01.raw', 'WT_Ctrl_02.raw', 'KO_Ctrl_03.raw', 'KO_Ctrl_04.raw'])).toEqual(pairs)
117
+ })
118
+
119
+ it('should split on spaces when no other delimiter is present', () => {
120
+ expect(experimental(['WT Ctrl 1', 'WT Ctrl 2', 'KO Ctrl 1', 'KO Ctrl 2'])).toEqual(pairs)
121
+ })
122
+
123
+ it('should read one delimiter when it repeats', () => {
124
+ expect(experimental(['WT__Ctrl_1', 'WT__Ctrl_2', 'KO__Ctrl_1', 'KO__Ctrl_2'])).toEqual(pairs)
125
+ })
126
+
127
+ it('should report the experiment header when all names share it', () => {
128
+ expect(engineFor(fixture('share-exp031')).experiment.value).toEqual({
129
+ code: 'exp031', date: '2025-03-18', operator: 'SU',
130
+ })
131
+ })
132
+ })
133
+
134
+ describe('auto-group defaults: lab share batches', () => {
135
+ it('should group exp031 into 8 conditions when the replicate is a letter', () => {
136
+ expect(experimental(fixture('share-exp031'))).toEqual({
137
+ '105 / DMSO': 4, '105 / TFP': 4, '270 / DMSO': 4, '270 / TFP': 4,
138
+ 'SF8628 / DMSO': 4, 'SF8628 / TFP': 4, 'SU-VI / DMSO': 4, 'SU-VI / TFP': 4,
139
+ })
140
+ })
141
+
142
+ it('should group exp114 by condition when a token holds condition and replicate', () => {
143
+ expect(experimental(fixture('share-exp114'))['HMW / 2']).toBe(3)
144
+ })
145
+
146
+ it('should group exp214 by stem when a counter is attached to it', () => {
147
+ expect(experimental(fixture('share-exp214'))['D1 / A']).toBe(3)
148
+ })
149
+
150
+ it('should group exp021 by time point when it has more than five levels', () => {
151
+ expect(Object.keys(experimental(fixture('share-exp021')))).toContain('inf2 / 1 / 0min')
152
+ })
153
+
154
+ it('should keep the label token B13 as a column when exp021 is parsed', () => {
155
+ const schemas = Object.values(engineFor(fixture('share-exp021')).schemas.value)
156
+ expect(schemas.flatMap(schema => schema.columns.flatMap(column => column.uniqueValues))).toContain('B13')
157
+ })
158
+
159
+ it('should read no QC class when the word is in the experiment title that all names share', () => {
160
+ const names = cross(['WT', 'KO'], ['Vehicle', 'Drug'], ['1', '2', '3'], '_')
161
+ .map(body => `exp137_250919_AK_betaine_std_HILIC_${body}`)
162
+ expect(qc(names)).toEqual({})
163
+ })
164
+ })
165
+
166
+ describe('auto-group defaults: second review', () => {
167
+ it('should keep a QC-only batch as one overlay when QC sits where the operator is expected', () => {
168
+ expect(qc(['exp021_250101_QC_1', 'exp021_250101_QC_2'])).toEqual({ IQC: 2 })
169
+ })
170
+
171
+ it('should keep a QC-only batch as one overlay when all names share the QC token after the header', () => {
172
+ expect(qc(['exp021_250101_SU_QC_1', 'exp021_250101_SU_QC_2'])).toEqual({ IQC: 2 })
173
+ })
174
+
175
+ it('should group by a bare-number dose when it is the last token of three samples', () => {
176
+ expect(experimental(['Drug_1', 'Drug_5', 'Drug_10'])).toEqual({ '1': 1, '5': 1, '10': 1 })
177
+ })
178
+
179
+ it('should drop known file extensions when the names carry two different ones', () => {
180
+ expect(experimental(['WT_Ctrl_1.raw', 'WT_Ctrl_2.mzML', 'KO_Ctrl_1.raw', 'KO_Ctrl_2.mzML'])).toEqual({ WT: 2, KO: 2 })
181
+ })
182
+
183
+ it('should leave a letter counter out of the group key when it sits before an unpadded injection number', () => {
184
+ const names = cross(['WT', 'KO'], ['A', 'B', 'C'], ['x'], '_').map((name, i) => name.replace('_x', `_${i + 5}`))
185
+ expect(experimental(names)).toEqual({ WT: 3, KO: 3 })
186
+ })
187
+ })
@@ -111,7 +111,7 @@ describe('useAutoGroup — field edits span every class exposing the field', ()
111
111
 
112
112
  it('should reorder the layer in every class when moving a field by name', () => {
113
113
  const auto = twoClassEngine()
114
- auto.moveGroupByField('Token 3', 'up')
114
+ auto.moveGroupByField('Field 3', 'up')
115
115
  expect(Object.values(auto.schemas.value).map(s => s.groupBy)).toEqual([[2, 1], [2, 1]])
116
116
  })
117
117
 
@@ -120,7 +120,7 @@ describe('useAutoGroup — field edits span every class exposing the field', ()
120
120
  const activeKey = auto.activeClassKey.value
121
121
  auto.toggleGroupBy(activeKey, 2)
122
122
 
123
- auto.moveGroupByField('Token 3', 'up')
123
+ auto.moveGroupByField('Field 3', 'up')
124
124
 
125
125
  expect(Object.values(auto.schemas.value).map(schema => schema.groupBy)).toEqual([[1], [2, 1]])
126
126
  })
@@ -131,7 +131,7 @@ describe('useAutoGroup — field edits span every class exposing the field', ()
131
131
  auto.moveGroupBy(inactiveKey, 3, 'up')
132
132
  auto.moveGroupBy(inactiveKey, 3, 'up')
133
133
 
134
- auto.moveGroupByField('Token 4', 'up')
134
+ auto.moveGroupByField('Field 4', 'up')
135
135
 
136
136
  expect(Object.values(auto.schemas.value).map(schema => schema.groupBy)).toEqual([
137
137
  [1, 3, 2],
@@ -162,14 +162,14 @@ describe('useAutoGroup — field edits span every class exposing the field', ()
162
162
  },
163
163
  }
164
164
 
165
- auto.moveGroupByField('Token 4', 'up')
165
+ auto.moveGroupByField('Field 4', 'up')
166
166
 
167
167
  expect(auto.schemas.value[inactiveKey].groupBy).toEqual([1, 3, 2, 99])
168
168
  })
169
169
 
170
170
  it('should compose consistently ordered names across classes after a field move', () => {
171
171
  const auto = twoClassEngine()
172
- auto.moveGroupByField('Token 3', 'up')
172
+ auto.moveGroupByField('Field 3', 'up')
173
173
  expect(auto.groups.value.map(g => g.name).sort()).toEqual([
174
174
  'Kidney / High / 10min',
175
175
  'Kidney / High / 5min',
@@ -218,8 +218,8 @@ describe('useAutoGroup — field edits span every class exposing the field', ()
218
218
  },
219
219
  }
220
220
 
221
- auto.toggleGroupByField('Token 3')
222
- auto.toggleGroupByField('Token 3')
221
+ auto.toggleGroupByField('Field 3')
222
+ auto.toggleGroupByField('Field 3')
223
223
 
224
224
  expect(Object.values(auto.schemas.value).map(schema => schema.groupBy)).toEqual([[1], [1, 2]])
225
225
  })
@@ -83,3 +83,10 @@ describe('splitByTokenLength', () => {
83
83
  expect(out.map(c => c.label)).toEqual(['Unknown', 'Biological / Plasma'])
84
84
  })
85
85
  })
86
+
87
+ describe('splitByTokenLength for QC-kind classes', () => {
88
+ it('should keep a QC-kind class whole when its members have two token lengths', () => {
89
+ const iqc = cls({ kind: 'iqc', label: 'IQC', subKind: undefined, members: [0, 1] })
90
+ expect(splitByTokenLength([iqc], [['IQC', '1'], ['x', 'IQC', '2']])).toHaveLength(1)
91
+ })
92
+ })
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest'
2
- import { splitMulti, pickPrimaryDelimiter } from '../../../composables/autoGroup/tokenize'
2
+ import { splitMulti, pickPrimaryDelimiter, parseDateToken, tokenizeBatch } from '../../../composables/autoGroup/tokenize'
3
3
 
4
4
  describe('pickPrimaryDelimiter', () => {
5
5
  it('should pick underscore when underscore field counts are most consistent', () => {
@@ -31,3 +31,80 @@ describe('splitMulti', () => {
31
31
  expect(splitMulti('', '_')).toEqual([''])
32
32
  })
33
33
  })
34
+
35
+ describe('parseDateToken', () => {
36
+ it('should read the date when the token is YYMMDD', () => {
37
+ expect(parseDateToken('250919')).toBe('2025-09-19')
38
+ })
39
+
40
+ it('should read the date when the token is YYYYMMDD', () => {
41
+ expect(parseDateToken('20250211')).toBe('2025-02-11')
42
+ })
43
+
44
+ it('should read DDMMYYYY when the first four digits are not a year', () => {
45
+ expect(parseDateToken('13102025')).toBe('2025-10-13')
46
+ })
47
+
48
+ it('should read DDMMYY when the year-first reading is outside the plausible window', () => {
49
+ expect(parseDateToken('091123')).toBe('2023-11-09')
50
+ })
51
+
52
+ it('should return undefined when the token is not a date', () => {
53
+ expect(parseDateToken('999999')).toBeUndefined()
54
+ })
55
+ })
56
+
57
+ describe('tokenizeBatch', () => {
58
+ it('should move the header out of the rows when all names share it', () => {
59
+ const batch = tokenizeBatch(['exp031_250318_SU_105_DMSO_A_23', 'exp031_250318_SU_270_TFP_B_09'])
60
+ expect(batch.rows).toEqual([['105', 'DMSO', 'A', '23'], ['270', 'TFP', 'B', '09']])
61
+ })
62
+
63
+ it('should read the header when the operator is before the date', () => {
64
+ const batch = tokenizeBatch(['exp157_LT_13102025_214_KI_13', 'exp157_LT_13102025_215_WT_35'])
65
+ expect(batch.experiment).toEqual({ code: 'exp157', date: '2025-10-13', operator: 'LT' })
66
+ })
67
+
68
+ it('should keep a header field in the row when the batch has two values', () => {
69
+ const batch = tokenizeBatch(['exp280_260413_YS_Plasma_M10_031', 'exp281_260413_YS_Tissues_M10_085'])
70
+ expect(batch.rows[0]).toEqual(['exp280', 'Plasma', 'M10', '031'])
71
+ })
72
+
73
+ it('should read no header when a name has no experiment code and no date', () => {
74
+ expect(tokenizeBatch(['WT_Ctrl_1', 'KO_Ctrl_2']).rows[0]).toEqual(['WT', 'Ctrl', '1'])
75
+ })
76
+
77
+ it('should drop the last token when it is a 14-digit acquisition stamp', () => {
78
+ const batch = tokenizeBatch(['WT_blank_011_20251003200301', 'KO_blank_012'])
79
+ expect(batch.rows[0]).toEqual(['WT', 'blank', '011'])
80
+ })
81
+
82
+ it('should count the shared tokens when all names share a title after the header', () => {
83
+ const batch = tokenizeBatch(['exp114_250730_YY_HEK_Put_HMW_1-1_35', 'exp114_250730_YY_HEK_Put_MMW_2-1_36'])
84
+ expect(batch.sharedPrefix).toBe(2)
85
+ })
86
+
87
+ it('should keep a decimal point inside a token when the delimiter is an underscore', () => {
88
+ expect(tokenizeBatch(['exp_drugA_0.1uM', 'exp_drugB_1uM']).rows[0]).toEqual(['exp', 'drugA', '0.1uM'])
89
+ })
90
+ })
91
+
92
+ describe('tokenizeBatch review regressions', () => {
93
+ it('should keep a caps token in the row when only some names carry it after the date', () => {
94
+ const batch = tokenizeBatch(['exp021_250101_QC_1', 'exp021_250101_QC_2', 'exp021_250101_Ctrl_1', 'exp021_250101_Ctrl_2'])
95
+ expect(batch.rows[0]).toEqual(['QC', '1'])
96
+ })
97
+
98
+ it('should keep rows aligned when one genotype looks like operator initials', () => {
99
+ const batch = tokenizeBatch(['exp021_250101_WT_1', 'exp021_250101_WT_2', 'exp021_250101_Het_1', 'exp021_250101_Het_2'])
100
+ expect(batch.rows.map(row => row.length)).toEqual([2, 2, 2, 2])
101
+ })
102
+
103
+ it('should count no shared prefix when the names have no experiment header', () => {
104
+ expect(tokenizeBatch(['QC_01', 'QC_02', 'QC_03']).sharedPrefix).toBe(0)
105
+ })
106
+
107
+ it('should reject a date that is not in the calendar when the token has six digits', () => {
108
+ expect(parseDateToken('250231')).toBeUndefined()
109
+ })
110
+ })
@@ -346,8 +346,9 @@ describe('useAutoGroup auto-disable degenerate columns', () => {
346
346
  ].join('\n')
347
347
  auto.parseInput()
348
348
 
349
+ // Replicate columns are visible fields too, but they are off by default.
349
350
  const goodCols = auto.fields.value.filter(
350
- f => f.cardinality > 1 && f.cardinality < auto.samples.value.length,
351
+ f => f.role !== 'replicate' && f.cardinality > 1 && f.cardinality < auto.samples.value.length,
351
352
  )
352
353
  for (const col of goodCols) {
353
354
  expect(auto.enabledFields.value.has(col.index)).toBe(true)
@@ -0,0 +1,103 @@
1
+ exp021_20250305_AK_blank_001
2
+ exp021_20250305_AK_blank_005
3
+ exp021_20250305_AK_blank_009
4
+ exp021_20250305_AK_blank_013
5
+ exp021_20250305_AK_blank_017
6
+ exp021_20250305_AK_blank_021
7
+ exp021_20250305_AK_blank_025
8
+ exp021_20250305_AK_blank_029
9
+ exp021_20250305_AK_blank_033
10
+ exp021_20250305_AK_blank_037
11
+ exp021_20250305_AK_blank_041
12
+ exp021_20250305_AK_blank_045
13
+ exp021_20250305_AK_blank_049
14
+ exp021_20250305_AK_blank_053
15
+ exp021_20250305_AK_blank_057
16
+ exp021_20250305_AK_blank_061
17
+ exp021_20250305_AK_blank_065
18
+ exp021_20250305_AK_blank_066
19
+ exp021_20250305_AK_ksEx5_inf2_B13_1_0min_022
20
+ exp021_20250305_AK_ksEx5_inf2_B13_1_120min_052
21
+ exp021_20250305_AK_ksEx5_inf2_B13_1_20min_042
22
+ exp021_20250305_AK_ksEx5_inf2_B13_1_240min_015
23
+ exp021_20250305_AK_ksEx5_inf2_B13_1_360min_031
24
+ exp021_20250305_AK_ksEx5_inf2_B13_1_40min_059
25
+ exp021_20250305_AK_ksEx5_inf2_B13_2_0min_050
26
+ exp021_20250305_AK_ksEx5_inf2_B13_2_120min_016
27
+ exp021_20250305_AK_ksEx5_inf2_B13_2_20min_006
28
+ exp021_20250305_AK_ksEx5_inf2_B13_2_240min_035
29
+ exp021_20250305_AK_ksEx5_inf2_B13_2_360min_038
30
+ exp021_20250305_AK_ksEx5_inf2_B13_2_40min_055
31
+ exp021_20250305_AK_ksEx5_inf2_B13_6_0min_036
32
+ exp021_20250305_AK_ksEx5_inf2_B13_6_120min_002
33
+ exp021_20250305_AK_ksEx5_inf2_B13_6_20min_034
34
+ exp021_20250305_AK_ksEx5_inf2_B13_6_240min_039
35
+ exp021_20250305_AK_ksEx5_inf2_B13_6_360min_010
36
+ exp021_20250305_AK_ksEx5_inf2_B13_6_40min_043
37
+ exp021_20250305_AK_ksEx5_inf2_B13_7_0min_062
38
+ exp021_20250305_AK_ksEx5_inf2_B13_7_120min_028
39
+ exp021_20250305_AK_ksEx5_inf2_B13_7_20min_058
40
+ exp021_20250305_AK_ksEx5_inf2_B13_7_240min_018
41
+ exp021_20250305_AK_ksEx5_inf2_B13_7_360min_026
42
+ exp021_20250305_AK_ksEx5_inf2_B13_7_40min_060
43
+ exp021_20250305_AK_ksEx5_inf3_B13_1_0min_019
44
+ exp021_20250305_AK_ksEx5_inf3_B13_1_120min_032
45
+ exp021_20250305_AK_ksEx5_inf3_B13_1_20min_064
46
+ exp021_20250305_AK_ksEx5_inf3_B13_1_240min_024
47
+ exp021_20250305_AK_ksEx5_inf3_B13_1_360min_014
48
+ exp021_20250305_AK_ksEx5_inf3_B13_1_40min_040
49
+ exp021_20250305_AK_ksEx5_inf3_B13_2_0min_063
50
+ exp021_20250305_AK_ksEx5_inf3_B13_2_120min_012
51
+ exp021_20250305_AK_ksEx5_inf3_B13_2_20min_047
52
+ exp021_20250305_AK_ksEx5_inf3_B13_2_240min_007
53
+ exp021_20250305_AK_ksEx5_inf3_B13_2_360min_023
54
+ exp021_20250305_AK_ksEx5_inf3_B13_2_40min_003
55
+ exp021_20250305_AK_ksEx5_inf3_B13_6_0min_004
56
+ exp021_20250305_AK_ksEx5_inf3_B13_6_120min_044
57
+ exp021_20250305_AK_ksEx5_inf3_B13_6_20min_030
58
+ exp021_20250305_AK_ksEx5_inf3_B13_6_240min_048
59
+ exp021_20250305_AK_ksEx5_inf3_B13_6_360min_011
60
+ exp021_20250305_AK_ksEx5_inf3_B13_6_40min_054
61
+ exp021_20250305_AK_ksEx5_inf3_B13_7_0min_046
62
+ exp021_20250305_AK_ksEx5_inf3_B13_7_120min_020
63
+ exp021_20250305_AK_ksEx5_inf3_B13_7_20min_056
64
+ exp021_20250305_AK_ksEx5_inf3_B13_7_240min_008
65
+ exp021_20250305_AK_ksEx5_inf3_B13_7_360min_051
66
+ exp021_20250305_AK_ksEx5_inf3_B13_7_40min_027
67
+ ksEx5_inf2_B13_1_120min
68
+ ksEx5_inf2_S13_8_120min
69
+ ksEx5_inf2_S13_8_120min__MS1_AGC100_70-1000_02
70
+ ksEx5_inf2_S13_8_120min_MS1_AGC100_70-500_03
71
+ ksEx5_inf2_S13_8_120min_MS1_AGC100_70-500_shape_g_04
72
+ ksEx5_inf2_S13_8_120min_MS1_AGC100_70-500_stead_g_05
73
+ ksEx7a_Betaine_10nM_std_in_442
74
+ ksEx7a_Betaine_Valine_Mix_10nM_std_in_442
75
+ ksEx7a_Blank_442
76
+ ksEx7a_FullLabBetaine_10nM_std_in_442
77
+ ksEx7a_FullLabBetaine_Valine_Mix_10nM_std_in_442
78
+ ksEx7a_Valine_10nM_std_in_442
79
+ ksEx7b_Betaine_10nM_std_in_442_MS1_AGC100_70-1000_02
80
+ ksEx7b_Betaine_10nM_std_in_442_MS1_AGC100_70-500_03
81
+ ksEx7b_Betaine_10nM_std_in_442_MS1_AGC100_70-500_shape_g_04
82
+ ksEx7b_Betaine_10nM_std_in_442_MS1_AGC100_70-500_stead_g_05
83
+ ksEx7b_Betaine_Valine_Mix_10nM_std_in_442__MS1_AGC100_70-1000_02
84
+ ksEx7b_Betaine_Valine_Mix_10nM_std_in_442_MS1_AGC100_70-500_03
85
+ ksEx7b_Betaine_Valine_Mix_10nM_std_in_442_MS1_AGC100_70-500_shape_g_04
86
+ ksEx7b_Betaine_Valine_Mix_10nM_std_in_442_MS1_AGC100_70-500_stead_g_05
87
+ ksEx7b_Blank_442_02
88
+ ksEx7b_Blank_442_03
89
+ ksEx7b_Blank_442_04
90
+ ksEx7b_Blank_442_05
91
+ ksEx7b_Blank_442_06
92
+ ksEx7b_FullLabBetaine_10nM_std_in_442__MS1_AGC100_70-1000_02
93
+ ksEx7b_FullLabBetaine_10nM_std_in_442_MS1_AGC100_70-500_03
94
+ ksEx7b_FullLabBetaine_10nM_std_in_442_MS1_AGC100_70-500_shape_g_04
95
+ ksEx7b_FullLabBetaine_10nM_std_in_442_MS1_AGC100_70-500_stead_g_05
96
+ ksEx7b_FullLabBetaine_Valine_Mix_10nM_std_in_442__MS1_AGC100_70-1000_02
97
+ ksEx7b_FullLabBetaine_Valine_Mix_10nM_std_in_442_MS1_AGC100_70-500_03
98
+ ksEx7b_FullLabBetaine_Valine_Mix_10nM_std_in_442_MS1_AGC100_70-500_shape_g_04
99
+ ksEx7b_FullLabBetaine_Valine_Mix_10nM_std_in_442_MS1_AGC100_70-500_stead_g_05
100
+ ksEx7b_Valine_10nM_std_in_442__MS1_AGC100_70-1000_02
101
+ ksEx7b_Valine_10nM_std_in_442_MS1_AGC100_70-500_03
102
+ ksEx7b_Valine_10nM_std_in_442_MS1_AGC100_70-500_shape_g_04
103
+ ksEx7b_Valine_10nM_std_in_442_MS1_AGC100_70-500_stead_g_05
@@ -0,0 +1,39 @@
1
+ exp031_250318_SU_105_media_DMSO_72_A_23
2
+ exp031_250318_SU_105_media_DMSO_72_B_17
3
+ exp031_250318_SU_105_media_DMSO_72_C_33
4
+ exp031_250318_SU_105_media_DMSO_72_D_02
5
+ exp031_250318_SU_105_media_TFP_72_A_20
6
+ exp031_250318_SU_105_media_TFP_72_B_34
7
+ exp031_250318_SU_105_media_TFP_72_C_30
8
+ exp031_250318_SU_105_media_TFP_72_D_18
9
+ exp031_250318_SU_270_media_DMSO_72_A_35
10
+ exp031_250318_SU_270_media_DMSO_72_B_11
11
+ exp031_250318_SU_270_media_DMSO_72_C_05
12
+ exp031_250318_SU_270_media_DMSO_72_D_37
13
+ exp031_250318_SU_270_media_TFP_72_A_09
14
+ exp031_250318_SU_270_media_TFP_72_B_31
15
+ exp031_250318_SU_270_media_TFP_72_C_13
16
+ exp031_250318_SU_270_media_TFP_72_D_21
17
+ exp031_250318_SU_blank_01
18
+ exp031_250318_SU_blank_06
19
+ exp031_250318_SU_blank_10
20
+ exp031_250318_SU_blank_12
21
+ exp031_250318_SU_blank_24
22
+ exp031_250318_SU_blank_32
23
+ exp031_250318_SU_blank_39
24
+ exp031_250318_SU_SF8628_media_DMSO_72_A_16
25
+ exp031_250318_SU_SF8628_media_DMSO_72_B_03
26
+ exp031_250318_SU_SF8628_media_DMSO_72_C_14
27
+ exp031_250318_SU_SF8628_media_DMSO_72_D_19
28
+ exp031_250318_SU_SF8628_media_TFP_72_A_28
29
+ exp031_250318_SU_SF8628_media_TFP_72_B_38
30
+ exp031_250318_SU_SF8628_media_TFP_72_C_08
31
+ exp031_250318_SU_SF8628_media_TFP_72_D_36
32
+ exp031_250318_SU_SU-VI_media_DMSO_72_A_26
33
+ exp031_250318_SU_SU-VI_media_DMSO_72_B_27
34
+ exp031_250318_SU_SU-VI_media_DMSO_72_C_29
35
+ exp031_250318_SU_SU-VI_media_DMSO_72_D_15
36
+ exp031_250318_SU_SU-VI_media_TFP_72_A_22
37
+ exp031_250318_SU_SU-VI_media_TFP_72_B_07
38
+ exp031_250318_SU_SU-VI_media_TFP_72_C_25
39
+ exp031_250318_SU_SU-VI_media_TFP_72_D_04
@@ -0,0 +1,123 @@
1
+ exp114_250730_YY_HEK_DFMO_Put_blank_01
2
+ exp114_250730_YY_HEK_DFMO_Put_blank_05
3
+ exp114_250730_YY_HEK_DFMO_Put_blank_05_b
4
+ exp114_250730_YY_HEK_DFMO_Put_blank_119
5
+ exp114_250730_YY_HEK_DFMO_Put_blank_120
6
+ exp114_250730_YY_HEK_DFMO_Put_blank_33
7
+ exp114_250730_YY_HEK_DFMO_Put_blank_58b_postCrash
8
+ exp114_250730_YY_HEK_DFMO_Put_blank_61
9
+ exp114_250730_YY_HEK_DFMO_Put_blank_90
10
+ exp114_250730_YY_HEK_DFMO_Put_DigestControl_91
11
+ exp114_250730_YY_HEK_DFMO_Put_HMW_1-1_35
12
+ exp114_250730_YY_HEK_DFMO_Put_HMW_1-2_44
13
+ exp114_250730_YY_HEK_DFMO_Put_HMW_1-3_52
14
+ exp114_250730_YY_HEK_DFMO_Put_HMW_2-1_36
15
+ exp114_250730_YY_HEK_DFMO_Put_HMW_2-2_45
16
+ exp114_250730_YY_HEK_DFMO_Put_HMW_2-3_53
17
+ exp114_250730_YY_HEK_DFMO_Put_HMW_3-1_37
18
+ exp114_250730_YY_HEK_DFMO_Put_HMW_3-2_46
19
+ exp114_250730_YY_HEK_DFMO_Put_HMW_3-3_54
20
+ exp114_250730_YY_HEK_DFMO_Put_HMW_4-1_09
21
+ exp114_250730_YY_HEK_DFMO_Put_HMW_4-1_38
22
+ exp114_250730_YY_HEK_DFMO_Put_HMW_4-2_47
23
+ exp114_250730_YY_HEK_DFMO_Put_HMW_4-3_55
24
+ exp114_250730_YY_HEK_DFMO_Put_HMW_5-1_39
25
+ exp114_250730_YY_HEK_DFMO_Put_HMW_5-2_48
26
+ exp114_250730_YY_HEK_DFMO_Put_HMW_5-3_56
27
+ exp114_250730_YY_HEK_DFMO_Put_HMW_6-1_40
28
+ exp114_250730_YY_HEK_DFMO_Put_HMW_6-2_49
29
+ exp114_250730_YY_HEK_DFMO_Put_HMW_6-3_57
30
+ exp114_250730_YY_HEK_DFMO_Put_HMW_7-1_41
31
+ exp114_250730_YY_HEK_DFMO_Put_HMW_7-2_50
32
+ exp114_250730_YY_HEK_DFMO_Put_HMW_7-3_rep2_89
33
+ exp114_250730_YY_HEK_DFMO_Put_HMW_8-1_42
34
+ exp114_250730_YY_HEK_DFMO_Put_HMW_8-3_59
35
+ exp114_250730_YY_HEK_DFMO_Put_HMW_8-3_59_20250731221931
36
+ exp114_250730_YY_HEK_DFMO_Put_HMW_9-1_43
37
+ exp114_250730_YY_HEK_DFMO_Put_HMW_9-2_51
38
+ exp114_250730_YY_HEK_DFMO_Put_HMW_9-3_60
39
+ exp114_250730_YY_HEK_DFMO_Put_HMW_DigestControl_34
40
+ exp114_250730_YY_HEK_DFMO_Put_MMW_1-1_63
41
+ exp114_250730_YY_HEK_DFMO_Put_MMW_1-2_72
42
+ exp114_250730_YY_HEK_DFMO_Put_MMW_1-3_80
43
+ exp114_250730_YY_HEK_DFMO_Put_MMW_2-1_64
44
+ exp114_250730_YY_HEK_DFMO_Put_MMW_2-2_73
45
+ exp114_250730_YY_HEK_DFMO_Put_MMW_2-3_81
46
+ exp114_250730_YY_HEK_DFMO_Put_MMW_3-1_65
47
+ exp114_250730_YY_HEK_DFMO_Put_MMW_3-2_74
48
+ exp114_250730_YY_HEK_DFMO_Put_MMW_3-3_82
49
+ exp114_250730_YY_HEK_DFMO_Put_MMW_4-1_66
50
+ exp114_250730_YY_HEK_DFMO_Put_MMW_4-3_83
51
+ exp114_250730_YY_HEK_DFMO_Put_MMW_5-1_67
52
+ exp114_250730_YY_HEK_DFMO_Put_MMW_5-2_75
53
+ exp114_250730_YY_HEK_DFMO_Put_MMW_5-3_84
54
+ exp114_250730_YY_HEK_DFMO_Put_MMW_6-1_68
55
+ exp114_250730_YY_HEK_DFMO_Put_MMW_6-2_76
56
+ exp114_250730_YY_HEK_DFMO_Put_MMW_6-3_85
57
+ exp114_250730_YY_HEK_DFMO_Put_MMW_7-1_69
58
+ exp114_250730_YY_HEK_DFMO_Put_MMW_7-2_77
59
+ exp114_250730_YY_HEK_DFMO_Put_MMW_7-3_86
60
+ exp114_250730_YY_HEK_DFMO_Put_MMW_8-1_70
61
+ exp114_250730_YY_HEK_DFMO_Put_MMW_8-2_78
62
+ exp114_250730_YY_HEK_DFMO_Put_MMW_8-3_87
63
+ exp114_250730_YY_HEK_DFMO_Put_MMW_9-1_71
64
+ exp114_250730_YY_HEK_DFMO_Put_MMW_9-2_79
65
+ exp114_250730_YY_HEK_DFMO_Put_MMW_9-3_88
66
+ exp114_250730_YY_HEK_DFMO_Put_MMW_DigestControl_62
67
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_1-1_06
68
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_1-1_250ng_03
69
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_1-1_500ng_04
70
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_1-1_92
71
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_1-2_101
72
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_1-2_15
73
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_1-3_110
74
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_1-3_24
75
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_2-1_07
76
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_2-1_93
77
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_2-2_102
78
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_2-2_16
79
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_2-3_111
80
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_2-3_25
81
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_3-1_08
82
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_3-1_94
83
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_3-2_103
84
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_3-2_17
85
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_3-3_112
86
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_3-3_26
87
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_4-1_09
88
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_4-1_95
89
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_4-2_104
90
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_4-2_18
91
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_4-3_113
92
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_4-3_27
93
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_5-1_10
94
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_5-1_96
95
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_5-2_105
96
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_5-2_19
97
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_5-3_114
98
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_5-3_28
99
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_6-1_11
100
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_6-1_97
101
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_6-2_106
102
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_6-2_20
103
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_6-3_115
104
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_6-3_29
105
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_7-1_12
106
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_7-1_98
107
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_7-2_107
108
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_7-2_21
109
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_7-3_116
110
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_7-3_30
111
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_8-1_13
112
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_8-1_99
113
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_8-2_108
114
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_8-2_22
115
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_8-3_117
116
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_8-3_31
117
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_9-1_100
118
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_9-1_14
119
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_9-2_109
120
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_9-2_23
121
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_9-3_118
122
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_9-3_32
123
+ exp114_250730_YY_HEK_DFMO_Put_tRNA_DigControl_02