Skip to content

Merge changes from PR54 + some fixes to PR54#57

Merged
AlanRockefeller merged 9 commits intomainfrom
test
Mar 24, 2026
Merged

Merge changes from PR54 + some fixes to PR54#57
AlanRockefeller merged 9 commits intomainfrom
test

Conversation

@AlanRockefeller
Copy link
Copy Markdown
Owner

@AlanRockefeller AlanRockefeller commented Mar 24, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Automatic migration of legacy metadata keys to stable path-based keys for more reliable metadata recognition.
    • Consistent per-image metadata lookups so flag toggles and upload/stack status behave correctly.
    • Improved cross-platform path normalization so images with same names in different folders are handled distinctly.
  • Reliability

    • Safer log directory selection and fault-tolerant file-logging fallback to console when disk/write locations are unavailable.
  • Tests

    • Added regression tests covering metadata migration, key collisions, and RAW/JPG visibility.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 24, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ffc00045-f8ca-4d62-a77f-ce45b6aeffa1

📥 Commits

Reviewing files that changed from the base of the PR and between 6cd43bf and 437efb1.

📒 Files selected for processing (1)
  • faststack/thumbnail_view/model.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • faststack/thumbnail_view/model.py

Walkthrough

Metadata access was changed from stem-based keys to stable path-aware keys. SidecarManager now accepts Path/str refs, normalizes/migrates legacy keys, and Sidecar lookups across app, thumbnail model, and folder stats were updated to use full paths. Logging and tests were updated accordingly.

Changes

Cohort / File(s) Summary
Sidecar & path utilities
faststack/io/sidecar.py, faststack/io/utils.py
Introduce stable-key helpers (metadata_key_for_path, _lookup_keys, _stable_key_from_key), make SidecarManager.get_metadata accept Union[str, Path], perform legacy-key migration/normalization, and normalize path keys to use forward slashes.
App controller metadata wiring
faststack/app.py
Replace stem-based metadata access with Path-based lookups; add metadata_key_fn propagation to ThumbnailModel; update bulk metadata mapping and all toggle/flag operations to call sidecar.get_metadata(image_path).
Thumbnail model & indexing
faststack/thumbnail_view/model.py
Change get_metadata_callback typing to accept `Path
Folder stats / coverage
faststack/thumbnail_view/folder_stats.py
Lookup metadata by filename-with-extension first, then fall back to stem to support legacy entries when computing coverage buckets.
Logging / app data dir
faststack/logging_setup.py
Make app-data directory selection and log-file creation fault-tolerant with multiple candidate locations, writable checks, and fallback to temp directory or console-only logging.
Tests
faststack/tests/test_sidecar.py, faststack/tests/test_jump_to_last_uploaded.py, faststack/tests/thumbnail_view/test_folder_stats.py
Update tests to pass Path refs and assert on computed metadata keys; add regression tests for legacy-key migration, RAW/JPG identity, namespace collisions, dotted keys, and coverage-bucket handling.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant UI as "UI / Controller"
  participant Model as "ThumbnailModel"
  participant Sidecar as "SidecarManager"
  participant FS as "Filesystem"

  UI->>Model: request metadata for image (Path)
  Model->>Sidecar: get_metadata(image_path)
  Sidecar->>Sidecar: compute stable_key + candidate_keys
  Sidecar->>Sidecar: if legacy key present -> migrate to stable_key
  Sidecar->>FS: (optional) check filesystem for path normalization
  Sidecar-->>Model: return EntryMetadata (create if allowed)
  Model-->>UI: present metadata / update UI
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • A few minor bug fixes #47 — Modifies ThumbnailModel metadata/path lookup surface; strongly related to metadata_key_fn changes.
  • fix minor bugs found by CodeRabbit #36 — Touches AppController metadata lookup and _get_metadata_dict changes relevant to Path-based lookups.
  • Version 0.1 #1 — Earlier sidecar/key-handling work that this PR extends (stable metadata key and get_metadata behavior).
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title references merging changes from 'PR54' with fixes, but lacks specificity about the actual technical changes—migration from stem-based to path-based metadata key handling across multiple modules. Consider a more descriptive title such as 'Refactor metadata lookup to use path-based keys instead of stems' or 'Migrate sidecar metadata keying from stems to normalized paths' to better convey the main technical change.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 80.28% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6cd43bf4ee

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread faststack/io/utils.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
faststack/app.py (1)

1810-1815: ⚠️ Potential issue | 🟠 Major

Keep read-side metadata access schema-tolerant.

_get_bulk_metadata_map() and toggle_todo() already treat metadata fields as optional, but these paths still do direct meta.<field> reads. On older sidecars that predate newer fields, that can either collapse the UI to all-false metadata or raise while scanning uploaded/favorite images or rendering the metadata panel.

🛠️ Minimal hardening pattern
- uploaded = meta.uploaded if meta else False
+ uploaded = getattr(meta, "uploaded", False) if meta else False

- "todo": meta.todo,
- "todo_date": (meta.todo_date or "") if meta else "",
+ "todo": getattr(meta, "todo", False),
+ "todo_date": (getattr(meta, "todo_date", "") or "") if meta else "",

- if meta and meta.favorite:
+ if meta and getattr(meta, "favorite", False):

Mirror that same getattr(..., default) pattern for the other flag/date reads in these methods.

Also applies to: 2126-2148, 2360-2401, 2621-2624, 2676-2679, 7362-7365

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@faststack/app.py` around lines 1810 - 1815, The code directly accesses
metadata fields (e.g., meta.uploaded) which can break on older sidecars; change
these reads to use getattr with safe defaults (for boolean flags use
getattr(meta, "uploaded", False) or getattr(meta, "favorite", False), for dates
use getattr(meta, "last_viewed", None) or an appropriate default) in the
affected methods (_get_bulk_metadata_map, toggle_todo and the blocks around the
shown diffs) so missing attributes don't raise and older schemas render
correctly.
🧹 Nitpick comments (2)
faststack/logging_setup.py (1)

