From 755fa7f59461e9b02db10f89448e833c19977236 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 10 Mar 2026 16:45:40 -0600 Subject: [PATCH] feat(comparecache): add -windowed flag for version-scoped comparison Uses Node.js version range (2nd to 2nd-to-last) as the window. All Node.js versions in the window are included so missing Go versions/assets are visible. Go-only versions are hidden since those are just deeper fetch history, not real gaps. --- cmd/comparecache/main.go | 130 +++++++++++++++++++++++++++++++-------- 1 file changed, 105 insertions(+), 25 deletions(-) diff --git a/cmd/comparecache/main.go b/cmd/comparecache/main.go index f544618..24491a5 100644 --- a/cmd/comparecache/main.go +++ b/cmd/comparecache/main.go @@ -58,6 +58,7 @@ func main() { goDir := flag.String("go", "./_cache", "path to Go cache directory") summary := flag.Bool("summary", false, "only print summary, not per-package details") latest := flag.Bool("latest", false, "only compare latest version in each cache") + windowed := flag.Bool("windowed", false, "limit Go versions to the Node.js version range (2nd to 2nd-to-last)") flag.Parse() filterPkgs := flag.Args() @@ -92,7 +93,7 @@ func main() { var diffs []packageDiff for _, pkg := range allPkgs { - d := compare(livePath, goPath, pkg, *latest) + d := compare(livePath, goPath, pkg, *latest, *windowed) categorize(&d) diffs = append(diffs, d) } @@ -185,7 +186,42 @@ func effectiveName(name, filename, download string) string { return "" } -func compare(livePath, goPath, pkg string, latestOnly bool) packageDiff { +// versionWindow returns the 2nd and 2nd-to-last versions from a sorted +// version list. This trims the edges where Node.js may have a newer fetch +// or Go may have deeper history, focusing on the overlapping middle. +func versionWindow(versions []string) (low, high string) { + if len(versions) <= 2 { + // Too few versions to window — use all. + if len(versions) > 0 { + return versions[0], versions[len(versions)-1] + } + return "", "" + } + // 2nd version (skip oldest) and 2nd-to-last (skip newest). + return versions[1], versions[len(versions)-2] +} + +// filterVersionRange returns only the versions in sorted order that fall +// within [low, high] inclusive (by lexver comparison). +func filterVersionRange(vf map[string]map[string]bool, versions []string, low, high string) (map[string]bool, []string) { + lowV := lexver.Parse(low) + highV := lexver.Parse(high) + + files := make(map[string]bool) + var kept []string + for _, v := range versions { + pv := lexver.Parse(v) + if lexver.Compare(pv, lowV) >= 0 && lexver.Compare(pv, highV) <= 0 { + kept = append(kept, v) + for f := range vf[v] { + files[f] = true + } + } + } + return files, kept +} + +func compare(livePath, goPath, pkg string, latestOnly, windowed bool) packageDiff { live := loadCache(livePath, pkg) goCache := loadCache(goPath, pkg) @@ -202,11 +238,6 @@ func compare(livePath, goPath, pkg string, latestOnly bool) packageDiff { } // Collect filenames by version. - type versionFiles struct { - version string - files map[string]bool - } - extractVersionFiles := func(ce *cacheEntry) (map[string]map[string]bool, []string) { vf := make(map[string]map[string]bool) for _, r := range ce.Releases { @@ -227,37 +258,86 @@ func compare(livePath, goPath, pkg string, latestOnly bool) packageDiff { var liveFiles, goFiles map[string]bool + // Parse live cache. + var liveVF map[string]map[string]bool + var liveVersions []string if live != nil { - vf, versions := extractVersionFiles(live) - d.VersionsLive = versions + liveVF, liveVersions = extractVersionFiles(live) + d.VersionsLive = liveVersions d.LiveCount = len(live.Releases) + } - if latestOnly && len(versions) > 0 { - liveFiles = vf[versions[len(versions)-1]] - } else { + // Parse Go cache. + var goVF map[string]map[string]bool + var goVersions []string + if goCache != nil { + goVF, goVersions = extractVersionFiles(goCache) + d.VersionsGo = goVersions + d.GoCount = len(goCache.Releases) + } + + // Determine which files to compare based on mode. + if latestOnly { + // Compare only the latest version from each cache. + if live != nil && len(liveVersions) > 0 { + liveFiles = liveVF[liveVersions[len(liveVersions)-1]] + } + if goCache != nil && len(goVersions) > 0 { + goFiles = goVF[goVersions[len(goVersions)-1]] + } + } else if windowed && live != nil && len(liveVersions) > 0 { + // Use the Node.js version range (2nd to 2nd-to-last) to establish + // the window. Include ALL Node.js versions in the window (so missing + // Go versions are visible), but exclude Go-only versions (those are + // just deeper history, not real gaps). + low, high := versionWindow(liveVersions) + lowV := lexver.Parse(low) + highV := lexver.Parse(high) + + // Collect all live files in the window. + liveFiles = make(map[string]bool) + liveInWindow := make(map[string]bool) + for _, v := range liveVersions { + pv := lexver.Parse(v) + if lexver.Compare(pv, lowV) >= 0 && lexver.Compare(pv, highV) <= 0 { + liveInWindow[v] = true + for f := range liveVF[v] { + liveFiles[f] = true + } + } + } + + // For Go, only include versions that Node.js also has in the window. + // Go-only versions are hidden (deeper history, not gaps). + goFiles = make(map[string]bool) + for _, v := range goVersions { + if !liveInWindow[v] { + continue + } + for f := range goVF[v] { + goFiles[f] = true + } + } + } else { + // Compare all versions. + if live != nil { liveFiles = make(map[string]bool) for _, r := range live.Releases { liveFiles[effectiveName(r.Name, r.Filename, r.Download)] = true } } - } else { - liveFiles = make(map[string]bool) - } - - if goCache != nil { - vf, versions := extractVersionFiles(goCache) - d.VersionsGo = versions - d.GoCount = len(goCache.Releases) - - if latestOnly && len(versions) > 0 { - goFiles = vf[versions[len(versions)-1]] - } else { + if goCache != nil { goFiles = make(map[string]bool) for _, r := range goCache.Releases { goFiles[effectiveName(r.Name, r.Filename, r.Download)] = true } } - } else { + } + + if liveFiles == nil { + liveFiles = make(map[string]bool) + } + if goFiles == nil { goFiles = make(map[string]bool) }