@morscherlab/mint-sdk 1.0.0-rc.4 → 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 (304) 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/AppSidebar.vue.d.ts +3 -3
  41. package/dist/components/AppTopBar.navigation.d.ts +11 -0
  42. package/dist/components/BaseButton.vue.d.ts +1 -1
  43. package/dist/components/BaseCheckbox.vue.d.ts +1 -1
  44. package/dist/components/BaseInput.vue.d.ts +2 -2
  45. package/dist/components/BasePill.vue.d.ts +1 -1
  46. package/dist/components/BaseRadioGroup.vue.d.ts +1 -1
  47. package/dist/components/BaseSelect.vue.d.ts +1 -1
  48. package/dist/components/BaseSlider.vue.d.ts +2 -2
  49. package/dist/components/BaseTextarea.vue.d.ts +2 -2
  50. package/dist/components/BaseToggle.vue.d.ts +1 -1
  51. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
  52. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +1 -1
  53. package/dist/components/ColorSlider.vue.d.ts +2 -2
  54. package/dist/components/ConcentrationInput.vue.d.ts +2 -2
  55. package/dist/components/ControlWorkspaceView.vue.d.ts +3 -3
  56. package/dist/components/DatePicker.vue.d.ts +1 -1
  57. package/dist/components/DateTimePicker.vue.d.ts +2 -2
  58. package/dist/components/DoseCalculatorVolumeField.vue.d.ts +15 -0
  59. package/dist/components/DoseDesignWorkspaceView.vue.d.ts +1 -1
  60. package/dist/components/DropdownButton.vue.d.ts +1 -1
  61. package/dist/components/FileUploader.vue.d.ts +2 -2
  62. package/dist/components/FormulaInput.vue.d.ts +2 -2
  63. package/dist/components/IconButton.vue.d.ts +1 -1
  64. package/dist/components/LoadingSpinner.vue.d.ts +1 -1
  65. package/dist/components/MoleculeInput.vue.d.ts +2 -2
  66. package/dist/components/MultiSelect.vue.d.ts +1 -1
  67. package/dist/components/NumberInput.vue.d.ts +1 -1
  68. package/dist/components/PlateMapEditor.vue.d.ts +6 -6
  69. package/dist/components/PluginWorkspaceView.controls.d.ts +28 -0
  70. package/dist/components/PluginWorkspaceView.navigation.d.ts +29 -0
  71. package/dist/components/PluginWorkspaceView.props.d.ts +151 -0
  72. package/dist/components/PluginWorkspaceView.shell.d.ts +19 -0
  73. package/dist/components/PluginWorkspaceView.vue.d.ts +46 -195
  74. package/dist/components/ProgressBar.vue.d.ts +1 -1
  75. package/dist/components/ProtocolStep.presentation.d.ts +4 -0
  76. package/dist/components/ProtocolStepEditor.state.d.ts +18 -0
  77. package/dist/components/ProtocolStepParameterField.vue.d.ts +12 -0
  78. package/dist/components/ReagentList.presentation.d.ts +16 -0
  79. package/dist/components/ResourceCard.vue.d.ts +1 -1
  80. package/dist/components/SampleSelector.colors.d.ts +13 -0
  81. package/dist/components/SampleSelector.drag.d.ts +24 -0
  82. package/dist/components/SampleSelector.groups.d.ts +15 -0
  83. package/dist/components/SampleSelector.selection.d.ts +26 -0
  84. package/dist/components/SampleSelector.vue.d.ts +4 -1
  85. package/dist/components/SampleSelectorSampleRow.vue.d.ts +21 -0
  86. package/dist/components/SegmentedControl.vue.d.ts +1 -1
  87. package/dist/components/SequenceInput.vue.d.ts +2 -2
  88. package/dist/components/SequenceProgressBar.vue.d.ts +1 -1
  89. package/dist/components/SettingsModal.schema.d.ts +9 -0
  90. package/dist/components/StatusIndicator.vue.d.ts +1 -1
  91. package/dist/components/TagsInput.vue.d.ts +2 -2
  92. package/dist/components/TimePicker.vue.d.ts +2 -2
  93. package/dist/components/TimeRangeInput.vue.d.ts +1 -1
  94. package/dist/components/UnitInput.vue.d.ts +2 -2
  95. package/dist/components/WellPlate.colors.d.ts +9 -0
  96. package/dist/components/WellPlate.conditions.d.ts +26 -0
  97. package/dist/components/WellPlate.geometry.d.ts +23 -0
  98. package/dist/components/WellPlate.interaction.d.ts +71 -0
  99. package/dist/components/WellPlate.legend.d.ts +2 -0
  100. package/dist/components/WellPlate.rendering.d.ts +24 -0
  101. package/dist/components/WellPlate.sampleDrop.d.ts +8 -0
  102. package/dist/components/WellPlate.vue.d.ts +1 -1
  103. package/dist/components/index.js +2 -2
  104. package/dist/components/internal/ActionItemInternal.vue.d.ts +1 -1
  105. package/dist/components/internal/PlateMapEditorToolbarInternal.vue.d.ts +28 -0
  106. package/dist/{components-DafPc4rM.js → components-DtHA2bgp.js} +3754 -2991
  107. package/dist/components-DtHA2bgp.js.map +1 -0
  108. package/dist/composables/autoGroup/classKey.d.ts +4 -0
  109. package/dist/composables/autoGroup/classify.d.ts +28 -0
  110. package/dist/composables/autoGroup/colors.d.ts +2 -0
  111. package/dist/composables/autoGroup/columns.d.ts +10 -0
  112. package/dist/composables/autoGroup/compose.d.ts +8 -0
  113. package/dist/composables/autoGroup/cooccurrence.d.ts +2 -0
  114. package/dist/composables/autoGroup/csv-shim.d.ts +2 -0
  115. package/dist/composables/autoGroup/fingerprint.d.ts +3 -0
  116. package/dist/composables/autoGroup/index.d.ts +16 -0
  117. package/dist/composables/autoGroup/replicatePreGroup.d.ts +38 -0
  118. package/dist/composables/autoGroup/template.d.ts +15 -0
  119. package/dist/composables/autoGroup/tokenize.d.ts +8 -0
  120. package/dist/composables/autoGroupConstants.d.ts +1 -0
  121. package/dist/composables/autoGroupGrouping.d.ts +3 -0
  122. package/dist/composables/controlComponentBindings.d.ts +1 -1
  123. package/dist/composables/controlSchemaAdapters.d.ts +1 -1
  124. package/dist/composables/controlSchemaDoseDesign.d.ts +11 -0
  125. package/dist/composables/controlSchemaLayout.d.ts +1 -1
  126. package/dist/composables/controlSchemaModel.d.ts +5 -0
  127. package/dist/composables/controlSchemaNormalize.d.ts +1 -1
  128. package/dist/composables/controlSchemaTypes.d.ts +305 -0
  129. package/dist/composables/controlWorkspaceOptions.d.ts +1 -1
  130. package/dist/composables/formBuilderSchema.d.ts +18 -0
  131. package/dist/composables/index.js +3 -3
  132. package/dist/composables/pluginEndpointBuilder.d.ts +13 -0
  133. package/dist/composables/protocolTemplateCatalog.d.ts +26 -0
  134. package/dist/composables/useAutoGroup.d.ts +61 -74
  135. package/dist/composables/useAutoGroupInputSources.d.ts +32 -0
  136. package/dist/composables/useBioTemplateControls.d.ts +1 -1
  137. package/dist/composables/useBioTemplatePresetWorkspace.d.ts +1 -1
  138. package/dist/composables/useBioTemplateWorkspace.d.ts +1 -1
  139. package/dist/composables/useControlSchema.d.ts +4 -316
  140. package/dist/composables/useControlWorkspace.d.ts +1 -1
  141. package/dist/composables/useForm.d.ts +2 -33
  142. package/dist/composables/useFormBuilder.d.ts +2 -9
  143. package/dist/composables/useFormValidation.d.ts +34 -0
  144. package/dist/composables/usePluginClient.d.ts +1 -4
  145. package/dist/composables/useProtocolTemplates.d.ts +2 -24
  146. package/dist/composables/useScheduleCalendarLayout.d.ts +49 -0
  147. package/dist/{composables-BMkPQhVK.js → composables-Dlg8jenH.js} +33 -31
  148. package/dist/composables-Dlg8jenH.js.map +1 -0
  149. package/dist/index.js +4 -4
  150. package/dist/install.js +2 -2
  151. package/dist/styles.css +547 -516
  152. package/dist/templates/controlSchemaTypes.d.ts +1 -1
  153. package/dist/templates/index.js +1 -1
  154. package/dist/templates/templateAdapterTypes.d.ts +48 -0
  155. package/dist/templates/templateCreateOptions.d.ts +165 -0
  156. package/dist/templates/templateQpcrTypes.d.ts +42 -0
  157. package/dist/templates/types.d.ts +5 -250
  158. package/dist/{templates-bUAWMn5L.js → templates-DtdUvJ4c.js} +144 -136
  159. package/dist/templates-DtdUvJ4c.js.map +1 -0
  160. package/dist/types/auto-group.d.ts +79 -9
  161. package/dist/types/componentLabTypes.d.ts +161 -0
  162. package/dist/types/componentWorkflowTypes.d.ts +150 -0
  163. package/dist/types/components.d.ts +2 -311
  164. package/dist/{useProtocolTemplates-QZtHFFH2.js → useProtocolTemplates-Bm5vyH4_.js} +1220 -454
  165. package/dist/useProtocolTemplates-Bm5vyH4_.js.map +1 -0
  166. package/package.json +1 -1
  167. package/src/__tests__/components/AppTopBar.navigation.test.ts +70 -0
  168. package/src/__tests__/components/DoseCalculatorVolumeField.test.ts +53 -0
  169. package/src/__tests__/components/PlateMapEditorToolbarInternal.test.ts +54 -0
  170. package/src/__tests__/components/PluginWorkspaceView.controls.test.ts +156 -0
  171. package/src/__tests__/components/PluginWorkspaceView.navigation.test.ts +102 -0
  172. package/src/__tests__/components/PluginWorkspaceView.shell.test.ts +41 -0
  173. package/src/__tests__/components/ProtocolStep.presentation.test.ts +31 -0
  174. package/src/__tests__/components/ProtocolStepEditor.state.test.ts +165 -0
  175. package/src/__tests__/components/ProtocolStepParameterField.test.ts +44 -0
  176. package/src/__tests__/components/ReagentList.presentation.test.ts +68 -0
  177. package/src/__tests__/components/SampleSelector.colors.test.ts +49 -0
  178. package/src/__tests__/components/SampleSelector.drag.test.ts +100 -0
  179. package/src/__tests__/components/SampleSelector.groups.test.ts +81 -0
  180. package/src/__tests__/components/SampleSelector.selection.test.ts +70 -0
  181. package/src/__tests__/components/SampleSelector.test.ts +32 -0
  182. package/src/__tests__/components/SampleSelectorSampleRow.test.ts +37 -0
  183. package/src/__tests__/components/ScheduleCalendar.test.ts +44 -0
  184. package/src/__tests__/components/SettingsModal.schema.test.ts +97 -0
  185. package/src/__tests__/components/WellPlate.colors.test.ts +28 -0
  186. package/src/__tests__/components/WellPlate.conditions.test.ts +68 -0
  187. package/src/__tests__/components/WellPlate.geometry.test.ts +54 -0
  188. package/src/__tests__/components/WellPlate.interaction.test.ts +171 -0
  189. package/src/__tests__/components/WellPlate.legend.test.ts +13 -0
  190. package/src/__tests__/components/WellPlate.rendering.test.ts +122 -0
  191. package/src/__tests__/components/WellPlate.sampleDrop.test.ts +70 -0
  192. package/src/__tests__/composables/autoGroup/classify.test.ts +107 -0
  193. package/src/__tests__/composables/autoGroup/columns.test.ts +135 -0
  194. package/src/__tests__/composables/autoGroup/compose.test.ts +227 -0
  195. package/src/__tests__/composables/autoGroup/cooccurrence.test.ts +91 -0
  196. package/src/__tests__/composables/autoGroup/fingerprint.test.ts +50 -0
  197. package/src/__tests__/composables/autoGroup/integration.test.ts +79 -0
  198. package/src/__tests__/composables/autoGroup/template.test.ts +70 -0
  199. package/src/__tests__/composables/autoGroup/tokenize.test.ts +33 -0
  200. package/src/__tests__/composables/useAutoGroup.test.ts +129 -625
  201. package/src/__tests__/composables/useAutoGroupInputSources.test.ts +107 -0
  202. package/src/__tests__/composables/useControlSchema.test.ts +23 -0
  203. package/src/__tests__/composables/useScheduleCalendarLayout.test.ts +89 -0
  204. package/src/__tests__/docs/extractDocsComponents.test.ts +142 -0
  205. package/src/__tests__/docs/extractDocsExports.test.ts +77 -0
  206. package/src/__tests__/docs/extractDocsParsing.test.ts +69 -0
  207. package/src/__tests__/docs/extractDocsTemplates.test.ts +54 -0
  208. package/src/__tests__/docs/extractDocsTheme.test.ts +89 -0
  209. package/src/__tests__/docs/frontendDocsCatalog.test.ts +1 -1
  210. package/src/__tests__/fixtures/auto-group/mixed-lc-ms-batch.txt +187 -0
  211. package/src/components/AppSidebar.vue +2 -6
  212. package/src/components/AppTopBar.navigation.ts +62 -0
  213. package/src/components/AppTopBar.vue +17 -44
  214. package/src/components/AutoGroupModal.story.vue +50 -0
  215. package/src/components/AutoGroupModal.vue +441 -158
  216. package/src/components/ControlWorkspaceView.vue +2 -6
  217. package/src/components/DoseCalculator.vue +13 -73
  218. package/src/components/DoseCalculatorVolumeField.vue +61 -0
  219. package/src/components/ExperimentTimeline.vue +6 -31
  220. package/src/components/FormBuilder.vue +2 -7
  221. package/src/components/PlateMapEditor.vue +32 -106
  222. package/src/components/PluginWorkspaceView.controls.ts +182 -0
  223. package/src/components/PluginWorkspaceView.navigation.ts +106 -0
  224. package/src/components/PluginWorkspaceView.props.ts +174 -0
  225. package/src/components/PluginWorkspaceView.shell.ts +66 -0
  226. package/src/components/PluginWorkspaceView.vue +85 -404
  227. package/src/components/ProtocolStep.presentation.ts +31 -0
  228. package/src/components/ProtocolStepEditor.state.ts +104 -0
  229. package/src/components/ProtocolStepEditor.vue +48 -179
  230. package/src/components/ProtocolStepParameterField.vue +134 -0
  231. package/src/components/ReagentList.presentation.ts +105 -0
  232. package/src/components/ReagentList.vue +16 -79
  233. package/src/components/SampleSelector.colors.ts +43 -0
  234. package/src/components/SampleSelector.drag.ts +164 -0
  235. package/src/components/SampleSelector.groups.ts +109 -0
  236. package/src/components/SampleSelector.selection.ts +103 -0
  237. package/src/components/SampleSelector.vue +82 -349
  238. package/src/components/SampleSelectorSampleRow.vue +64 -0
  239. package/src/components/ScheduleCalendar.vue +44 -199
  240. package/src/components/SettingsModal.schema.ts +71 -0
  241. package/src/components/SettingsModal.vue +16 -46
  242. package/src/components/WellPlate.colors.ts +56 -0
  243. package/src/components/WellPlate.conditions.ts +100 -0
  244. package/src/components/WellPlate.geometry.ts +91 -0
  245. package/src/components/WellPlate.interaction.ts +272 -0
  246. package/src/components/WellPlate.legend.ts +8 -0
  247. package/src/components/WellPlate.rendering.ts +105 -0
  248. package/src/components/WellPlate.sampleDrop.ts +73 -0
  249. package/src/components/WellPlate.vue +102 -550
  250. package/src/components/internal/PlateMapEditorToolbarInternal.vue +128 -0
  251. package/src/composables/autoGroup/classKey.ts +5 -0
  252. package/src/composables/autoGroup/classify.ts +205 -0
  253. package/src/composables/autoGroup/colors.ts +6 -0
  254. package/src/composables/autoGroup/columns.ts +226 -0
  255. package/src/composables/autoGroup/compose.ts +156 -0
  256. package/src/composables/autoGroup/cooccurrence.ts +46 -0
  257. package/src/composables/autoGroup/csv-shim.ts +44 -0
  258. package/src/composables/autoGroup/fingerprint.ts +49 -0
  259. package/src/composables/autoGroup/index.ts +20 -0
  260. package/src/composables/autoGroup/replicatePreGroup.ts +90 -0
  261. package/src/composables/autoGroup/template.ts +126 -0
  262. package/src/composables/autoGroup/tokenize.ts +41 -0
  263. package/src/composables/autoGroup/vocab.json +67 -0
  264. package/src/composables/autoGroupConstants.ts +4 -0
  265. package/src/composables/autoGroupGrouping.ts +148 -0
  266. package/src/composables/controlComponentBindings.ts +1 -1
  267. package/src/composables/controlSchemaAdapters.ts +1 -1
  268. package/src/composables/controlSchemaDoseDesign.ts +215 -0
  269. package/src/composables/controlSchemaFormFields.ts +1 -1
  270. package/src/composables/controlSchemaLayout.ts +1 -1
  271. package/src/composables/controlSchemaModel.ts +163 -0
  272. package/src/composables/controlSchemaNormalize.ts +1 -1
  273. package/src/composables/controlSchemaTypes.ts +364 -0
  274. package/src/composables/controlWorkspaceOptions.ts +1 -1
  275. package/src/composables/formBuilderSchema.ts +153 -0
  276. package/src/composables/pluginEndpointBuilder.ts +203 -0
  277. package/src/composables/protocolTemplateCatalog.ts +325 -0
  278. package/src/composables/useAutoGroup.ts +395 -549
  279. package/src/composables/useAutoGroupInputSources.ts +147 -0
  280. package/src/composables/useBioTemplateControls.ts +1 -1
  281. package/src/composables/useBioTemplatePresetWorkspace.ts +1 -1
  282. package/src/composables/useBioTemplateWorkspace.ts +1 -1
  283. package/src/composables/useControlSchema.ts +21 -692
  284. package/src/composables/useControlWorkspace.ts +7 -13
  285. package/src/composables/useForm.ts +5 -187
  286. package/src/composables/useFormBuilder.ts +11 -153
  287. package/src/composables/useFormValidation.ts +154 -0
  288. package/src/composables/usePluginClient.ts +10 -193
  289. package/src/composables/useProtocolTemplates.ts +10 -328
  290. package/src/composables/useScheduleCalendarLayout.ts +287 -0
  291. package/src/styles/components/auto-group-modal.css +248 -310
  292. package/src/templates/controlSchemaTypes.ts +1 -1
  293. package/src/templates/templateAdapterTypes.ts +58 -0
  294. package/src/templates/templateCreateOptions.ts +208 -0
  295. package/src/templates/templateQpcrTypes.ts +48 -0
  296. package/src/templates/types.ts +79 -275
  297. package/src/types/auto-group.ts +107 -9
  298. package/src/types/componentLabTypes.ts +235 -0
  299. package/src/types/componentWorkflowTypes.ts +190 -0
  300. package/src/types/components.ts +74 -424
  301. package/dist/components-DafPc4rM.js.map +0 -1
  302. package/dist/composables-BMkPQhVK.js.map +0 -1
  303. package/dist/templates-bUAWMn5L.js.map +0 -1
  304. package/dist/useProtocolTemplates-QZtHFFH2.js.map +0 -1