53-53: Consider using an absolute path for the local fallback.

Path.cwd() / "var" / "appdata" captures the working directory at call time. If the process changes its working directory before this function runs, the candidate path may resolve unexpectedly. Consider whether this relative-path candidate is intentional or if an absolute path (e.g., relative to the module/package location) would be more predictable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@faststack/logging_setup.py` at line 53, The current fallback candidate uses
Path.cwd() (candidates.append(Path.cwd() / "var" / "appdata")) which depends on
the process working directory; change it to build an absolute path anchored to
the module/package (for example using Path(__file__).resolve().parent or the
package root) so the candidate is deterministic regardless of CWD changes—update
the code that appends to candidates to use that module-root based Path instead
of Path.cwd().
faststack/io/utils.py (1)

23-23: Centralize path-key construction on this helper.

Now that normalize_path_key() forces / separators, the remaining raw os.path.normcase(os.path.abspath(...)) call sites in faststack/thumbnail_view/model.py (_get_loupe_index_for_entry() and remove_rows_by_path()) are easy to drift from the canonical form. I'd switch those to this helper too so Windows lookups and hashes stay coupled.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@faststack/io/utils.py` at line 23, Normalize path-key construction by
replacing raw os.path.normcase(os.path.abspath(...)).replace("\\", "/") usages
with the canonical helper normalize_path_key(). Update
_get_loupe_index_for_entry() and remove_rows_by_path() in
thumbnail_view/model.py to call normalize_path_key(path) for all path
normalization/lookup and hashing operations, and add the necessary import for
normalize_path_key at the top of the module so Windows separators and casing are
handled consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@faststack/io/sidecar.py`:
- Around line 156-179: The current lookup in sidecar.py promotes only the first
matching alias into self.data.entries[stable_key], which can drop fields from
other aliases; update the logic around the candidate_keys/stable_key resolution
(the block that reads self.data.entries.get(stable_key), loops candidate_keys,
and the fallback using _stable_key_from_key) to collect all matching
candidate_meta objects whose keys normalize to stable_key, merge their metadata
fields deterministically (or detect conflicting keys and surface an error),
remove all original alias keys from self.data.entries, and write the merged meta
once to self.data.entries[stable_key]; also add a regression test that seeds two
legacy keys (e.g., "photo.CR2" and "photo.jpg") with different fields and
asserts the merged canonical "photo" contains both sets of fields (or that a
conflict is reported).
- Around line 204-225: The _lookup_keys function currently uses path.stem as a
migration candidate for nested paths which lets a bare-stem key like "photo" be
treated as a match for Path("a/photo.jpg"); change the logic in _lookup_keys so
the bare-stem fallback (path.stem) is only returned when the path is top-level
(no parent or parent is "."/empty), both in the Path branch and the string-path
branch, and otherwise omit the bare-stem candidate for nested paths to avoid
migrating top-level metadata in get_metadata; also add a regression test that
creates a canonical "photo" metadata entry and then calls
get_metadata/_lookup_keys for Path("a/photo.jpg") asserting the nested lookup
does not migrate or return the bare "photo" entry.

In `@faststack/thumbnail_view/model.py`:
- Around line 552-555: The code treats an empty metadata_map as falsy and falls
back to per-image _get_metadata, causing bulk-refresh to be bypassed; change the
conditional to check for metadata_map is not None (or explicitly "metadata_map
is not None") instead of truthiness so that an empty dict from
_get_bulk_metadata_map() is accepted and used when _metadata_key_fn is present
(affect the blocks using metadata_map and self._metadata_key_fn, and repeat the
same change at the other occurrence around the _get_metadata usage at lines
corresponding to the second block).

---

Outside diff comments:
In `@faststack/app.py`:
- Around line 1810-1815: The code directly accesses metadata fields (e.g.,
meta.uploaded) which can break on older sidecars; change these reads to use
getattr with safe defaults (for boolean flags use getattr(meta, "uploaded",
False) or getattr(meta, "favorite", False), for dates use getattr(meta,
"last_viewed", None) or an appropriate default) in the affected methods
(_get_bulk_metadata_map, toggle_todo and the blocks around the shown diffs) so
missing attributes don't raise and older schemas render correctly.

---

Nitpick comments:
In `@faststack/io/utils.py`:
- Line 23: Normalize path-key construction by replacing raw
os.path.normcase(os.path.abspath(...)).replace("\\", "/") usages with the
canonical helper normalize_path_key(). Update _get_loupe_index_for_entry() and
remove_rows_by_path() in thumbnail_view/model.py to call
normalize_path_key(path) for all path normalization/lookup and hashing
operations, and add the necessary import for normalize_path_key at the top of
the module so Windows separators and casing are handled consistently.

In `@faststack/logging_setup.py`:
- Line 53: The current fallback candidate uses Path.cwd()
(candidates.append(Path.cwd() / "var" / "appdata")) which depends on the process
working directory; change it to build an absolute path anchored to the
module/package (for example using Path(__file__).resolve().parent or the package
root) so the candidate is deterministic regardless of CWD changes—update the
code that appends to candidates to use that module-root based Path instead of
Path.cwd().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 066240fe-7580-474f-bc96-4fd9be18f214

📥 Commits

Reviewing files that changed from the base of the PR and between c65442a and 6cd43bf.

📒 Files selected for processing (9)
  • faststack/app.py
  • faststack/io/sidecar.py
  • faststack/io/utils.py
  • faststack/logging_setup.py
  • faststack/tests/test_jump_to_last_uploaded.py
  • faststack/tests/test_sidecar.py
  • faststack/tests/thumbnail_view/test_folder_stats.py
  • faststack/thumbnail_view/folder_stats.py
  • faststack/thumbnail_view/model.py

Comment thread faststack/io/sidecar.py
Comment on lines +156 to +179
meta = self.data.entries.get(stable_key)
if meta is None:
for candidate_key in candidate_keys:
if candidate_key == stable_key:
continue
candidate_meta = self.data.entries.get(candidate_key)
if candidate_meta is None:
continue
meta = candidate_meta
if stable_key not in self.data.entries:
self.data.entries[stable_key] = candidate_meta
if candidate_key in self.data.entries and candidate_key != stable_key:
del self.data.entries[candidate_key]
break
if meta is None:
for existing_key, existing_meta in list(self.data.entries.items()):
if existing_key == stable_key:
continue
if self._stable_key_from_key(existing_key, check_fs=True) != stable_key:
continue
meta = existing_meta
self.data.entries[stable_key] = existing_meta
del self.data.entries[existing_key]
break
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Promoting only the first matching alias can hide metadata.

If a regressed sidecar contains both photo.CR2 and photo.jpg, this block migrates whichever alias it sees first to photo and leaves the other behind. Later lookups hit photo immediately, so fields that lived only on the skipped alias become unreachable. Please merge all aliases that normalize to the same stable_key (or surface a conflict) before finalizing the canonical entry, and add a regression that seeds both legacy keys with different fields.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@faststack/io/sidecar.py` around lines 156 - 179, The current lookup in
sidecar.py promotes only the first matching alias into
self.data.entries[stable_key], which can drop fields from other aliases; update
the logic around the candidate_keys/stable_key resolution (the block that reads
self.data.entries.get(stable_key), loops candidate_keys, and the fallback using
_stable_key_from_key) to collect all matching candidate_meta objects whose keys
normalize to stable_key, merge their metadata fields deterministically (or
detect conflicting keys and surface an error), remove all original alias keys
from self.data.entries, and write the merged meta once to
self.data.entries[stable_key]; also add a regression test that seeds two legacy
keys (e.g., "photo.CR2" and "photo.jpg") with different fields and asserts the
merged canonical "photo" contains both sets of fields (or that a conflict is
reported).

