[object Object]

← back to Wallco Ai

triage: colorway-sibling fallback in resolveImagePath

3fc81bf33e5d5fcdf259d255e86b61066f4b4a31 · 2026-05-24 07:58:48 -0700 · Steve Abrams

Many flagged designs have spoon_all_designs.local_path pointing at a base
<stem>.png that no longer exists on disk (~1,130 ERROR rows in the last
sweep) — but the per-colorway variants <stem>.cw-<slug>.png are still
there. These are colorway-only designs whose "base" path was a
derivation pattern, not a real file.

New resolver:
  - indexColorways(dir) — scans dir once, builds stem → [cw-files] map
  - findColorwaySibling(basePath, category) — picks the variant whose
    cw-slug matches the trailing colorway in the row's category
    ("designer-zoo-calm · slate-mist" → cw-slate-mist), or lexicographically
    first if no match. Geometry is identical across colorways so any
    variant works for ghost-layer / structural defect detection.

Wired in after BOTH pg-source AND json-source primary lookups in
resolveImagePath. Smoke-tested on 5 cases — exact, unsuffixed, unknown
slug, missing sibling, missing-everything — all correct.

Files touched

Diff

commit 3fc81bf33e5d5fcdf259d255e86b61066f4b4a31
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun May 24 07:58:48 2026 -0700

    triage: colorway-sibling fallback in resolveImagePath
    
    Many flagged designs have spoon_all_designs.local_path pointing at a base
    <stem>.png that no longer exists on disk (~1,130 ERROR rows in the last
    sweep) — but the per-colorway variants <stem>.cw-<slug>.png are still
    there. These are colorway-only designs whose "base" path was a
    derivation pattern, not a real file.
    
    New resolver:
      - indexColorways(dir) — scans dir once, builds stem → [cw-files] map
      - findColorwaySibling(basePath, category) — picks the variant whose
        cw-slug matches the trailing colorway in the row's category
        ("designer-zoo-calm · slate-mist" → cw-slate-mist), or lexicographically
        first if no match. Geometry is identical across colorways so any
        variant works for ghost-layer / structural defect detection.
    
    Wired in after BOTH pg-source AND json-source primary lookups in
    resolveImagePath. Smoke-tested on 5 cases — exact, unsuffixed, unknown
    slug, missing sibling, missing-everything — all correct.
---
 scripts/triage-ghost-flagged.js | 59 ++++++++++++++++++++++++++++++++++++++---
 1 file changed, 56 insertions(+), 3 deletions(-)

diff --git a/scripts/triage-ghost-flagged.js b/scripts/triage-ghost-flagged.js
index 8c79441..ba0543b 100755
--- a/scripts/triage-ghost-flagged.js
+++ b/scripts/triage-ghost-flagged.js
@@ -74,6 +74,53 @@ function designsJsonMap() {
   return DESIGNS_JSON_MAP;
 }
 
+// Colorway-sibling fallback. Many flagged designs in PG point to a base PNG
+// like `<stem>.png` that no longer exists on disk, but per-colorway variants
+// `<stem>.cw-<slug>.png` are still there (the design is colorway-only).
+// Strategy: scan the directory once, build a map of stem → list of sibling
+// colorway files. When the base path doesn't exist, return the colorway
+// variant matching the row's category-suffix slug ("designer-zoo-calm · slate-mist"
+// → cw-slate-mist), or the first variant if no suffix match.
+const COLORWAY_INDEX = new Map(); // dir → Map(stem → [filename, ...])
+function indexColorways(dir) {
+  if (COLORWAY_INDEX.has(dir)) return COLORWAY_INDEX.get(dir);
+  const idx = new Map();
+  try {
+    for (const f of fs.readdirSync(dir)) {
+      // Match <stem>.cw-<slug>.png — slug allowed to contain dashes
+      const m = f.match(/^(.+?)\.cw-([a-z0-9-]+)\.png$/i);
+      if (!m) continue;
+      const stem = m[1];
+      if (!idx.has(stem)) idx.set(stem, []);
+      idx.get(stem).push(f);
+    }
+  } catch { /* dir unreadable — leave empty */ }
+  COLORWAY_INDEX.set(dir, idx);
+  return idx;
+}
+
+// Given a base path like /.../1234_5678.png that doesn't exist, look for
+// /.../1234_5678.cw-<slug>.png siblings. Prefer the slug matching the
+// trailing colorway suffix in the row's category (after " · ").
+function findColorwaySibling(basePath, category) {
+  if (!basePath || !basePath.endsWith('.png')) return null;
+  const dir = path.dirname(basePath);
+  const stem = path.basename(basePath, '.png');
+  const siblings = indexColorways(dir).get(stem);
+  if (!siblings || siblings.length === 0) return null;
+
+  // Try to match the colorway slug from "category · slug" if present
+  if (category && category.includes(' · ')) {
+    const slug = category.split(' · ').pop().trim().toLowerCase();
+    const exact = siblings.find(f => f.toLowerCase().includes(`.cw-${slug}.`));
+    if (exact) return path.join(dir, exact);
+  }
+  // Otherwise return the lexicographically first variant — any colorway works
+  // for ghost-layer / structural defect detection since the motif geometry
+  // is identical across colorways.
+  return path.join(dir, siblings.slice().sort()[0]);
+}
+
 // PG fallback — look up local_path for ids that don't carry one in the flagged row.
 const PG_PATH_CACHE = new Map();
 function pgLocalPath(id) {
@@ -91,17 +138,23 @@ function pgLocalPath(id) {
 
 async function resolveImagePath(row) {
   if (row.local_path && fs.existsSync(row.local_path)) return row.local_path;
-  // pg source → consult PG for the local_path
+  // pg source → consult PG for the local_path; if file gone, try cw-* siblings
   if (row.source === 'pg') {
     const p = pgLocalPath(row.id);
-    if (p && fs.existsSync(p)) return p;
+    if (p) {
+      if (fs.existsSync(p)) return p;
+      const cw = findColorwaySibling(p, row.category);
+      if (cw) return cw;
+    }
   }
-  // json source → consult designs.json for the filename
+  // json source → consult designs.json for the filename; same cw-* fallback
   if (row.source === 'json') {
     const fn = designsJsonMap().get(row.id);
     if (fn) {
       const cand = path.join(GENERATED_DIR, fn);
       if (fs.existsSync(cand)) return cand;
+      const cw = findColorwaySibling(cand, row.category);
+      if (cw) return cw;
     }
   }
   // last resort — fetch via local API (works regardless of source if server is up)

← e110dad ghost-scan flagged.jsonl: gate on unanimous 3/3, not confide  ·  back to Wallco Ai  ·  seamless-tile physics gate: detector lib + sweeper + pre-pub afee5cd →