@@ -1,650 +1,496 @@
1
- import { ref, computed } from 'vue'
1
+ import { computed, ref } from 'vue'
2
2
  import type {
3
+ AutoGroupResult,
4
+ ClassDisposition,
5
+ ClassSchema,
6
+ ColumnInfo,
7
+ ColumnRole,
3
8
  InputMode,
9
+ MergeSuggestion,
10
+ NumericBinning,
4
11
  OutlierAction,
5
12
  OutlierInfo,
6
- ColumnInfo,
7
- MetadataRow,
8
- AutoGroupResult,
9
13
  ParsedCsvData,
14
+ SampleClass,
15
+ SchemaFingerprint,
16
+ ValueOps,
10
17
  } from '../types/auto-group'
11
- import type { SampleGroup } from '../types/components'
18
+ import {
19
+ buildClassSchema,
20
+ classKey,
21
+ composeGroups,
22
+ composeTemplate,
23
+ detectClass,
24
+ expandGroupsWithReplicates,
25
+ findMerges,
26
+ pickPrimaryDelimiter,
27
+ preGroupReplicates,
28
+ restoreFingerprint,
29
+ serializeFingerprint,
30
+ splitMulti,
31
+ type ReplicatePreGrouping,
32
+ type TemplateOptions,
33
+ } from './autoGroup'
12
34
  import { unwrapExperimentDesignData } from './experimentDesignData'
