Skip to content

Read sample identity from the folder, not just the file name - #129

Closed
PoslavskySV wants to merge 1 commit into
mainfrom
feat/folder-per-sample-import
Closed

Read sample identity from the folder, not just the file name#129
PoslavskySV wants to merge 1 commit into
mainfrom
feat/folder-per-sample-import

Conversation

@PoslavskySV

@PoslavskySV PoslavskySV commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Part 2 of 3 for folder-per-sample import. Spec: milaboratory/text#201. Independent of the other two — this can merge on its own.

Why

Patterns were matched against the bare file name (datasets.ts:169 did extractFileName(getFileNameFromHandle(handle))), so a one-folder-per-sample tree — what BaseSpace, bcl2fastq and CellRanger all produce — could not be described at all.

Worse: where the folder is the only place a sample name appears, every file resolves to the same sample, getOrCreateSample matches by label, and fileGroup[readIndex] = handle keeps only the last file. A wrong result that looks like a successful import. Auto-inference refuses (there's a collision guard), but a hand-written pattern walks straight into it.

What changed

Match against relative paths. useParsedFiles strips the longest common directory prefix across all accumulated files and matches what remains. This needs no file-dialog API change: recomputing the prefix over the whole set beats threading a root through, because it stays well defined when the selection spans folders or grows over several "add more files" rounds. With every file in one folder the remainder is the bare file name, so flat imports are unchanged by construction.

Segment-bounded matchers. {{Sample}}, {{*}} and tag matchers compile to [^/]+?; new {{**}} crosses segments. This is required, not cosmetic — a lazy .+? swallows the separator, and Synolo's sample name comes out as Sel78_R1_02_L1-ds.bf24…/Sel78_R1_02. Backward compatible: today's inputs contain no /. escapeRegExp already left / alone, so no new syntax was needed for the literal.

Inference tries path variants — file name, {{**}}/…, and two folder-carries-identity forms — taking the first that gives every file its own identity. The existing duplicate-key guard does the choosing for free. Newly inferable: per-sample-folder FASTQ (Sample_A/R1.fastq.gz) and per-sample-folder CellRanger MTX (Sample_A/matrix.mtx.gz).

Duplicate identities are reported and block the import instead of overwriting silently.

One deliberate behaviour change, please look at this

<Sample>_S<n>_L<lane>_<read>_001 — canonical Illumina naming — is now recognised, so A_S7_L001_R1_001.fastq.gz gives sample A, not A_S7. _S<n> is the sample number bcl2fastq assigns; no well-known pattern claimed it, so it was being absorbed into {{Sample}}.

Synolo needs this: folder and file name both carry _S<n>, and without it their samples come out as Sel78_R1_02_S3.

Three existing inference tests changed expectation. All three inputs are real 10x file names (10k_PBMC_..._gex_1_S7_L001_R1_001.fastq.gz) where the new answer is the better one — I checked each rather than just re-baselining. A user who wants the number in the name can still say so in the pattern field. Flagging it because it changes inferred sample names for existing bcl2fastq/BaseSpace imports.

Verification

pnpm check clean (types, lint, format). 78 tests pass — 41 in file_name_parser.test.ts (7 new, including the real Synolo tree and a generic-basename tree) and 11 new in datasets.test.ts covering the prefix arithmetic and duplicate detection.

Not verified in a running app — no desktop instance available. The pattern engine is where the risk is, and it's covered by tests.

Greptile Summary

This PR makes dataset imports path-aware and prevents detected identity collisions.

  • Relative file path: The path remaining after removing the selection’s longest common directory; it now replaces the bare filename as the pattern-matching input.
  • Pattern matcher: A placeholder compiled into a regular expression; {{Sample}}, {{*}}, and tags are now segment-bounded, while new {{**}} can cross directories.
  • Pattern inference: Selection of a well-known pattern from observed files; it now tries flat, folder-ignored, and folder-carried sample identities and recognizes Illumina _S<n> sample numbers.
  • Sample identity: The fields used to place a file into dataset content; a new duplicate-key check reports collisions and disables import.
  • CellRanger file role: The matrix, features/genes, or barcode member of an MTX group; folder-per-sample layouts can now infer this role.

Confidence Score: 3/5

The PR should not merge until multi-round path inference remains stable and duplicate detection uses the same normalized identities as the dataset builders.

Adding files from another directory can invalidate the inferred pattern for existing files, while raw collision keys still allow equivalent read indices and CellRanger roles to overwrite one another silently.

Files Needing Attention: ui/src/dialogs/ImportDatasetDialog.vue, ui/src/dialogs/datasets.ts

Important Files Changed

Filename Overview
ui/src/dialogs/datasets.ts Adds relative-path computation and collision detection, but duplicate keys do not canonicalize values like the content builders do.
ui/src/dialogs/ImportDatasetDialog.vue Integrates path inference and collision blocking, but multi-round selections infer against a different path set than subsequent matching.
ui/src/dialogs/file_name_parser.ts Bounds ordinary matchers to path segments, adds deep matching and path inference variants, and recognizes canonical Illumina sample numbers.
ui/src/dialogs/datasets.test.ts Covers prefix arithmetic and basic duplicate identities but omits multi-round path changes and canonicalized-key collisions.
ui/src/dialogs/file_name_parser.test.ts Adds focused coverage for path matching, folder-carried identities, CellRanger trees, and Illumina naming.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Selected file handles] --> B[Normalize separators]
  B --> C[Remove common directory prefix]
  C --> D[Infer or compile path pattern]
  D --> E[Parse sample, lane, read, role, and tags]
  E --> F[Check duplicate identities]
  F -->|Unique| G[Build dataset content]
  F -->|Collision| H[Show error and block import]
