Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ jobs:
"- Tesseract OCR is auto-installed or uses bundled tesseract.js fallback.",
"- Use **Download Base Models** after install to enable AI features.",
'- **Windows:** the Setup installer is the recommended download and updates itself. The `-win-x64.zip` runs without installing: extract it once to a short folder such as `C:\StratoSort` and start `StratoSort Stack.exe`; it does not auto-update.',
('- **macOS:** download `StratoSortStack-{0}-mac-arm64.dmg` for a Mac with Apple Silicon (M1 or later) or `StratoSortStack-{0}-mac-x64.dmg` for an Intel Mac. To check, open the Apple menu and choose **About This Mac**: "Chip" means Apple Silicon, "Processor" means Intel.' -f $version),
$releaseIntegrityNote,
$macIntegrityNote,
"- See the [Install Guide](https://stratosort.com/docs/INSTALL_GUIDE.html) for Windows, Mac & Linux — one download, no CLI.",
Expand Down
55 changes: 55 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,29 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Fixed

- **Two identical files in one folder are no longer both sent to the trash when one run renames or
moves them.** Each was taken for a copy of the other that was already in place, so both were
removed instead of moved, and the run could then fail and be undone with the two files missing
from the folder. A file the same run has still to move no longer counts as a copy already in
place, and renaming a file inside its own folder never removes it. A file removed this way went to
the system trash.
- **An organize run that did not move everything says so and stays on the Organize screen.** A
banner above the file list says how many files moved, how many did not and why, or that the whole
run was undone and the files are back where they were. Before, a run that failed part-way or was
undone could move on to the Complete screen as if it had worked. An identical copy that was
removed instead of moved is listed as a duplicate, with the copy that was kept, and is no longer
counted as a moved file.
- **Files that are already in the folder chosen for them are pointed out before the run.** When a
folder you sort from is also one of your Smart Folders, files matched to it stay where they are
(or are renamed there). A notice on the Organize screen and a mark in Preview now say which, so a
run no longer reports files as organized while nothing appears anywhere new.
- **A folder on an external or second drive can be a Smart Folder, can be scanned and can have
folders created in it.** These were refused for any folder outside your user folder, with a
message about a link or short name. Folders inside system folders, or reached through a link that
leads into one, are still refused. A folder that is not inside a Smart Folder or Documents and was
not picked in a file dialog is confirmed once in a prompt.
- **A folder the app will not scan, or cannot read, gives the reason.** Choosing or dropping such a
folder on the Discover screen said that no files were found.
- **An image that arrives just as a text model starts loading no longer starts the vision runtime
beside it.** The image looked for a loading model before the load began and found none. The vision
runtime now checks the memory budget again at the moment it starts; if a load began meanwhile, the
Expand All @@ -25,6 +48,38 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- **A text model larger than the memory budget no longer unloads models that hold no GPU memory.**
An embedding model on the CPU and Whisper on the CPU were unloaded to make room, which freed
nothing, and were loaded again for the next file.
- **A short category is no longer taken for a misspelling of a short Smart Folder name.** Any name
within two letters of a folder's name counted as that folder, so a file labeled "SEO" could be
sent to a folder named "Web" or "Dev". The allowance now grows with the length of the name: none
under four letters, one letter per four, three at most.
- **A file with a generic category ("document", "file", "image") is no longer sent to the first
Smart Folder in the list when there is no Uncategorized folder.** It is left without a match, and
the other suggestions decide.
- **A suggestion says what named its folder.** "Using Date-Based organization method" described a
folder that was named after the file's category, its type or its purpose; the explanation now says
which.
- **Editing a Smart Folder's description past its first 64 characters is used by the next
analysis.** The saved answer for a file was looked up by the start of each description only, so a
change further in returned the answer for the old wording.
- **Preview on the Organize screen says why when it cannot open.** A preview that failed or was
refused said only "Nothing to preview."; it now gives the reason. The button shows that it is
working, and pressing it twice no longer asks for two previews.
- **Preview marks files that would get the same name in the same folder.** A move with such files
does not start; Preview now shows which files they are and keeps its Move button off until they
have different names.
- **Analysis progress shows one figure for files and one for percent.** It read "Analyzing 3 of 10"
with the percentage twice; it now reads "Analyzing 3 of 10 files" and "30% complete".
- **An analysis that continues after a restart says so.** The notice now reads "Continuing the
analysis that was interrupted" with the number of files left, and stays up for six seconds.
- **Model download speed and time left follow the current speed.** They were averaged from the start
of the download and shown after the first second, so the first figures could be far off and a
change in speed took a long time to show. The speed is now measured over the last ten seconds and
shown once three seconds of data have arrived; until then the row reads "Calculating speed…".

### Documentation

- **The Install Guide, the README and each release's notes say which Mac file to download:**
`mac-arm64` for Apple Silicon, `mac-x64` for Intel, and how to check in About This Mac.