Comment thread faststack/io/sidecar.py
Comment on lines +204 to +225
def _lookup_keys(self, image_ref: Union[str, Path]) -> tuple[str, list[str]]:
"""Return (stable_key, migration_candidate_keys) for a metadata lookup."""
if isinstance(image_ref, Path):
if not image_ref.name:
return "", []
stable_key = self.metadata_key_for_path(image_ref)
full_name_key = self._metadata_filename_key(image_ref)
return stable_key, [full_name_key, image_ref.stem]

value = str(image_ref)
if not value:
return "", []

# Only treat string as a path if it contains explicit path separators.
# Dotted strings (even with image extensions like "photo.CR2") are
# treated as exact keys — migration of legacy filename keys is handled
# by the _stable_key_from_key scan in get_metadata.
if os.path.sep in value or "/" in value or "\\" in value:
path = Path(value)
stable_key = self.metadata_key_for_path(path)
full_name_key = self._metadata_filename_key(path)
return stable_key, [full_name_key, path.stem]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Don't let nested lookups steal top-level metadata.

For Path("a/photo.jpg"), _lookup_keys() currently returns ["a/photo.jpg", "photo"]. If "photo" already exists as the canonical key for a top-level file, get_metadata() will migrate that entry to "a/photo" and the root image loses its metadata. The bare-stem fallback is only safe for top-level paths; nested paths need stronger disambiguation than path.stem.