Loading

Fix All in Claude Code

Prompt To Fix All With AI
### Issue 1
ui/src/dialogs/ImportDatasetDialog.vue:271-273
**Multi-round paths invalidate inference**

When a user selects files from one directory and then adds files from another, inference uses paths relative to the first batch while `useParsedFiles` recomputes every path against the full selection. The shorter common prefix introduces directory segments that the inferred segment-bounded pattern cannot match, causing previously selected files to become unmatched and either blocking the import or allowing only a partial selection to be imported.

### Issue 2
ui/src/dialogs/datasets.ts:227-228
**Raw keys miss normalized collisions**

When equivalent read indices such as `1`, `r1`, and `R1` are selected, or a CellRanger group contains both `genes.tsv` and `features.tsv`, `sampleKeyOf` treats the raw values as distinct. The content builders normalize them to the same `R1` or `features.tsv` slot, so duplicate detection permits the import and the later file silently overwrites the earlier one.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "Read sample identity from the folder, no..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

Patterns were matched against the bare file name, so a one-folder-per-sample
tree — what BaseSpace, bcl2fastq and CellRanger all produce — could not be
described at all. Where the folder was the only place a sample name appeared,
every file resolved to the same sample and getOrCreateSample collapsed them,
keeping only the last.

- Match against each file's path relative to the longest common directory of
  the selection. Recomputed over the whole set on every render rather than
  threaded through the file dialog, so it stays well defined when the
  selection spans folders or grows over several "add more files" rounds. With
  every file in one folder the remainder is the bare name, so flat imports are
  byte-identical to before.
- Bound {{Sample}}, {{*}} and tag matchers to a single path segment; add
  {{**}} for crossing them. Required, not cosmetic: a lazy `.+?` swallows the
  separator and pulls the folder into the sample name.
- Infer over file-name, folder-ignored and folder-carries-identity forms,
  taking the first that gives every file its own identity. Per-sample-folder
  FASTQ and CellRanger MTX now infer.
- Recognise <Sample>_S<n>_L<lane>_<read>_001, so the Illumina sample number
  stops being absorbed into the sample name. Three inference tests changed
  expectation; all three inputs are real 10x names where the new answer is
  the right one.
- Report files that collapse onto one identity and hold the import, rather
  than overwriting silently.
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

Comment on lines +271 to 273
const fileNames = relativeFilePaths(files);
if (data.files.length === 0) {
const inferredPattern = inferFileNamePattern(fileNames);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Multi-round paths invalidate inference

When a user selects files from one directory and then adds files from another, inference uses paths relative to the first batch while useParsedFiles recomputes every path against the full selection. The shorter common prefix introduces directory segments that the inferred segment-bounded pattern cannot match, causing previously selected files to become unmatched and either blocking the import or allowing only a partial selection to be imported.

Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/src/dialogs/ImportDatasetDialog.vue
Line: 271-273

Comment:
**Multi-round paths invalidate inference**

When a user selects files from one directory and then adds files from another, inference uses paths relative to the first batch while `useParsedFiles` recomputes every path against the full selection. The shorter common prefix introduces directory segments that the inferred segment-bounded pattern cannot match, causing previously selected files to become unmatched and either blocking the import or allowing only a partial selection to be imported.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code

Comment on lines +227 to +228
if (match.readIndex) parts.push("read=" + match.readIndex.value);
if (match.cellRangerFileRole) parts.push("role=" + match.cellRangerFileRole.value);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Raw keys miss normalized collisions

When equivalent read indices such as 1, r1, and R1 are selected, or a CellRanger group contains both genes.tsv and features.tsv, sampleKeyOf treats the raw values as distinct. The content builders normalize them to the same R1 or features.tsv slot, so duplicate detection permits the import and the later file silently overwrites the earlier one.

Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/src/dialogs/datasets.ts
Line: 227-228

Comment:
**Raw keys miss normalized collisions**

When equivalent read indices such as `1`, `r1`, and `R1` are selected, or a CellRanger group contains both `genes.tsv` and `features.tsv`, `sampleKeyOf` treats the raw values as distinct. The content builders normalize them to the same `R1` or `features.tsv` slot, so duplicate detection permits the import and the later file silently overwrites the earlier one.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code

@PoslavskySV

Copy link
Copy Markdown
Contributor Author

One rollout note that isn't obvious from this diff, so it doesn't get lost.

This PR ships only the pattern engine. The other half of the work — cross-folder selection in the file dialog, and the "Include subfolders" control — lives in PlFileDialog, which this block bundles from its own pinned @platforma-sdk/ui-vue (1.79.20 in pnpm-workspace.yaml; the built bundle carries the dialog's own strings). It does not arrive with a desktop-app upgrade.

So there's a fourth step after milaboratory/platforma#1766 publishes: bump @platforma-sdk/ui-vue in this repo's catalog. Until then S&D users get path-aware patterns but still descend folder by folder to select.

Full order: platforma#1766 → publish → bump ui-vue here → desktop SDK bump + platforma-desktop-app#524. This PR is independent of all of it and can merge first.

The dialog is built to degrade safely in the meantime: it feature-detects whether the host honours {depth} (via the echoed ListFilesResult.depth) and hides the control when it doesn't, rather than offering a silent no-op.

@PoslavskySV

Copy link
Copy Markdown
Contributor Author

Closing — implementation was premature. This feature is being specified first as a mispec corpus in docs/text; code will follow only after that spec is reviewed and approved. The branch is deleted; commits remain reachable from this closed PR if any of the analysis is wanted later.

@PoslavskySV
PoslavskySV deleted the feat/folder-per-sample-import branch July 28, 2026 10:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant