<!-- SOLIDABILITY-WORLD · part 46 -->
} catch (e) { investigationMemory = {}; }

  function saveMemory() {
    try {
      localStorage.setItem('sw-investigation-memory', JSON.stringify(investigationMemory));
    } catch (e) {}
  }

  // Stable finding identity: file + pattern + line
  function findingId(case_) {
    return `${case_.file}:${case_.pattern?.pattern_id || case_.type}:${case_.pattern?.line || 0}`;
  }

  // Check if already investigated (and unchanged)
  function isResolved(case_) {
    const id = findingId(case_);
    const mem = investigationMemory[id];
    if (!mem) return false;
    // Stale after 7 days
    const age = Date.now() - new Date(mem.timestamp).getTime();
    if (age > 7 * 24 * 60 * 60 * 1000) return false;
    return mem.outcome === 'resolved' || mem.outcome === 'intentional';
  }

  // Smart prioritization (not complexity alone)
  function prioritize(candidates) {
    return candidates.map(c => {
      let score = 0;
      // Potential consequence (security > correctness > style)
      if (c.pattern?.pattern_id === 'equality-loose') score += 30; // type coercion risk
      if (c.pattern?.title?.includes('XSS') || c.file?.includes('views')) score += 20;
      if (c.pattern?.category === 'risk') score += 15;
      // Affected components (more callers = higher impact)
      const room = SWWorld.getLayout()?.rooms.find(r => r.key === c.roomKey);
      const callers = room?.functions?.reduce((n, f) => n + (f.called_by?.length || 0), 0) || 0;
      score += Math.min(callers, 20);
      // Testability (can we safely test it?)
      if (c.pattern?.pattern_id) score += 10; // pattern-based = testable in fixture
      // Prior investigation (deprioritize resolved)
      if (isResolved(c)) score -= 100;
      return { ...c, score };
    }).sort((a, b) => b.score - a.score);
  }

  async function startInvestigation(mode = 'risks', limit = 5) {
    if (investigating) return { ok: false, error: 'Already investigating' };

    const layout = SWWorld.getLayout();
    if (!layout) return { ok: false, error: 'No world loaded' };

    let candidates = buildCandidates(layout, mode);
    candidates = prioritize(candidates);
    investigationQueue = candidates.slice(0, limit);

    // Filter already-resolved
    const skipped = investigationQueue.filter(c => isResolved(c)).length;
    investigationQueue = investigationQueue.filter(c => !isResolved(c));

    if (!investigationQueue.length) {
      return { ok: false, error: skipped ? `All ${skipped} candidates already investigated` : 'No candidates found' };
    }

    investigating = true;
    paused = false;

    SWHyper.narrate(
      `🔍 <b>AI Investigation started.</b> ${investigationQueue.length} candidates ` +
      `${skipped ? `(${skipped} already resolved, skipped). ` : ''}` +
      `I'll investigate each fully, then move on. Tap 🕵️ to pause, ⏹️ to stop.`
    );

    await investigateNext();
    return { ok: true, queued: investigationQueue.length, skipped };
  }

  function buildCandidates(layout, mode) {
    const candidates = [];
    if (mode === 'risks') {
      layout.rooms.forEach(room => {
        (room.outdated_patterns || [])
          .filter(p => p.category === 'risk')
          .forEach(pattern => {
            candidates.push({
              roomKey: room.key, room: room.label, file: room.filePath,
              type: 'pattern-risk', pattern,
            });
          });
      });
    } else if (mode === 'complexity') {
      layout.rooms
        .filter(r => r.complexity > 500 && r.has_test === false)
        .slice(0, 10)
        .forEach(room => {
          candidates.push({
            roomKey: room.key, room: room.label, file: room.filePath,
            type: 'high-complexity-untested',
          });
        });
    }
    return candidates;
  }

  // V2: Task completion, not fixed timer
  // Tracks: navigation arrived → analysis complete → findings presented
  let investigationPhase = 'idle'; // idle → navigating → analyzing → presenting → done

  async function investigateNext() {
    if (!investigationQueue.length) {
      investigating = false;
      investigationPhase = 'idle';
      SWHyper.narrate(`Investigation complete. All candidates reviewed.`);
      if (typeof SWTrust !== 'undefined') {
        SWTrust.serverAudit('investigation-complete', 'AI investigation', 'queue exhausted');
      }
      updateButton();
      return;
    }

    if (paused) return; // wait for resume

    currentCase = investigationQueue.shift();
    investigationPhase = 'navigating';

    const layout = SWWorld.getLayout();
    const room = layout.rooms.find(r => r.key === currentCase.roomKey);
    if (!room) { await investigateNext(); return; }

    SWHyper.narrate(
      `Navigating to <b>${currentCase.room}</b> — ${currentCase.pattern?.title || currentCase.type}.`
    );

    // Navigate, then analyze on arrival (not timer)
    SWHyper.aiDriveTo(room, `Investigating ${currentCase.pattern?.title || 'candidate'}.`);

    // Poll for