💡 Narrow the bare-stem fallback to top-level paths
     def _lookup_keys(self, image_ref: Union[str, Path]) -> tuple[str, list[str]]:
         """Return (stable_key, migration_candidate_keys) for a metadata lookup."""
         if isinstance(image_ref, Path):
             if not image_ref.name:
                 return "", []
             stable_key = self.metadata_key_for_path(image_ref)
             full_name_key = self._metadata_filename_key(image_ref)
-            return stable_key, [full_name_key, image_ref.stem]
+            candidate_keys = [full_name_key]
+            if "/" not in stable_key:
+                candidate_keys.append(image_ref.stem)
+            return stable_key, candidate_keys
 ...
         if os.path.sep in value or "/" in value or "\\" in value:
             path = Path(value)
             stable_key = self.metadata_key_for_path(path)
             full_name_key = self._metadata_filename_key(path)
-            return stable_key, [full_name_key, path.stem]
+            candidate_keys = [full_name_key]
+            if "/" not in stable_key:
+                candidate_keys.append(path.stem)
+            return stable_key, candidate_keys

Please also add a regression for an existing "photo" entry plus a lookup for Path("a/photo.jpg").

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@faststack/io/sidecar.py` around lines 204 - 225, The _lookup_keys function
currently uses path.stem as a migration candidate for nested paths which lets a
bare-stem key like "photo" be treated as a match for Path("a/photo.jpg"); change
the logic in _lookup_keys so the bare-stem fallback (path.stem) is only returned
when the path is top-level (no parent or parent is "."/empty), both in the Path
branch and the string-path branch, and otherwise omit the bare-stem candidate
for nested paths to avoid migrating top-level metadata in get_metadata; also add
a regression test that creates a canonical "photo" metadata entry and then calls
get_metadata/_lookup_keys for Path("a/photo.jpg") asserting the nested lookup
does not migrate or return the bare "photo" entry.

Comment on lines +552 to +555
if metadata_map and self._metadata_key_fn:
meta = metadata_map.get(self._metadata_key_fn(img.path), {})
elif self._get_metadata:
meta = self._get_metadata(img.path.stem)
meta = self._get_metadata(img.path)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Treat an empty metadata_map as present.

An empty bulk map is the normal output of faststack/app.py:_get_bulk_metadata_map() when a folder has images but no sidecar flags yet. With the current truthiness check, {} falls back to _get_metadata() here, which reintroduces per-image lookups and lets the bulk-refresh path stop using a single metadata source.

🛠️ Minimal fix
-                        if metadata_map and self._metadata_key_fn:
+                        if metadata_map is not None and self._metadata_key_fn:
                             meta = metadata_map.get(self._metadata_key_fn(img.path), {})
@@
-            if metadata_map and self._metadata_key_fn:
+            if metadata_map is not None and self._metadata_key_fn:
                 meta = metadata_map.get(self._metadata_key_fn(img.path), {})

Also applies to: 614-615

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@faststack/thumbnail_view/model.py` around lines 552 - 555, The code treats an
empty metadata_map as falsy and falls back to per-image _get_metadata, causing
bulk-refresh to be bypassed; change the conditional to check for metadata_map is
not None (or explicitly "metadata_map is not None") instead of truthiness so
that an empty dict from _get_bulk_metadata_map() is accepted and used when
_metadata_key_fn is present (affect the blocks using metadata_map and
self._metadata_key_fn, and repeat the same change at the other occurrence around
the _get_metadata usage at lines corresponding to the second block).

