Zip members were live-only: expandable while the source was connected, but the
workspace snapshot dropped the archive (.zip became a plain file), so a
classification made inside one vanished on reopen — and copy couldn't extract it
anyway (it tried to walk the archive path as a real directory).
Now zips are first-class:
- snapshotTree/loadSnapshot persist the scanned archive subtree — zip-root +
virtual folders + members carry isVirtual/zipPath/zipEntryPath, so the tree
rebuilds on reopen and assignments inside an archive survive. An archive that
was never opened persists as a lazy 'zip' node that reopens on demand.
- scanner.ensureZipLoaded(rootHandle, zipPath) reloads an archive from the
workspace root when the in-memory cache is cold (post-restore); scanZipNode
falls back to it when a restored zip node has no live file object.
- copy.js reads a member via scanner.extractZipMember (Blob from the archive)
instead of a non-existent file handle; preview.js reloads the archive for a
restored member before opening it.
This also reconciles export/import with the snapshot: both now keep zip members,
so a round-trip no longer leaves dangling in-archive assignments.
Tests: zip subtree snapshot round-trip; copy extracts a member to the output (45).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>