13
35
 
14
- export const DEFAULT_COLORS = [
15
- '#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6',
16
- '#EC4899', '#06B6D4', '#84CC16', '#F97316', '#6366F1',
17
- ]
18
-
19
- const DELIMITER_CANDIDATES = ['_', '-', '.'] as const
36
+ export { DEFAULT_COLORS } from './autoGroup'
20
37
 
21
- // --- Pure functions (exported for testing) ---
38
+ let deprecationWarned = false
39
+ let outlierDeprecationWarned = false
40
+ function warnDeprecated(api: string) {
41
+ if (deprecationWarned) return
42
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') return
43
+ console.warn(
44
+ `[useAutoGroup] ${api} is deprecated; use the new class-first API (setClassDisposition / toggleGroupBy / setValueOps).`,
45
+ )
46
+ deprecationWarned = true
47
+ }
22
48
 
23
- export function analyzeDelimiter(lines: string[]): {
24
- delimiter: string
25
- dominantFieldCount: number
26
- minFieldCount: number
27
- consistency: number
28
- } {
29
- if (lines.length === 0) {
30
- return { delimiter: '_', dominantFieldCount: 1, minFieldCount: 1, consistency: 0 }
31
- }
49
+ // Re-export `parseCSV` for callers that imported it from this file.
50
+ export { parseCSV } from './autoGroup/csv-shim'
32
51
 
33
- let bestDelimiter = '_'
34
- let bestConsistency = -1
35
- let bestFieldCount = 1
52
+ export function useAutoGroup() {
53
+ const inputMode = ref<InputMode>('paste')
54
+ const rawText = ref('')
55
+ const csvData = ref<ParsedCsvData | null>(null)
56
+ const sampleTypeHints = ref<(string | undefined)[]>([])
36
57
 
37
- for (const candidate of DELIMITER_CANDIDATES) {
38
- const fieldCounts = lines.map(line => line.split(candidate).length)
39
- const countFrequency = new Map<number, number>()
58
+ const classes = ref<SampleClass[]>([])
59
+ const schemas = ref<Record<string, ClassSchema>>({})
60
+ const activeClassKey = ref<string>('')
40
61
 
41
- for (const count of fieldCounts) {
42
- countFrequency.set(count, (countFrequency.get(count) ?? 0) + 1)
62
+ const samples = computed(() => {
63
+ if (csvData.value) {
64
+ return csvData.value.rows.map(r => r[csvData.value!.sampleColumn] ?? '')
43
65
  }
66
+ return rawText.value.split('\n').map(l => l.trim()).filter(l => l.length > 0)
67
+ })
44
68
 
45
- // Find mode (most frequent field count)
46
- let modeCount = 1
47
- let modeFrequency = 0
48
- for (const [count, freq] of countFrequency) {
49
- if (freq > modeFrequency || (freq === modeFrequency && count > modeCount)) {
50
- modeCount = count
51
- modeFrequency = freq
52
- }
53
- }
69
+ const tokenized = ref<string[][]>([])
70
+ const delimiter = ref<'_' | '-' | '.'>('_')
71
+ // Replicate pre-grouping: strip injection-number / T<n> / B<n> / Rep<n> markers,
72
+ // collapse samples with the same base name into one "row" for tokenisation,
73
+ // then expand the resulting groups back to the original samples in `result`.
74
+ // null when the paste path hasn't run (CSV path keeps the old behaviour for now).
75
+ const preGrouping = ref<ReplicatePreGrouping | null>(null)
54
76
 
55
- const rawConsistency = modeFrequency / lines.length
56
- // A delimiter that produces field count 1 didn't actually split anything
57
- const consistency = modeCount > 1 ? rawConsistency : 0
58
-
59
- if (
60
- consistency > bestConsistency ||
61
- (consistency === bestConsistency &&
62
- DELIMITER_CANDIDATES.indexOf(candidate) < DELIMITER_CANDIDATES.indexOf(bestDelimiter as typeof candidate))
63
- ) {
64
- bestDelimiter = candidate
65
- bestConsistency = consistency
66
- bestFieldCount = modeCount
77
+ function parseInput() {
78
+ if ((inputMode.value === 'csv' || inputMode.value === 'experiment') && csvData.value) {
79
+ parseInputFromCsv()
80
+ return
67
81
  }
82
+ parseInputFromPaste()
68
83
  }
69
84
 
70
- const bestFieldCounts = lines.map(line => line.split(bestDelimiter).length)
71
- const multiFieldCounts = bestFieldCounts.filter(c => c >= 2)
72
- const minFieldCount = multiFieldCounts.length > 0 ? Math.min(...multiFieldCounts) : 1
73
-
74
- return {
75
- delimiter: bestDelimiter,
76
- dominantFieldCount: bestFieldCount,
77
- minFieldCount,
78
- consistency: bestConsistency,
85
+ function parseInputFromCsv() {
86
+ // CSV input already exposes structured columns, so replicate pre-grouping
87
+ // isn't needed (and would interfere with header-aware role inference).
88
+ preGrouping.value = null
89
+ const csv = csvData.value!
90
+ const nonSampleCols = csv.columns.filter(c => c !== csv.sampleColumn && c !== 'sample_type')
91
+ if (nonSampleCols.length === 0) {
92
+ // No metadata columns — degenerate, single Unknown class
93
+ tokenized.value = csv.rows.map(() => [])
94
+ classes.value = [{
95
+ kind: 'unknown',
96
+ label: 'Unknown',
97
+ members: csv.rows.map((_, i) => i),
98
+ classTagPositions: [],
99
+ disposition: 'group',
100
+ }]
101
+ schemas.value = { unknown: { classKind: 'unknown', columns: [], groupBy: [] } }
102
+ activeClassKey.value = 'unknown'
103
+ return
104
+ }
105
+ const tokens = csv.rows.map(r => nonSampleCols.map(col => r[col] ?? ''))
106
+ tokenized.value = tokens
107
+ // Class detection: use explicit sample_type hint from csv.rows[i].sample_type
108
+ const hints = csv.rows.map(r => {
109
+ const t = String(r['sample_type'] ?? '').toLowerCase()
110
+ if (t === 'qc') return 'qc'
111
+ if (t === 'blank') return 'blank'
112
+ return undefined
113
+ })
114
+ const detected = detectClass(tokens, { sampleTypeHints: hints })
115
+ classes.value = detected
116
+ // Build schema per class with CSV header names
117
+ const newSchemas: Record<string, ClassSchema> = {}
118
+ for (const cls of detected) {
119
+ const memberTokens = cls.members.map(i => tokens[i])
120
+ const schema = buildClassSchema(memberTokens, cls.kind, cls.subKind, cls.classTagPositions)
121
+ // Override default Token N names with actual CSV header names
122
+ schema.columns = schema.columns.map((c, i) => ({
123
+ ...c,
124
+ name: nonSampleCols[i] ?? c.name,
125
+ originalName: nonSampleCols[i],
126
+ }))
127
+ newSchemas[classKey(cls)] = schema
128
+ }
129
+ schemas.value = newSchemas
130
+ if (detected.length > 0) activeClassKey.value = classKey(detected[0])
79
131
  }
80
- }
81
132
 
82
- export function detectOutliers(
83
- lines: string[],
84
- delimiter: string,
85
- minFieldCount: number,
86
- ): OutlierInfo[] {
87
- const outliers: OutlierInfo[] = []
88
-
89
- for (let i = 0; i < lines.length; i++) {
90
- const fieldCount = lines[i].split(delimiter).length
91
- if (fieldCount < minFieldCount) {
92
- outliers.push({
93
- sample: lines[i],
94
- index: i,
95
- fieldCount,
96
- action: 'include',
97
- })
133
+ function parseInputFromPaste() {
134
+ const lines = samples.value
135
+ if (lines.length === 0) {
136
+ classes.value = []
137
+ schemas.value = {}
138
+ preGrouping.value = null
139
+ return
140
+ }
141
+ // Replicate pre-pass: strip run-order / T<n> / B<n> / Rep<n> markers, then
142
+ // collapse samples whose stripped names match into one base entry. The
143
+ // tokenize → classify → schema pipeline runs on the *base* names; the
144
+ // `result` computed expands each resulting group back to the original
145
+ // samples via `expandGroupsWithReplicates`.
146
+ const pre = preGroupReplicates(lines)
147
+ preGrouping.value = pre
148
+
149
+ const d = pickPrimaryDelimiter(pre.baseNames)
150
+ delimiter.value = d
151
+ tokenized.value = pre.baseNames.map(l => splitMulti(l, d))
152
+ // sampleTypeHints align to original samples, so pick the first sample's
153
+ // hint per base group as the representative hint.
154
+ const hints = pre.membersByBase.map(members => sampleTypeHints.value[members[0]])
155
+ const detected = detectClass(tokenized.value, { sampleTypeHints: hints })
156
+ classes.value = detected
157
+ const newSchemas: Record<string, ClassSchema> = {}
158
+ for (const cls of detected) {
159
+ const memberTokens = cls.members.map(i => tokenized.value[i])
160
+ newSchemas[classKey(cls)] = buildClassSchema(
161
+ memberTokens,
162
+ cls.kind,
163
+ cls.subKind,
164
+ cls.classTagPositions,
165
+ )
166
+ }
167
+ schemas.value = newSchemas
168
+ if (detected.length > 0) {
169
+ activeClassKey.value = classKey(detected[0])
98
170
  }
99
171
  }
100
172
 
101
- return outliers
102
- }
173
+ const activeSchema = computed<ClassSchema | null>(() => schemas.value[activeClassKey.value] ?? null)
103
174
 
104
- const QC_KEYWORDS = new Set([
105
- 'eqc', 'iqc', 'qc', 'blank', 'std', 'standard', 'test',
106
- ])
107
-
108
- export function classifyOutlierAction(
109
- sample: string,
110
- delimiter: string,
111
- ): OutlierAction {
112
- const segments = sample.split(delimiter)
113
- return segments.some(seg => QC_KEYWORDS.has(seg.toLowerCase()))
114
- ? 'qc'
115
- : 'include'
116
- }
117
-
118
- // A column is useful for grouping when it has more than one distinct value
119
- // AND it does not produce a unique value per row (which would create N
120
- // singleton groups instead of meaningful aggregation).
121
- export function isUsefulField(field: ColumnInfo, rowCount: number): boolean {
122
- return field.cardinality > 1 && !(rowCount > 1 && field.cardinality === rowCount)
123
- }
124
-
125
- export function extractColumns(
126
- samples: string[],
127
- delimiter: string,
128
- minFieldCount: number,
129
- ): ColumnInfo[] {
130
- if (samples.length === 0) return []
131
-
132
- const suffixCount = minFieldCount - 1
133
- const rows = samples.map(s => {
134
- const parts = s.split(delimiter)
135
- const splitAt = parts.length - suffixCount
136
- return [
137
- parts.slice(0, splitAt).join(delimiter),
138
- ...parts.slice(splitAt),
139
- ]
175
+ const suggestions = computed<MergeSuggestion[]>(() => {
176
+ const schema = activeSchema.value
177
+ if (!schema) return []
178
+ const activeClass = classes.value.find(
179
+ c => classKey(c) === activeClassKey.value,
180
+ )
181
+ if (!activeClass) return []
182
+ const memberTokens = activeClass.members.map(i => tokenized.value[i])
183
+ return findMerges(schema, memberTokens)
140
184
  })
141
185
 
142
- const columnCount = minFieldCount
143
- const columns: ColumnInfo[] = []
144
- for (let col = 0; col < columnCount; col++) {
145
- const values = rows.map(row => row[col])
146
- const unique = [...new Set(values)]
147
- columns.push({
148
- index: col,
149
- name: col === 0 ? 'Condition' : `Field ${col + 1}`,
150
- uniqueValues: unique,
151
- cardinality: unique.length,
152
- type: col === 0 ? 'prefix' : 'suffix',
186
+ const result = computed<AutoGroupResult>(() => {
187
+ const pre = preGrouping.value
188
+ const composed = composeGroups({
189
+ tokenizedSamples: tokenized.value,
190
+ // After the pre-pass, sampleNames passed to compose are the *base* names
191
+ // (one per replicate group); fall back to the original samples when
192
+ // pre-grouping hasn't run (e.g. the CSV path or empty input).
193
+ sampleNames: pre ? pre.baseNames : samples.value,
194
+ schemas: schemas.value,
195
+ classes: classes.value,
153
196
  })
154
- }
155
-
156
- return columns
157
- }
158
-
159
- export function parseCSVLine(line: string, delimiter: string = ','): string[] {
160
- const result: string[] = []
161
- let current = ''
162
- let inQuotes = false
163
-
164
- for (let i = 0; i < line.length; i++) {
165
- const char = line[i]
166
- if (char === '"') {
167
- inQuotes = !inQuotes
168
- } else if (char === delimiter && !inQuotes) {
169
- result.push(current.trim())
170
- current = ''
171
- } else {
172
- current += char
197
+ if (!pre) return composed
198
+ // Expand each group's `samples` from base names back to the original
199
+ // sample names so the Preview panel shows real filenames, not stripped
200
+ // bases, and so `metadata` reflects each input row exactly once.
201
+ return {
202
+ ...composed,
203
+ groups: expandGroupsWithReplicates(composed.groups, pre),
204
+ experimentalGroups: composed.experimentalGroups
205
+ ? expandGroupsWithReplicates(composed.experimentalGroups, pre)
206
+ : undefined,
207
+ qcGroups: composed.qcGroups
208
+ ? expandGroupsWithReplicates(composed.qcGroups, pre)
209
+ : undefined,
173
210
  }
174
- }
175
- result.push(current.trim())
211
+ })
176
212
 
177
- return result
178
- }
213
+ const fingerprint = computed<SchemaFingerprint>(() =>
214
+ serializeFingerprint(result.value.schemas ?? []),
215
+ )
179
216
 
180
- export function parseCSV(text: string): ParsedCsvData {
181
- const lines = text.trim().split('\n')
182
- if (lines.length < 2) {
183
- throw new Error('CSV must have at least a header and one data row')
184
- }
217
+ const groups = computed(() => result.value.groups)
218
+ const qcGroups = computed(() => result.value.qcGroups ?? [])
219
+ const metadata = computed(() => result.value.metadata)
220
+ const excludedSamples = computed(() => result.value.excludedSamples)
185
221
 
186
- // Auto-detect delimiter: tab takes precedence if present in header
187
- const firstLine = lines[0]
188
- const csvDelimiter = firstLine.includes('\t') ? '\t' : ','
222
+ // ----- Mutators -----
189
223
 
190
- const headers = parseCSVLine(firstLine, csvDelimiter)
191
- const rows: Record<string, string>[] = []
224
+ function setClassDisposition(key: string, disposition: ClassDisposition) {
225
+ const target = classes.value.find(c => classKey(c) === key)
226
+ if (!target || target.disposition === disposition) return
227
+ classes.value = classes.value.map(c =>
228
+ classKey(c) === key ? { ...c, disposition } : c,
229
+ )
230
+ }
192
231
 
193
- for (let i = 1; i < lines.length; i++) {
194
- const values = parseCSVLine(lines[i], csvDelimiter)
195
- if (values.length !== headers.length) continue
196
- const row: Record<string, string> = {}
197
- headers.forEach((header, idx) => {
198
- row[header] = values[idx]
232
+ function setClassDispositions(
233
+ predicate: (c: SampleClass) => boolean,
234
+ disposition: ClassDisposition,
235
+ ) {
236
+ let changed = false
237
+ const next = classes.value.map(c => {
238
+ if (predicate(c) && c.disposition !== disposition) {
239
+ changed = true
240
+ return { ...c, disposition }
241
+ }
242
+ return c
199
243
  })
200
- rows.push(row)
244
+ if (changed) classes.value = next
201
245
  }
202
246
 
203
- // Auto-detect sample column
204
- const sampleKeywords = ['sample', 'name', 'id', 'sample_name', 'samplename', 'file name', 'filename', 'file_name']
205
- const sampleColumn =
206
- headers.find(h => sampleKeywords.includes(h.toLowerCase())) ?? headers[0]
207
-
208
- return { columns: headers, rows, sampleColumn, delimiter: csvDelimiter }
209
- }
210
-
211
- export function computeGroups(
212
- allSamples: string[],
213
- columns: ColumnInfo[],
214
- enabledFields: Set<number>,
215
- outlierActions: Map<number, OutlierAction>,
216
- delimiter: string,
217
- minFieldCount: number,
218
- ): { groups: SampleGroup[]; metadata: MetadataRow[]; excludedSamples: string[] } {
219
- const excludedSamples: string[] = []
220
- const qcSamples: string[] = []
221
- const conformingSamples: string[] = []
222
-
223
- for (let i = 0; i < allSamples.length; i++) {
224
- const action = outlierActions.get(i)
225
- if (action === 'exclude') {
226
- excludedSamples.push(allSamples[i])
227
- } else if (action === 'qc') {
228
- qcSamples.push(allSamples[i])
229
- } else {
230
- conformingSamples.push(allSamples[i])
247
+ function toggleGroupBy(key: string, idx: number) {
248
+ const schema = schemas.value[key]
249
+ if (!schema) return
250
+ const set = new Set(schema.groupBy)
251
+ if (set.has(idx)) set.delete(idx)
252
+ else set.add(idx)
253
+ schemas.value = {
254
+ ...schemas.value,
255
+ [key]: { ...schema, groupBy: [...set].sort((a, b) => a - b) },
231
256
  }
232
257
  }
233
258
 
234
- // Build group map
235
- const groupMap = new Map<string, string[]>()
236
- const metadata: MetadataRow[] = []
237
- const enabledIndices = [...enabledFields].sort((a, b) => a - b)
238
-
239
- const suffixCount = minFieldCount - 1
240
-
241
- for (const sample of conformingSamples) {
242
- const parts = sample.split(delimiter)
243
- const splitAt = Math.max(1, parts.length - suffixCount)
244
- const row = [
245
- parts.slice(0, splitAt).join(delimiter),
246
- ...parts.slice(splitAt),
247
- ]
248
-
249
- // Build group key from enabled columns
250
- const keyParts: string[] = []
251
- for (const idx of enabledIndices) {
252
- if (idx < row.length && idx < columns.length) {
253
- keyParts.push(row[idx])
259
+ function updateColumn(key: string, idx: number, patch: Partial<ColumnInfo>) {
260
+ const schema = schemas.value[key]
261
+ if (!schema) return
262
+ const col = schema.columns.find(c => c.index === idx)
263
+ if (!col) return
264
+ let changed = false
265
+ for (const k of Object.keys(patch) as (keyof ColumnInfo)[]) {
266
+ if ((col as unknown as Record<string, unknown>)[k as string] !== (patch as unknown as Record<string, unknown>)[k as string]) {
267
+ changed = true
268
+ break
254
269
  }
255
270
  }
256
- const groupKey = keyParts.join(' / ')
257
-
258
- const group = groupMap.get(groupKey)
259
- if (group) {
260
- group.push(sample)
261
- } else {
262
- groupMap.set(groupKey, [sample])
271
+ if (!changed) return
272
+ schemas.value = {
273
+ ...schemas.value,
274
+ [key]: {
275
+ ...schema,
276
+ columns: schema.columns.map(c => (c.index === idx ? { ...c, ...patch } : c)),
277
+ },
263
278
  }
264
-
265
- // Build metadata row with ALL columns
266
- const fields: Record<string, string> = {}
267
- for (const col of columns) {
268
- if (col.index < row.length) {
269
- fields[col.name] = row[col.index]
270
- }
271
- }
272
- metadata.push({ sampleName: sample, fields, group: groupKey })
273
279
  }
274
280
 
275
- // Convert to SampleGroup[]
276
- const groups: SampleGroup[] = []
277
- let colorIdx = 0
278
- for (const [name, samples] of groupMap) {
279
- groups.push({
280
- name,
281
- color: DEFAULT_COLORS[colorIdx % DEFAULT_COLORS.length],
282
- samples,
283
- })
284
- colorIdx++
281
+ function setRole(key: string, idx: number, role: ColumnRole) {
282
+ updateColumn(key, idx, { role })
285
283
  }
286
284
 
287
- // QC group
288
- if (qcSamples.length > 0) {
289
- groups.push({
290
- name: 'QC',
291
- color: '#6B7280',
292
- samples: qcSamples,
293
- })
294
- for (const sample of qcSamples) {
295
- metadata.push({ sampleName: sample, fields: {}, group: 'QC' })
296
- }
285
+ function setColumnName(key: string, idx: number, name: string) {
286
+ updateColumn(key, idx, { displayName: name })
297
287
  }
298
288
 
299
- return { groups, metadata, excludedSamples }
300
- }
301
-
302
- /**
303
- * Extract sample metadata from raw design_data into ParsedCsvData format.
304
- *
305
- * Looks for a `samples` array in the design data. For each sample, merges
306
- * the `conditions` dict (the metadata table) with the `sample_name` to
307
- * produce a flat tabular row. QC and blank samples are filtered out by
308
- * their explicit `sample_type` field.
309
- *
310
- * Returns null if no samples with conditions are found.
311
- */
312
- export function extractSamplesFromDesignData(
313
- rawData: Record<string, unknown>,
314
- ): ParsedCsvData | null {
315
- const designData = unwrapExperimentDesignData(rawData)
316
- if (!designData) return null
317
-
318
- const samples = designData.samples
319
- if (!Array.isArray(samples) || samples.length === 0) return null
320
-
321
- // Single pass: filter QC/blank and collect all condition keys
322
- const allConditionKeys: string[] = []
323
- const keySet = new Set<string>()
324
- const filteredSamples: Record<string, unknown>[] = []
325
-
326
- for (const sample of samples) {
327
- const sampleType = String((sample as Record<string, unknown>).sample_type ?? 'sample').toLowerCase()
328
- if (sampleType === 'qc' || sampleType === 'blank') continue
329
-
330
- filteredSamples.push(sample as Record<string, unknown>)
331
- const conditions = (sample as Record<string, unknown>).conditions as Record<string, string> | undefined
332
- if (conditions && typeof conditions === 'object') {
333
- for (const key of Object.keys(conditions)) {
334
- if (!keySet.has(key)) {
335
- keySet.add(key)
336
- allConditionKeys.push(key)
337
- }
338
- }
339
- }
289
+ function setValueOps(key: string, idx: number, ops: ValueOps) {
290
+ updateColumn(key, idx, { ops })
340
291
  }
341
292
 
342
- if (filteredSamples.length === 0 || allConditionKeys.length === 0) return null
343
-
344
- const columns = ['sample_name', ...allConditionKeys]
345
- const rows: Record<string, string>[] = filteredSamples.map((sample) => {
346
- const conditions = (sample.conditions as Record<string, string>) ?? {}
347
- const row: Record<string, string> = {
348
- sample_name: String(sample.sample_name ?? ''),
349
- }
350
- for (const key of allConditionKeys) {
351
- row[key] = conditions[key] ?? ''
352
- }
353
- return row
354
- })
355
-
356
- return { columns, rows, sampleColumn: 'sample_name', delimiter: ',' }
357
- }
293
+ function setBinning(key: string, idx: number, binning: NumericBinning) {
294
+ updateColumn(key, idx, { binning })
295
+ }
358
296
 
359
- export function computeGroupsFromCsv(
360
- csvData: ParsedCsvData,
361
- columns: ColumnInfo[],
362
- enabledFields: Set<number>,
363
- ): { groups: SampleGroup[]; metadata: MetadataRow[]; excludedSamples: string[] } {
364
- const groupMap = new Map<string, string[]>()
365
- const metadata: MetadataRow[] = []
366
- const enabledCols = columns
367
- .filter(c => enabledFields.has(c.index))
368
- .sort((a, b) => a.index - b.index)
369
-
370
- for (const row of csvData.rows) {
371
- const sampleName = row[csvData.sampleColumn]
372
-
373
- // Build group key from enabled CSV column values
374
- // Use originalName for CSV row lookup (survives user renames), display name for group key
375
- const keyParts = enabledCols.map(col => row[col.originalName ?? col.name])
376
- const groupKey = keyParts.join(' / ')
377
-
378
- const group = groupMap.get(groupKey)
379
- if (group) {
380
- group.push(sampleName)
381
- } else {
382
- groupMap.set(groupKey, [sampleName])
297
+ function mergeColumns(key: string, indices: number[]) {
298
+ const schema = schemas.value[key]
299
+ if (!schema || indices.length < 2) return
300
+ const sorted = [...indices].sort((a, b) => a - b)
301
+ const start = sorted[0]
302
+ const merged: ColumnInfo = {
303
+ index: start,
304
+ name: `Column ${sorted.map(i => i + 1).join('–')}`,
305
+ sourceIndices: sorted.flatMap(i => schema.columns.find(c => c.index === i)?.sourceIndices ?? [i]),
306
+ uniqueValues: [],
307
+ cardinality: 0,
308
+ role: 'factor',
383
309
  }
384
-
385
- // Build metadata row with ALL columns — use display name as key, original for lookup
386
- const fields: Record<string, string> = {}
387
- for (const col of columns) {
388
- fields[col.name] = row[col.originalName ?? col.name]
310
+ const dropped = new Set(sorted)
311
+ schemas.value = {
312
+ ...schemas.value,
313
+ [key]: {
314
+ ...schema,
315
+ columns: [merged, ...schema.columns.filter(c => !dropped.has(c.index))]
316
+ .sort((a, b) => a.index - b.index),
317
+ groupBy: schema.groupBy.filter(i => !dropped.has(i) || i === start),
318
+ },
389
319
  }
390
- metadata.push({ sampleName, fields, group: groupKey })
391
320
  }
392
321
 
393
- // Convert to SampleGroup[]
394
- const groups: SampleGroup[] = []
395
- let colorIdx = 0
396
- for (const [name, samples] of groupMap) {
397
- groups.push({
398
- name,
399
- color: DEFAULT_COLORS[colorIdx % DEFAULT_COLORS.length],
400
- samples,
401
- })
402
- colorIdx++
322
+ function loadFingerprint(fp: SchemaFingerprint) {
323
+ const restored = restoreFingerprint(fp, Object.values(schemas.value))
324
+ const next: Record<string, ClassSchema> = {}
325
+ for (const s of restored) next[classKey({ kind: s.classKind, subKind: s.subKind })] = s
326
+ schemas.value = next
403
327
  }
404
328
 
405
- return { groups, metadata, excludedSamples: [] }
406
- }
407
-
408
- // --- Reactive composable ---
409
-
410
- /** Parses sample names or CSV data to propose group assignments with outlier detection and preview. */
411
- export function useAutoGroup() {
412
- const inputMode = ref<InputMode>('paste')
413
- const rawText = ref('')
414
- const csvData = ref<ParsedCsvData | null>(null)
415
- const delimiter = ref('_')
416
- const dominantFieldCount = ref(1)
417
- const minFieldCount = ref(1)
418
- const outliers = ref<OutlierInfo[]>([])
419
- const fields = ref<ColumnInfo[]>([])
420
- const fieldNames = ref<Record<number, string>>({})
421
- const enabledFields = ref(new Set<number>())
422
-
423
- const isTabularMode = computed(() =>
424
- (inputMode.value === 'csv' || inputMode.value === 'experiment') && csvData.value !== null,
425
- )
426
-
427
- const samples = computed(() => {
428
- const data = csvData.value
429
- if (isTabularMode.value && data) {
430
- return data.rows.map(r => r[data.sampleColumn])
431
- }
432
- return rawText.value
433
- .split('\n')
434
- .map(l => l.trim())
435
- .filter(l => l.length > 0)
436
- })
437
-
438
- const hasOutliers = computed(() => outliers.value.length > 0)
439
-
440
- const conformingSamples = computed(() => {
441
- const outlierIndices = new Set(outliers.value.map(o => o.index))
442
- return samples.value.filter((_, i) => !outlierIndices.has(i))
443
- })
444
-
445
- const outlierActions = computed(() => {
446
- const map = new Map<number, OutlierAction>()
447
- for (const o of outliers.value) {
448
- map.set(o.index, o.action)
449
- }
450
- return map
451
- })
452
-
453
- const effectiveColumns = computed(() => {
454
- return fields.value.map(col => ({
455
- ...col,
456
- name: fieldNames.value[col.index] ?? col.name,
457
- }))
458
- })
459
-
460
- const _computedResult = computed((): AutoGroupResult => {
461
- if (effectiveColumns.value.length === 0 || enabledFields.value.size === 0) {
462
- return { groups: [], metadata: [], excludedSamples: [] }
463
- }
329
+ // ----- Template download -----
464
330
 
465
- if (isTabularMode.value && csvData.value) {
466
- return computeGroupsFromCsv(
467
- csvData.value,
468
- effectiveColumns.value,
469
- enabledFields.value,
470
- )
471
- }
331
+ const canDownloadTemplate = computed(() => samples.value.length > 0)
472
332
 
473
- return computeGroups(
333
+ function downloadTemplate(mode: 'blank' | 'prefilled' = 'prefilled', format: 'csv' | 'tsv' = 'csv') {
334
+ const { content, filename } = composeTemplate(
474
335
  samples.value,
475
- effectiveColumns.value,
476
- enabledFields.value,
477
- outlierActions.value,
478
- delimiter.value,
479
- minFieldCount.value,
336
+ mode === 'prefilled' ? Object.values(schemas.value) : null,
337
+ mode === 'prefilled' ? classes.value : null,
338
+ { mode, format, tokenizedSamples: tokenized.value },
480
339
  )
481
- })
482
-
483
- const groups = computed(() => _computedResult.value.groups)
484
- const metadata = computed(() => _computedResult.value.metadata)
485
- const excludedSamples = computed(() => _computedResult.value.excludedSamples)
486
-
487
- const allSingletons = computed(() =>
488
- groups.value.length > 1 && groups.value.every(g => g.samples.length === 1),
489
- )
490
-
491
- const result = _computedResult
492
-
493
- function parseInput() {
494
- if (isTabularMode.value) {
495
- parseCsvInput()
496
- } else {
497
- parsePasteInput()
498
- }
340
+ const blob = new Blob([content], { type: format === 'tsv' ? 'text/tab-separated-values' : 'text/csv' })
341
+ const url = URL.createObjectURL(blob)
342
+ const a = document.createElement('a')
343
+ a.href = url
344
+ a.download = filename
345
+ document.body.appendChild(a)
346
+ a.click()
347
+ document.body.removeChild(a)
348
+ URL.revokeObjectURL(url)
499
349
  }
500
350
 
501
- function parsePasteInput() {
502
- const lines = samples.value
503
- if (lines.length === 0) return
504
-
505
- const analysis = analyzeDelimiter(lines)
506
- delimiter.value = analysis.delimiter
507
- dominantFieldCount.value = analysis.dominantFieldCount
508
-
509
- // Use dominantFieldCount as outlier threshold so QC/test samples with
510
- // fewer fields than the majority are correctly flagged
511
- outliers.value = detectOutliers(lines, analysis.delimiter, analysis.dominantFieldCount)
512
-
513
- // Apply smart default actions: auto-classify QC/test samples
514
- for (const outlier of outliers.value) {
515
- outlier.action = classifyOutlierAction(outlier.sample, analysis.delimiter)
516
- }
517
-
518
- const conforming = lines.filter(
519
- (_, i) => !outliers.value.some(o => o.index === i)
520
- )
521
-
522
- // Recompute minFieldCount from conforming samples only
523
- const conformingFieldCounts = conforming.map(s => s.split(analysis.delimiter).length)
524
- minFieldCount.value = conformingFieldCounts.length > 0
525
- ? Math.min(...conformingFieldCounts)
526
- : analysis.dominantFieldCount
527
-
528
- fields.value = extractColumns(conforming, analysis.delimiter, minFieldCount.value)
529
-
530
- fieldNames.value = {}
531
- const rowCount = conforming.length
532
- enabledFields.value = new Set(
533
- fields.value.filter(f => isUsefulField(f, rowCount)).map(f => f.index),
351
+ /** Test seam — exposes the template content/filename without triggering a browser download. */
352
+ function composeTemplateForTest(opts: Pick<TemplateOptions, 'mode' | 'format'>) {
353
+ return composeTemplate(
354
+ samples.value,
355
+ opts.mode === 'prefilled' ? Object.values(schemas.value) : null,
356
+ opts.mode === 'prefilled' ? classes.value : null,
357
+ { ...opts, tokenizedSamples: tokenized.value },
534
358
  )
535
359
  }
536
360
 
537
- function parseCsvInput() {
538
- if (!csvData.value) return
539
-
540
- const csv = csvData.value
541
- const nonSampleCols = csv.columns.filter(c => c !== csv.sampleColumn)
542
-
543
- fields.value = nonSampleCols.map((col, i) => {
544
- const values = csv.rows.map(r => r[col])
545
- const unique = [...new Set(values)]
546
- return {
547
- index: i,
548
- name: col,
549
- originalName: col,
550
- uniqueValues: unique,
551
- cardinality: unique.length,
552
- }
553
- })
361
+ // ----- Legacy public refs and shims -----
554
362
 
555
- // For CSV, no outliers
556
- outliers.value = []
557
- delimiter.value = csv.delimiter
558
- dominantFieldCount.value = csv.columns.length
559
-
560
- fieldNames.value = Object.fromEntries(fields.value.map(f => [f.index, f.name]))
561
- const rowCount = csv.rows.length
562
- enabledFields.value = new Set(
563
- fields.value.filter(f => isUsefulField(f, rowCount)).map(f => f.index),
564
- )
565
- }
363
+ const outliers = ref<OutlierInfo[]>([])
364
+ const hasOutliers = computed(() => outliers.value.length > 0)
365
+ const conformingSamples = computed(() => samples.value)
366
+ const dominantFieldCount = computed(() => tokenized.value[0]?.length ?? 0)
367
+ const minFieldCount = computed(() => {
368
+ if (tokenized.value.length === 0) return 0
369
+ return Math.min(...tokenized.value.map(t => t.length))
370
+ })
371
+ const fields = computed(() => activeSchema.value?.columns ?? [])
372
+ const fieldNames = computed<Record<number, string>>(() => {
373
+ const out: Record<number, string> = {}
374
+ for (const c of fields.value) out[c.index] = c.displayName ?? c.name
375
+ return out
376
+ })
377
+ const enabledFields = computed<Set<number>>(() =>
378
+ activeSchema.value ? new Set(activeSchema.value.groupBy) : new Set(),
379
+ )
380
+ const effectiveColumns = fields
381
+ const allSingletons = computed(
382
+ () => groups.value.length > 1 && groups.value.every(g => g.samples.length === 1),
383
+ )
566
384
 
567
- function setOutlierAction(index: number, action: OutlierAction) {
568
- const outlier = outliers.value.find(o => o.index === index)
569
- if (outlier) {
570
- outlier.action = action
571
- // Trigger reactivity
572
- outliers.value = [...outliers.value]
385
+ function setOutlierAction(_: number, __: OutlierAction) {
386
+ if (!outlierDeprecationWarned) {
387
+ if (typeof process === 'undefined' || process.env?.NODE_ENV !== 'production') {
388
+ console.warn(
389
+ `[useAutoGroup] setOutlierAction is deprecated AND inactive in the class-first redesign. ` +
390
+ `Use setClassDisposition('iqc' | 'eqc' | …, 'group' | 'overlay' | 'exclude') instead.`,
391
+ )
392
+ outlierDeprecationWarned = true
393
+ }
573
394
  }
574
395
  }
575
396
 
576
- function setAllOutlierActions(action: OutlierAction) {
577
- for (const outlier of outliers.value) {
578
- outlier.action = action
397
+ function setAllOutlierActions(__: OutlierAction) {
398
+ if (!outlierDeprecationWarned) {
399
+ if (typeof process === 'undefined' || process.env?.NODE_ENV !== 'production') {
400
+ console.warn(
401
+ `[useAutoGroup] setAllOutlierActions is deprecated AND inactive in the class-first redesign. ` +
402
+ `Use setClassDisposition('iqc' | 'eqc' | …, 'group' | 'overlay' | 'exclude') instead.`,
403
+ )
404
+ outlierDeprecationWarned = true
405
+ }
579
406
  }
580
- outliers.value = [...outliers.value]
581
407
  }
582
408
 
583
409
  function toggleField(index: number) {
584
- const newSet = new Set(enabledFields.value)
585
- if (newSet.has(index)) {
586
- newSet.delete(index)
587
- } else {
588
- newSet.add(index)
589
- }
590
- enabledFields.value = newSet
410
+ warnDeprecated('toggleField')
411
+ if (activeClassKey.value) toggleGroupBy(activeClassKey.value, index)
591
412
  }
592
413
 
593
414
  function renameField(index: number, name: string) {
594
- fieldNames.value = { ...fieldNames.value, [index]: name }
415
+ warnDeprecated('renameField')
416
+ if (activeClassKey.value) setColumnName(activeClassKey.value, index, name)
595
417
  }
596
418
 
597
419
  function loadExperimentData(rawData: Record<string, unknown>): boolean {
598
420
  const parsed = extractSamplesFromDesignData(rawData)
599
421
  if (!parsed) return false
600
-
601
422
  inputMode.value = 'experiment'
602
423
  csvData.value = parsed
603
- parseCsvInput()
424
+ sampleTypeHints.value = parsed.sampleTypeHints
425
+ parseInput()
604
426
  return true
605
427
  }
606
428
 
607
429
  function reset() {
608
430
  rawText.value = ''
609
431
  csvData.value = null
610
- delimiter.value = '_'
611
- dominantFieldCount.value = 1
612
- minFieldCount.value = 1
432
+ classes.value = []
433
+ schemas.value = {}
434
+ activeClassKey.value = ''
435
+ tokenized.value = []
613
436
  outliers.value = []
614
- fields.value = []
615
- fieldNames.value = {}
616
- enabledFields.value = new Set()
437
+ sampleTypeHints.value = []
617
438
  }
618
439
 
619
440
  return {
620
441
  // State
621
- inputMode,
622
- rawText,
623
- csvData,
624
- delimiter,
625
- dominantFieldCount,
626
- minFieldCount,
627
- outliers,
628
- fields,
629
- fieldNames,
630
- enabledFields,
442
+ inputMode, rawText, csvData, sampleTypeHints,
443
+ classes, schemas, activeClassKey,
444
+ delimiter, dominantFieldCount, minFieldCount,
631
445
  // Computed
632
- samples,
633
- hasOutliers,
634
- conformingSamples,
635
- groups,
636
- metadata,
637
- excludedSamples,
638
- allSingletons,
639
- result,
640
- effectiveColumns,
446
+ samples, tokenized, activeSchema, suggestions,
447
+ groups, qcGroups, metadata, excludedSamples, result, fingerprint,
448
+ fields, fieldNames, enabledFields, effectiveColumns,
449
+ hasOutliers, outliers, conformingSamples, allSingletons,
450
+ canDownloadTemplate,
451
+ // Mutators
452
+ setClassDisposition, setClassDispositions, toggleGroupBy, setRole, setColumnName, setValueOps, setBinning,
453
+ mergeColumns, loadFingerprint,
641
454
  // Actions
642
- parseInput,
643
- loadExperimentData,
644
- setOutlierAction,
645
- setAllOutlierActions,
646
- toggleField,
647
- renameField,
648
- reset,
455
+ parseInput, downloadTemplate, composeTemplateForTest, loadExperimentData, reset,
456
+ // Deprecated shims
457
+ setOutlierAction, setAllOutlierActions, toggleField, renameField,
458
+ }
459
+ }
460
+
461
+ export function extractSamplesFromDesignData(
462
+ rawData: Record<string, unknown>,
463
+ ): (ParsedCsvData & { sampleTypeHints: (string | undefined)[] }) | null {
464
+ const designData = unwrapExperimentDesignData(rawData)
465
+ if (!designData) return null
466
+ const samples = designData.samples
467
+ if (!Array.isArray(samples) || samples.length === 0) return null
468
+ const allConditionKeys: string[] = []
469
+ const keySet = new Set<string>()
470
+ const sampleTypeHints: (string | undefined)[] = []
471
+ for (const sample of samples) {
472
+ const rawType = (sample as Record<string, unknown>).sample_type
473
+ sampleTypeHints.push(typeof rawType === 'string' ? rawType.toLowerCase() : undefined)
474
+ const conditions = (sample as Record<string, unknown>).conditions as Record<string, string> | undefined
475
+ if (conditions && typeof conditions === 'object') {
476
+ for (const key of Object.keys(conditions)) {
477
+ if (!keySet.has(key)) {
478
+ keySet.add(key)
479
+ allConditionKeys.push(key)
480
+ }
481
+ }
482
+ }
649
483
  }
484
+ if (allConditionKeys.length === 0) return null
485
+ const columns = ['sample_name', ...allConditionKeys, 'sample_type']
486
+ const rows = (samples as Record<string, unknown>[]).map(s => {
487
+ const conditions = (s.conditions as Record<string, string>) ?? {}
488
+ const row: Record<string, string> = {
489
+ sample_name: String(s.sample_name ?? ''),
490
+ sample_type: String(s.sample_type ?? 'sample'),
491
+ }
492
+ for (const key of allConditionKeys) row[key] = conditions[key] ?? ''
493
+ return row
494
+ })
495
+ return { columns, rows, sampleColumn: 'sample_name', delimiter: ',', sampleTypeHints }
650
496
  }