@AlanRockefeller
Copy link
Copy Markdown
Owner Author

@coderabbitai 1. Sidecar alias merging (lines 156-179) — NOT a real bug.

The concern is that two legacy keys (e.g., "photo.CR2" and "photo.jpg") could both exist with different metadata, and only the first is promoted. In practice,
both "photo.CR2" and "photo.jpg" represent the same image pair — the app would never write distinct metadata to both. This is a legacy migration path for v1→v2
sidecar format where old keys included extensions. The first-match-wins behavior is correct: it migrates one alias and removes it. If a second orphaned alias
remains, it gets migrated on its own lookup later. This is acceptable because the metadata should be identical.

  1. Bare-stem fallback for nested paths (lines 204-225) — NOT a real bug.

The concern is that path.stem ("photo") is returned as a candidate for Path("a/photo.jpg"), potentially matching a top-level "photo" entry. However,
_scan_directory uses os.scandir(directory) which is non-recursive — all images are in the top-level directory. No nested paths like Path("a/photo.jpg") are
produced by the indexer. The bare-stem fallback is safe because all paths are flat.

  1. Empty metadata_map truthiness (line 552) — NOT a real bug.

if metadata_map and self._metadata_key_fn: treats {} as falsy and falls back to per-image _get_metadata. But if the map is empty, no images have metadata — both
code paths produce identical results (no flags match). The fallback is slightly slower but only runs when there's nothing to find anyway.

  1. Direct meta attribute access (app.py ~1810) — NOT a bug.