## [5.2.0] - 2026-11-10

Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ tree.
`ci`) enforces a monotonic floor and blocks new .js under `COVERED_DIRS` (`src/shared`,
`src/preload`, `src/main/{ipc,services,analysis,errors,utils,workers,transports,core}`,
`src/renderer/{services,hooks,phases,store,utils,components}` today (every `.js` under `src/` is
now opted in — 495/495 as of 2026-11-11, and `GLOBAL_DIRECTIVE_FLOOR` matches at 495) — see
now opted in — 496/496 as of 2026-11-12, and `GLOBAL_DIRECTIVE_FLOOR` matches at 496) — see
`scripts/verify-checkjs-ratchet.js` for the authoritative list) from landing without the
directive. When adding a file to a covered dir: put `// @ts-check` on line 1 and fix the errors
before commit. When opting in a new file elsewhere: bump `GLOBAL_DIRECTIVE_FLOOR` in
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ no telemetry.
## Install

1. **Download** the installer for your system from
[GitHub Releases](https://github.com/iLevyTate/StratoSortStack/releases).
[GitHub Releases](https://github.com/iLevyTate/StratoSortStack/releases). On a Mac, take
`…-mac-arm64.dmg` for Apple Silicon (M1 or later) or `…-mac-x64.dmg` for Intel; Apple menu →
**About This Mac** shows which one you have.
2. **Launch it** and choose a model profile. StratoSort downloads its AI models once: about 7 GB for
Base, or about 10 GB for Better Quality.
3. **Add a folder** and review the suggestions.
Expand Down
13 changes: 13 additions & 0 deletions docs/INSTALL_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@ download the single file that matches your operating system.
Replace **X.X.X** with the version number shown on the Releases page (e.g. `5.2.0`). The Windows
arm64 build ships without bundled FFmpeg, so audio/video analysis there needs FFmpeg on your `PATH`.

### Mac -- Apple Silicon or Intel?

Open the Apple menu and choose **About This Mac**.

| About This Mac shows | Your Mac is | Download this file |
| :--------------------------------- | :------------ | :------------------------------------ |
| **Chip**: Apple M1, M2, M3, M4 ... | Apple Silicon | `StratoSortStack-X.X.X-mac-arm64.dmg` |
| **Processor**: ... Intel Core ... | Intel | `StratoSortStack-X.X.X-mac-x64.dmg` |

The `arm64` file does not open on an Intel Mac. On an Apple Silicon Mac, take the `arm64` file: only
that build runs the local AI on the Mac's GPU. The `.zip` files with the same names hold the same
app and are what the in-app updater downloads; you do not need them for a first install.

### Windows -- installer vs zip

| File | What it does | Recommended? |
Expand Down
19 changes: 10 additions & 9 deletions docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,17 +292,18 @@ Then run a small batch first and review outcomes before scaling up.
- Review naming defaults in **Settings -> Default Locations**.
- Confirm convention/date/case/separator values.

### "That folder is inside a link that points outside your allowed locations."
### "That folder leads into a system folder through a link or short name."

You can see this when you use **Create directory** on a Smart Folder marked **Missing** in Setup. On
this computer, StratoSort creates folders only inside your home folder, Documents, Downloads or
Desktop, and never inside a system folder. It goes by where the folder really is, not by the path as
written. If the path runs through a shortcut-style link (a symbolic link, or a junction on Windows)
that leads somewhere outside those places, or into a system folder, the folder is refused.
You can see this when you use **Create directory** on a Smart Folder marked **Missing** in Setup.
StratoSort creates folders on any drive, an external one included, but never inside a system folder.
It goes by where the folder really is, not by the path as written. If the path runs through a
shortcut-style link (a symbolic link, or a junction on Windows) or a Windows short name that leads
into a system folder, the folder is refused.

- Edit the Smart Folder to use the real folder the link points to.
- If that real folder is outside those places, create it yourself in File Explorer or Finder, then
choose it as the Smart Folder's location with **Browse**.
- Edit the Smart Folder to use a folder outside the system folders.

A folder that is not inside one of your Smart Folders or Documents is created after you confirm it
in a prompt. If the path runs through a link, the prompt names the folder the link really leads to.

### Need to see logs

Expand Down
2 changes: 1 addition & 1 deletion scripts/verify-checkjs-ratchet.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const COVERED_DIRS = [
'src/renderer/utils',
'src/renderer/components'
];
const GLOBAL_DIRECTIVE_FLOOR = 495;
const GLOBAL_DIRECTIVE_FLOOR = 496;
// ────────────────────────────────────────────────────────────────────────

const DIRECTIVE_SCAN_LINES = 5;
Expand Down
84 changes: 64 additions & 20 deletions src/main/ipc/files/batchOrganizeHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,13 @@ async function handleBatchOrganize(params) {
}
const batchOperationTotal = batch.operations.length;
const limit = pLimit(effectiveConcurrency);
// Files this batch has still to move. The duplicate check must not
// take one of them for a copy that is already filed: it may be moved
// or renamed away later in this batch. Built here because each
// operation rewrites its paths to the validated form once it starts.
const pendingSources = batch.operations
.filter((op) => op.status !== 'done' && typeof op.source === 'string')
.map((op) => op.source);

// Abort reasons the USER deliberately triggered (renderer buttons).
// Anything else on the signal — 'ipc-timeout', 'shutdown' (abortAll),
Expand Down Expand Up @@ -814,7 +821,13 @@ async function handleBatchOrganize(params) {
const { signal } = abortController;
if (signal.aborted) startGrace();
else signal.addEventListener('abort', startGrace, { once: true });
const movePromise = performFileMove(op, log, computeFileChecksum, signal);
const movePromise = performFileMove(
op,
log,
computeFileChecksum,
signal,
pendingSources
);
try {
const outcome = await Promise.race([movePromise, abandoned]);
if (outcome !== MOVE_ABANDONED) return outcome;
Expand Down Expand Up @@ -909,17 +922,36 @@ async function handleBatchOrganize(params) {
}

if (moveResult && moveResult.skipped) {
skippedCount++;
const resolvedDestination = moveResult.destination || op.destination;
const currentFileName = path.basename(op.source);
results.push({
success: true,

// Verify state is consistent (dest exists, source gone) before
// anything reports the file as filed.
try {
await verifyMoveCompletion(op.source, resolvedDestination, log);
} catch (verifyError) {
// The source was removed as a duplicate and the copy it
// duplicated cannot be confirmed. Rollback has nothing to
// copy back, so the message is where the user learns that the
// file is in the trash.
if (moveResult.sourceRemoval === 'trashed' && verifyError instanceof Error) {
verifyError.message +=
'. The original was moved to the system Trash / Recycle Bin as a duplicate and can be restored from there.';
}
throw verifyError;
}

consecutivePermissionFailures = 0;
// A duplicate skip still mutates disk state by deleting the source file.
// Track it so a later critical failure can restore the original path.
completedOperations.push({
index: i,
source: op.source,
destination: resolvedDestination,
operation: op.type || 'move',
skipped: true,
reason: moveResult.reason
originalDestination: op._originalDestination,
rollbackAction: 'restore_duplicate_source'
});
renewBatchLock(batchId);

// One info line per skipped file in every build: the Live
// Activity feed shows "Skipped <name> (already in place)". File
Expand All @@ -932,20 +964,22 @@ async function handleBatchOrganize(params) {
destination: path.basename(resolvedDestination)
});

// Verify state is consistent (dest exists, source gone)
await verifyMoveCompletion(op.source, resolvedDestination, log);
// Rollback started while this skip was being verified: it
// restores the source and reports the row (see the same check
// on the move path below).
if (shouldRollback) return;

consecutivePermissionFailures = 0;
// A duplicate skip still mutates disk state by deleting the source file.
// Track it so a later critical failure can restore the original path.
completedOperations.push({
index: i,
skippedCount++;
results.push({
success: true,
source: op.source,
destination: resolvedDestination,
originalDestination: op._originalDestination,
rollbackAction: 'restore_duplicate_source'
operation: op.type || 'move',
skipped: true,
reason: moveResult.reason,
// 'trashed' or 'deleted': where the source went.
sourceRemoval: moveResult.sourceRemoval
});
renewBatchLock(batchId);

// Not awaited (see the success path below for why).
void processingState?.markOrganizeOpDone?.(batchId, i, {
Expand Down Expand Up @@ -1629,8 +1663,11 @@ function reconcileRolledBackResults(rollbackOutcome, completedOperations, rollba
* @param {LoggerLike} log
* @param {typeof computeFileChecksum} checksumFn
* @param {AbortSignal|null} [abortSignal]
* @param {string[]} [pendingSources] - Every source the caller's batch has
* still to move (the same array for each call), so the duplicate check does
* not count one of them as a copy already at the destination.
*/
async function performFileMove(op, log, checksumFn, abortSignal = null) {
async function performFileMove(op, log, checksumFn, abortSignal = null, pendingSources) {
// This prevents creating numbered copies of files that already exist.
const duplicateResult = await handleDuplicateMove({
sourcePath: op.source,
Expand All @@ -1639,7 +1676,8 @@ async function performFileMove(op, log, checksumFn, abortSignal = null) {
logger: log,
logPrefix: '[FILE-OPS]',
dedupContext: 'batchOrganize',
removeEmbeddings: removeEmbeddingsForPathBestEffort
removeEmbeddings: removeEmbeddingsForPathBestEffort,
pendingSources
});
if (duplicateResult) return duplicateResult;

Expand Down Expand Up @@ -2431,9 +2469,15 @@ async function previewBatchOrganize({ operation, log }) {
// PREVIEW_MAX_NUMERIC_ATTEMPTS so the preview stays snappy even when the
// destination directory is densely packed. The real executor still
// walks up to MAX_NUMERIC_RETRIES on collision before UUID-fallback.
//
// A file that is already at its destination (same path, or the same name
// in another case where the filesystem ignores case) occupies that path
// itself, so the executor's move does not collide with anything.
let finalDestination = op.destination;
let collision = false;
if (await pathExistsSilent(op.destination)) {
const alreadyAtDestination =
_canonicalizeForOverlapCheck(op.source) === _canonicalizeForOverlapCheck(op.destination);
if (!alreadyAtDestination && (await pathExistsSilent(op.destination))) {
collision = true;
conflicts += 1;
const ext = path.extname(op.destination);
Expand Down
Loading
Loading