fix: require cached gallery rows to keep the enrolled order

A cache key proves the store, not the rows inside the file. The old check
asked only that every cached row appear somewhere in the enrolled list, so a
row moved across SKUs still passed: the matrix that travelled with it would
then be filed under the wrong product, and a valid key would rename a SKU.

Rows now walk the enrolled list forward and never backwards, so a moved,
repeated or foreign row has nowhere left to match. Views that could not be
decoded stay absent without disturbing the order of the ones after them,
which is what keeps a shorter cache a hit rather than a re-embed.
This commit is contained in:
kiyo9w 2026-09-28 17:22:46 +09:00
parent 13a4e0f475
commit 02e69acac7
2 changed files with 105 additions and 33 deletions

View file

@ -382,18 +382,26 @@ class ProductGallery:
return None
if not matrix.shape[0] or matrix.shape[0] != len(skus) or len(skus) != len(views):
return None
if not np.issubdtype(matrix.dtype, np.number) or not np.isfinite(matrix).all():
return None
# Undecodable views may be omitted; all cached rows must retain the
# exact enrolled SKU/view order or a valid key could name wrong SKUs.
rows = list(zip(skus, views))
remaining = iter((sku_id, os.path.basename(view)) for sku_id, view in enrolled)
if not all(any(pair == cached for pair in remaining) for cached in rows):
if not np.issubdtype(matrix.dtype, np.floating) or not np.isfinite(matrix).all():
return None
# Rows walk the enrolled list forward and never backwards, so a
# moved, repeated or foreign row has nowhere left to match. Views
# that could not be decoded are simply absent: everyone after one
# keeps its order, which is what stops a valid key from renaming a
# SKU.
expected = [(sku_id, os.path.basename(view)) for sku_id, view in enrolled]
rows: List[Tuple[str, str]] = []
position = 0
for sku_id, view in zip(skus, views):
while position < len(expected) and expected[position] != (sku_id, view):
position += 1
if position == len(expected):
return None
rows.append((sku_id, os.path.join(self.stock_dir, view)))
position += 1
if not self._weights_pinned():
return None
paths = [(sku, os.path.join(self.stock_dir, view)) for sku, view in rows]
return paths, matrix.astype(np.float32, copy=False)
return rows, matrix.astype(np.float32, copy=False)
except (OSError, ValueError, TypeError, KeyError, IndexError):
return None # absent, malformed, or raced with a checkpoint removal