OverviewFilesCommitsIssuesDocsReleasesPeers

pgh / scripts / rebuild-refs.js

129 lines · javascript ref:
#!/usr/bin/env node
/**
 * rebuild-refs — reconcile refs.json against published-index.json.
 *
 * The legacy addRef path appended refs forever. After the retention-
 * policy work the index keeps only N most recent versions per app,
 * but refs.json still claims app X references every tree ever
 * published by X, including ones from versions long-trimmed from
 * the index. This script:
 *
 *   1. Reads the published-index (now capped to MAX_VERSIONS per app)
 *   2. Walks each retained lockfile + collects every dep.tree hash
 *      (plus per-arch prebuild hashes + the rootTreeHash)
 *   3. Rebuilds refs.json so each app references only the hashes
 *      its retained versions actually need
 *
 * Then `pgh store gc` correctly identifies historical-version trees
 * as unreferenced and frees them.
 *
 * Usage:
 *   node scripts/rebuild-refs.js [--dry-run]
 *
 * SAFETY:
 *   - Backs up refs.json before mutating.
 *   - Does NOT touch any identity files, the corestore, the registry,
 *     or any app install dir.
 *   - Idempotent.
 */

import fs from 'fs'
import path from 'path'
import { homedir } from 'os'

const STORE_ROOT = process.env.PGH_STORE_ROOT ||
  path.join(homedir(), '.local', 'share', 'store')
const INDEX_PATH = process.env.PGH_INDEX_PATH ||
  path.join(homedir(), '.config', 'pgh', 'published-index.json')
const REFS_PATH = path.join(STORE_ROOT, 'refs.json')

const dryRun = process.argv.includes('--dry-run')

if (!fs.existsSync(INDEX_PATH)) {
  console.error('no index at ' + INDEX_PATH + ' — nothing to rebuild from')
  process.exit(1)
}
if (!fs.existsSync(REFS_PATH)) {
  console.error('no refs.json at ' + REFS_PATH)
  process.exit(1)
}

const index = JSON.parse(fs.readFileSync(INDEX_PATH, 'utf8'))
const before = JSON.parse(fs.readFileSync(REFS_PATH, 'utf8'))

console.log('=== rebuild-refs ===')
console.log('  index: ' + INDEX_PATH)
console.log('  refs:  ' + REFS_PATH)
console.log('  dryRun: ' + dryRun)
console.log('')

// Build the canonical (app -> Set<hash>) from the index.
const want = {} // appName -> Set of tree hashes
for (const [appName, app] of Object.entries(index.apps || {})) {
  const set = new Set()
  for (const v of (app.versions || [])) {
    if (v.rootTreeHash) set.add(v.rootTreeHash)
    const lockfile = v.lockfile || {}
    for (const dep of (lockfile.deps || [])) {
      if (dep.tree) set.add(dep.tree)
      if (dep.prebuilds && typeof dep.prebuilds === 'object') {
        for (const archHash of Object.values(dep.prebuilds)) {
          if (archHash) set.add(archHash)
        }
      }
    }
  }
  want[appName] = set
}

// Build new refs.json: for every hash referenced by ANY kept version
// of some app, list that app under it. Hashes referenced by no app
// don't appear (they become gc-able).
const after = {}
for (const [appName, hashes] of Object.entries(want)) {
  for (const hash of hashes) {
    if (!after[hash]) after[hash] = []
    if (!after[hash].includes(appName)) after[hash].push(appName)
  }
}

// Stats
let appStats = []
for (const [appName, hashes] of Object.entries(want)) {
  const beforeCount = Object.values(before).filter(arr => arr.includes(appName)).length
  appStats.push({ app: appName, beforeRefs: beforeCount, afterRefs: hashes.size })
}
appStats.sort((a, b) => b.beforeRefs - a.beforeRefs)

console.log('  per-app ref counts:')
console.log('  ' + 'app'.padEnd(28) + 'before    after   delta')
for (const s of appStats) {
  const delta = s.beforeRefs - s.afterRefs
  console.log('  ' + s.app.padEnd(28) +
    String(s.beforeRefs).padStart(6) + '    ' +
    String(s.afterRefs).padStart(5) + '   ' +
    (delta > 0 ? '-' + delta : '0'))
}

const beforeTotal = Object.keys(before).length
const afterTotal = Object.keys(after).length
console.log('')
console.log('  total referenced hashes: ' + beforeTotal + ' → ' + afterTotal +
  ' (-' + (beforeTotal - afterTotal) + ')')

if (dryRun) {
  console.log('')
  console.log('  dry-run: no changes written.')
  console.log('  next: `pgh store gc --dry-run --max-age 0` to see how many bytes free up.')
  process.exit(0)
}

// Write — back up first.
const backup = REFS_PATH + '.backup-' + new Date().toISOString().replace(/[:.]/g, '-')
fs.copyFileSync(REFS_PATH, backup)
console.log('  backup: ' + backup)

fs.writeFileSync(REFS_PATH, JSON.stringify(after, null, 2))
console.log('  wrote new refs.json')
console.log('  next: `pgh store gc --max-age 0` to actually reclaim disk')