EntryMetadata is a @dataclasses.dataclass with explicit defaults (uploaded: bool = False, favorite: bool = False, todo: bool = False, etc.). The
_entrymetadata_from_json constructor filters to known fields and lets the dataclass fill in defaults for missing ones. Every EntryMetadata instance always has
all attributes. getattr with a default is unnecessary. (Line 2243 already uses getattr(meta, "todo", False) but that's redundant, not a pattern to spread.)

  1. normalize_path_key in model.py (nitpick) — Already fixed in the previous turn.

  2. logging_setup.py Path.cwd() fallback — NOT worth fixing.

Path.cwd() / "var" / "appdata" is fallback candidate #4 of 5 (after APPDATA, LOCALAPPDATA, and ~/.faststack). On any normal system, an earlier candidate
succeeds. The CWD fallback is a last resort before tempdir. Anchoring to the module's file would be worse — package install locations are often read-only.
The current fallback order is sensible.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 24, 2026

Tip

For best results, initiate chat on the files or code changes.

@AlanRockefeller, thank you for the thorough explanations — all six points are well-reasoned. I'll acknowledge each and record the key architectural facts for future reviews.

  1. Sidecar alias merging — Understood. First-match-wins is the correct migration strategy since the app never writes distinct metadata to both photo.CR2 and photo.jpg; a second orphaned alias will be promoted on its own later lookup.
  2. Bare-stem fallback for nested paths — Understood. _scan_directory uses os.scandir (non-recursive), so all indexed paths are flat and no Path("a/photo.jpg")-style nested paths are ever produced by the indexer.
  3. Empty metadata_map truthiness — Understood. An empty map means no images have metadata, so both the fast path and the per-image fallback produce identical results.
  4. Direct meta attribute access — Understood. EntryMetadata is a @dataclass with explicit defaults for all fields, and _entrymetadata_from_json filters to known fields with the dataclass filling in defaults; every instance is always fully initialized.
  5. normalize_path_key in model.py — Noted as already fixed.
  6. logging_setup.py Path.cwd() fallback — Understood. It's candidate #4 of 5 and only reached on unusual systems; anchoring to __file__ would be worse for read-only install locations.

[resolve]


✏️ Learnings added
Learnt from: AlanRockefeller
URL: https://github.com/AlanRockefeller/faststack/pull/57

Timestamp: 2026-03-24T23:20:04.052Z
Learning: In faststack/io/sidecar.py, the sidecar alias merging (legacy v1→v2 migration) uses first-match-wins when promoting a candidate key (e.g., "photo.CR2" or "photo.jpg") to the stable key. This is intentional: the app never writes distinct metadata to both a RAW and JPG key for the same image pair, so metadata is always identical. A second orphaned alias will be promoted on its own subsequent lookup. Do not flag this as a bug.

Learnt from: AlanRockefeller
URL: https://github.com/AlanRockefeller/faststack/pull/57

Timestamp: 2026-03-24T23:20:04.052Z
Learning: In faststack, `_scan_directory` uses `os.scandir(directory)` which is non-recursive (not `os.walk`). All indexed image paths are always flat (top-level directory only). No nested paths like Path("a/photo.jpg") are ever produced by the indexer. The bare-stem fallback in `_lookup_keys` for Path inputs is therefore safe — there are no same-stem collisions across subdirectories in practice.

Learnt from: AlanRockefeller
URL: https://github.com/AlanRockefeller/faststack/pull/57

Timestamp: 2026-03-24T23:20:04.052Z
Learning: In faststack, `EntryMetadata` (in faststack/io/sidecar.py) is a `dataclasses.dataclass` with explicit boolean defaults for all fields (uploaded, favorite, todo, stacked, etc.). The `_entrymetadata_from_json` constructor filters to known fields and lets the dataclass supply defaults for missing ones. Every `EntryMetadata` instance always has all attributes populated. Using `getattr(meta, field, default)` defensively is redundant and not a pattern to adopt or enforce in code review.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: Dj-Shortcut
Repo: AlanRockefeller/faststack PR: 54
File: faststack/io/sidecar.py:204-205
Timestamp: 2026-03-17T19:13:22.868Z
Learning: In faststack/io/sidecar.py, SidecarManager.get_metadata must guard against empty stable_key (produced by _lookup_keys("") returning ("", [])) before any entry creation or lookup. The agreed contract is: return None when create=False, raise ValueError when create=True. Additionally, Path("") must be guarded in the Path branch of _lookup_keys by checking `not image_ref.name`.

Learnt from: Dj-Shortcut
Repo: AlanRockefeller/faststack PR: 54
File: faststack/app.py:2161-2165
Timestamp: 2026-03-17T19:12:27.694Z
Learning: In faststack.app.AppController._get_bulk_metadata_map, the dictionary must be keyed by SidecarManager.metadata_key_for_path(path) (the sidecar canonical key), not by normalize_path_key(img.path), to avoid metadata lookup misses across consumers like the thumbnail model and folder stats.

@AlanRockefeller AlanRockefeller merged commit 148b9f7 into main Mar 24, 2026
3 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Apr 17, 2026
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.

2 participants