( providerName: string, sources: SessionSource[], seenKeys: Set<string>, diskCache: SessionCache, dateRange?: DateRange, )
| 1958 | } |
| 1959 | |
| 1960 | async function parseProviderSources( |
| 1961 | providerName: string, |
| 1962 | sources: SessionSource[], |
| 1963 | seenKeys: Set<string>, |
| 1964 | diskCache: SessionCache, |
| 1965 | dateRange?: DateRange, |
| 1966 | ): Promise<ProjectSummary[]> { |
| 1967 | const provider = await getProvider(providerName) |
| 1968 | if (!provider) return [] |
| 1969 | |
| 1970 | const section = getOrCreateProviderSection(diskCache, providerName) |
| 1971 | const allDiscoveredFiles = new Set<string>() |
| 1972 | |
| 1973 | type SourceInfo = { source: SessionSource; fp: NonNullable<Awaited<ReturnType<typeof fingerprintFile>>> } |
| 1974 | const unchangedSources: Array<{ source: SessionSource; cached: CachedFile }> = [] |
| 1975 | const changedSources: SourceInfo[] = [] |
| 1976 | |
| 1977 | for (const source of sources) { |
| 1978 | allDiscoveredFiles.add(source.path) |
| 1979 | |
| 1980 | // Network providers (e.g. Vercel AI Gateway) have no on-disk file — their data |
| 1981 | // comes from a live API fetch in createSessionParser. There's nothing to |
| 1982 | // fingerprint or incrementally cache, so re-fetch every run with a synthetic |
| 1983 | // fingerprint (mtime=now so the date-range filter below never excludes it). |
| 1984 | if (provider.network) { |
| 1985 | changedSources.push({ source, fp: { dev: 0, ino: 0, mtimeMs: Date.now(), sizeBytes: 0 } }) |
| 1986 | continue |
| 1987 | } |
| 1988 | |
| 1989 | const fp = await fingerprintFile(source.path) |
| 1990 | if (!fp) continue |
| 1991 | |
| 1992 | const cached = section.files[source.path] |
| 1993 | const action = reconcileFile(fp, cached) |
| 1994 | // A cached parse failure at this same fingerprint stays skipped — don't |
| 1995 | // re-read a file that already threw and hasn't changed. It re-parses only |
| 1996 | // when the file changes (then `reconcileFile` reports non-'unchanged'). |
| 1997 | if (action.action === 'unchanged' && cached && (cached.failed || !cachedFileNeedsProviderReparse(providerName, source.path, cached))) { |
| 1998 | unchangedSources.push({ source, cached }) |
| 1999 | } else { |
| 2000 | changedSources.push({ source, fp }) |
| 2001 | } |
| 2002 | } |
| 2003 | |
| 2004 | // Parser dedup: cross-provider keys + cached file keys. |
| 2005 | // Separate from seenKeys so parsing doesn't suppress query-time output. |
| 2006 | const parserDedup = new Set(seenKeys) |
| 2007 | for (const { cached } of unchangedSources) { |
| 2008 | for (const turn of cached.turns) { |
| 2009 | for (const call of turn.calls) { |
| 2010 | parserDedup.add(call.deduplicationKey) |
| 2011 | } |
| 2012 | } |
| 2013 | } |
| 2014 | |
| 2015 | // Parse changed files, update cache |
| 2016 | let didParse = false |
| 2017 | // Track which paths have already been cleared this pass so that subsequent |
no test coverage detected