# PLAN-mSD512.md Migration: `/Volumes/MicroSD-512` -> `/Volumes/photos library iCloud` Version: 2.2 (POST-INVENTORY, EXECUTABLE) | Host: macOS Mac Studio, zsh Operator: human approves | Agent: executes all phases and reports before each phase transition (revised f18, see below). Supersedes v2.0 and v1.1. Phase 0 is COMPLETE; its measured results are baked in. ## 0. Mission Copy all keep-worthy data from the slow SD card (SRC) to the external APFS volume (DST) with content-hash verification, no duplication of data already at DST, zero data loss, then repoint the two Jellyfin libraries. Optimise for the measured 8.65 MiB/s read ceiling: EVERY BYTE OF SRC IS READ AT MOST ONCE. ## 0.5 Run order (quickstart) Scripts live in /Users/patricksingletary/dev/_shared/scripts/. All artifacts land in $AUDIT = ~/msd512-audit. Run from any cwd. THE AGENT executes every script (revised f18). The operator is informed before each phase starts and approves before any phase transition. Operator may stop, override, or re-run any step at any time. The <=40-line digest per phase is the AGENT's report to the operator, not a paste-back. # 0. syntax gate - MANDATORY before every execution (rule S4) zsh -n phase1-mapping.sh && chmod +x phase1-mapping.sh # 1. mapping - fast, no SRC file reads. Re-run after each edit until # the UNMAPPED section is empty or every entry is a deliberate SKIP. ./phase1-mapping.sh $EDITOR ~/msd512-audit/mapping.tsv ./phase1-mapping.sh # repeat until clean [GATE-1] # 2. dedup oracle - DST only, parallel, minutes not hours zsh -n phase2-dstindex.sh && ./phase2-dstindex.sh # 3. copy + verify - the ONLY slow phase, ~5.8 h at 8.65 MiB/s [GATE-2] # tmux survives terminal/SSH loss; caffeinate blocks sleep and disk idle. zsh -n phase3-copy.sh tmux new -s msd512 caffeinate -dimsu ./phase3-copy.sh # detach: Ctrl-b then d reattach: tmux attach -t msd512 # watch: tail -f ~/msd512-audit/phase3.progress # interrupted? just re-run the same command. It resumes from $LEDGER # with zero SRC re-reads (rule R6). # 4. reconciliation - BLOCKING. Phase 5 is forbidden unless this is clean. zsh -n phase4-reconcile.sh && ./phase4-reconcile.sh # 5. jellyfin repoint - quit Jellyfin.app FIRST [GATE-4] zsh -n phase5-jellyfin.sh && ./phase5-jellyfin.sh # then the manual UI steps in Phase 5, TV library first. Per-phase the AGENT reports only the `===== ... =====` digest (<=40 lines, rule R5) and checks with the operator before the next phase. Phase 5's manual UI scan steps are still operator actions (see Phase 5). Phase durations, measured or estimated: | Phase | Reads SRC | Expected wall time | |----------------|-----------|---------------------------------------| | 1 mapping | no | seconds | | 2 dst index | no | < 15 min (stop and report if longer) | | 3 copy+verify | YES | ~5.8 h (176 GiB @ 8.65 MiB/s) | | 4 reconcile | no | minutes (1% spot re-hash on DST) | | 5 jellyfin | no | minutes + scan time | ## 1. Measured facts (Phase 0, do not re-derive) ``` SRC /Volumes/MicroSD-512 ExFAT, SD slot, 477 GiB, 185 GiB used DST /Volumes/photos library iCloud APFS, USB, 1.8 TiB, 1.2 TiB free BUNDLE $DST/photos-restore.photoslibrary 108,274 files / 201.4 GiB (READ-ONLY) SRC payload: 1,833 files / 175.8 GiB / mtime 2015-09 .. 2026-06 SRC composition: 455 .mp4 (150.9 GB) + 100 .mkv (37.9 GB) = ~99.6% of bytes 872 .torrent (13.6 MB), 268 .srt, 50 .jpg (1.9 MB) 901 AppleDouble ._* files, 6 .DS_Store -> EXCLUDED SRC top dirs: archive/z-New folder 333f/87.0GB (2024-04..2026-05) Jellyfin/Movie 447f/66.5GB (2015-09..2025-12) Jellyfin/TV 118f/35.3GB (2025-01..2025-09) archive/Twitch 8f/30MB QBT/0-torrents 872f/13.6MB Bundles on SRC: NONE (b6 validated - the only bundle is $BUNDLE on DST) Overlap: size-only 1,396 files but 0.0 GiB; name+size 472 files 0.0 GiB => NONE of the ~176 GiB of video exists at DST yet Read speed: 8.65 MiB/s sequential (small-file bench invalid, n=3) Projection: ~5.8 h for one full read pass DST split: in-bundle 108,274f / 201.4 GiB; loose 3,926f / 462.8 GiB Jellyfin: 10.11.11, stopped, datadir 329 MB root/default/Movies/Movie.mblink -> /Volumes/MicroSD-512/Jellyfin/Movie root/default/TV/TV1.mblink -> /Volumes/MicroSD-512/Jellyfin/TV (video.mblink and collections.mblink are unrelated, do not touch) Scan errors: none (find/stat error logs empty) ``` ## 2. Resolved decisions (do not re-ask the operator) ``` GATE-0 imaging DECLINED. Only 37% of the card is occupied and the payload is large sequential video; a full-device image would read 477 GiB (~15.7 h) vs 176 GiB (~5.8 h) of real data. Read SRC directly, concurrency 1. Q-NEW bundle-only dupes SKIP + log to in-bundle-only.tsv. Moot in practice: only 50 tiny JPEGs could possibly be affected. a4 already-copied data AVOID recopy via content hash, but the measured overlap is ~0 GiB, so expect ~176 GiB of genuine new data. c8 move vs copy COPY. Never delete from SRC. Emit delete-candidates only. c9 layout MIRROR. Reorg is deferred to PLAN-REORG. c10 hashing b3sum > xxh128sum > shasum -a 256. Pick once, record it. c11 collisions Quarantine to $DST/_CONFLICTS/<relpath>. b7 sidecars .srt/.nfo/.jpg follow their media, exempt from dedup. e16 watch state NOT required. No DB surgery under any circumstances. f18 execution AGENT EXECUTES the scripts itself and reports a <=40-line digest per phase. Agent checks with the operator BEFORE starting each phase and before moving to the next phase. Operator may override any proposed action. ``` ## 3. Non-negotiable rules ``` R1 SRC is READ-ONLY for this entire plan. No write, move, rename, delete, chmod, chown, or touch on SRC. Deletion is a separate future task, operator-only. R2 $BUNDLE is READ-ONLY and is used ONLY as a dedup oracle. Never write into it, never open it with Photos.app during migration, never make it a copy target, never point a Jellyfin library at it or at the $DST root. R3 Each SRC byte is read once. Hash in-stream while copying. Forbidden: rsync --checksum, any "hash pass then copy pass", any second verification read of SRC. Verification re-reads happen on the DST side only. R4 Concurrency against SRC is exactly 1. Parallelism against DST/internal is fine. R5 Bulk output goes to $AUDIT files. Chat receives digests of <=40 lines only. Never paste manifests, file lists, or hash tables into the conversation. R6 Every phase is reuse-first and resumable. If a ledger entry exists for a file, that file is NEVER re-read from SRC. Rebuilding requires an explicit --force. R7 I/O errors are logged and retried once at end-of-phase, never fatal mid-run. R8 Jellyfin: NEVER edit *.db, *.db-wal, *.db-shm, or *.db.bak* with a text editor or with sqlite. 10.11 replaced library.db with jellyfin.db and a new schema; all pre-10.11 path-rewrite recipes are invalid. Path changes go through .mblink files or the web UI only. ``` ## 4. Script hygiene (binding on every generated script) ``` S1 Generated scripts are PURE ASCII. No em/en dashes, ellipses, curly quotes, or non-breaking spaces, including in comments and print strings. S2 Every awk/sed/perl program is a SINGLE LINE. If too long, write it to $AUDIT/prog.awk and call awk -f. S3 No apostrophes inside single-quoted strings. S4 Operator runs `zsh -n