Name-link starvation.unlinked_name_candidates now takes an id > after_id cursor and returns ids. sweep_name_links keeps a per-library in-memory cursor (up to 4 pages of 500 per call), wraps to 0 at the end and continues, so unresolvable matches no longer block later candidates. Test: 120 orphans with a 50-row page, then a linkable pair is still linked.
Stranded annotations / unscoped canonical path.
canonical_rel_path takes Option<library_id> and scopes the version row by it. The tag and favorite DAOs only know a rel_path (library-agnostic by design), so they pass None. As the scoping that applies to them, a path that also names a non-version file in any library is never redirected, since annotations are keyed by rel_path alone.
link_version (so ingest and link_versions alike) merges favorites and tags from the version's path into the original's inside the link transaction. Rows are unioned, duplicates dropped, and the version path's rows removed.
Not covered: a link made while the original was not yet indexed does not migrate later when the original appears.
Hidden-only folders. The non-recursive /photos listing drops a dir when it has indexed rows and all of them are hidden versions. Unless include_versions=true. One indexed prefix-range query per dir, no walk. Dirs with no indexed rows are kept.
Minor.
Display change and reels: no change needed. Reels build from memory items whose path is always the original's, and never use the display file. The reason is documented in CLAUDE.md.
POST /image/exif/date and /clear on a version path now apply to the original's row (new original_location_of), and the response describes the original.
link_versions --reviewed now also checks original_hash against the index (hash must exist, and the TSV's original path must not hold other bytes). Stale rows are refused and listed on stderr.
Tests: DATABASE_URL=":memory:" cargo test (1036 pass), cargo fmt --check and cargo clippy --all-targets are clean.
Closes #194
1. **Name-link starvation.** `unlinked_name_candidates` now takes an `id > after_id` cursor and returns ids. `sweep_name_links` keeps a per-library in-memory cursor (up to 4 pages of 500 per call), wraps to 0 at the end and continues, so unresolvable matches no longer block later candidates. Test: 120 orphans with a 50-row page, then a linkable pair is still linked.
2. **Stranded annotations / unscoped canonical path.**
- `canonical_rel_path` takes `Option<library_id>` and scopes the version row by it. The tag and favorite DAOs only know a rel_path (library-agnostic by design), so they pass `None`. As the scoping that applies to them, a path that also names a non-version file in any library is never redirected, since annotations are keyed by rel_path alone.
- `link_version` (so ingest and `link_versions` alike) merges favorites and tags from the version's path into the original's inside the link transaction. Rows are unioned, duplicates dropped, and the version path's rows removed.
- Not covered: a link made while the original was not yet indexed does not migrate later when the original appears.
3. **Hidden-only folders.** The non-recursive `/photos` listing drops a dir when it has indexed rows and all of them are hidden versions. Unless `include_versions=true`. One indexed prefix-range query per dir, no walk. Dirs with no indexed rows are kept.
4. **Minor.**
- Display change and reels: no change needed. Reels build from memory items whose `path` is always the original's, and never use the display file. The reason is documented in CLAUDE.md.
- `POST /image/exif/date` and `/clear` on a version path now apply to the original's row (new `original_location_of`), and the response describes the original.
- `link_versions --reviewed` now also checks `original_hash` against the index (hash must exist, and the TSV's original path must not hold other bytes). Stale rows are refused and listed on stderr.
Tests: `DATABASE_URL=":memory:" cargo test` (1036 pass), `cargo fmt --check` and `cargo clippy --all-targets` are clean.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #194
unlinked_name_candidatesnow takes anid > after_idcursor and returns ids.sweep_name_linkskeeps a per-library in-memory cursor (up to 4 pages of 500 per call), wraps to 0 at the end and continues, so unresolvable matches no longer block later candidates. Test: 120 orphans with a 50-row page, then a linkable pair is still linked.canonical_rel_pathtakesOption<library_id>and scopes the version row by it. The tag and favorite DAOs only know a rel_path (library-agnostic by design), so they passNone. As the scoping that applies to them, a path that also names a non-version file in any library is never redirected, since annotations are keyed by rel_path alone.link_version(so ingest andlink_versionsalike) merges favorites and tags from the version's path into the original's inside the link transaction. Rows are unioned, duplicates dropped, and the version path's rows removed./photoslisting drops a dir when it has indexed rows and all of them are hidden versions. Unlessinclude_versions=true. One indexed prefix-range query per dir, no walk. Dirs with no indexed rows are kept.pathis always the original's, and never use the display file. The reason is documented in CLAUDE.md.POST /image/exif/dateand/clearon a version path now apply to the original's row (neworiginal_location_of), and the response describes the original.link_versions --reviewednow also checksoriginal_hashagainst the index (hash must exist, and the TSV's original path must not hold other bytes). Stale rows are refused and listed on stderr.Tests:
DATABASE_URL=":memory:" cargo test(1036 pass),cargo fmt --checkandcargo clippy --all-targetsare clean.🤖 Generated with Claude Code