Files
bare-operating-system/scripts/report-holepunch-lockfile-drift.mjs
T
Raven Scott 7171618c74
Release rolling / release (push) Successful in 9m59s
Update Docs
2026-08-12 21:10:14 -04:00

232 lines
8.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Compare npm lockfile versions for P2P stack packages against optional local
* Holepunch clones (same root as sync-holepunch-clones.mjs).
*
* Writes:
* docs/audit/holepunch-lockfile-drift.json
* docs/audit/holepunch-lockfile-drift-summary.ndjson (one row per package)
*
* Env:
* BARE_OS_HOLEPUNCH_CLONES_ROOT — default ~/dev/pearcli/holepunch-repos/holepunchto_repos
* BARE_OS_HOLEPUNCH_LOCKFILE_DRIFT_TIER1_FAIL=1 — fail when a tier1 lockfile package
* has semverMismatchWithClone=true (uses docs/audit/holepunch-drift-repos.json tier1Repos)
*
* Config: docs/audit/holepunch-drift-repos.json
* lockfilePackages: [{ "cloneName": "protomux", "lockKey": "protomux" }, ...]
* If omitted, defaults are derived from suggestedCriticalRepos + common wire deps.
*/
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const lockPath = path.join(root, 'package-lock.json')
const driftConfigPath = path.join(root, 'docs/audit/holepunch-drift-repos.json')
const outJson = path.join(root, 'docs/audit/holepunch-lockfile-drift.json')
const outNd = path.join(root, 'docs/audit/holepunch-lockfile-drift-summary.ndjson')
const defaultClones = path.join(
process.env.HOME || '',
'dev/pearcli/holepunch-repos/holepunchto_repos'
)
const clonesRoot = String(
process.env.BARE_OS_HOLEPUNCH_CLONES_ROOT || defaultClones
).trim()
/** @type {{ cloneName: string, lockKey: string }[]} */
const DEFAULT_LOCK_PACKAGES = [
{ cloneName: 'protomux', lockKey: 'protomux' },
{ cloneName: 'hyperdrive', lockKey: 'hyperdrive' },
{ cloneName: 'hypercore', lockKey: 'hypercore' },
{ cloneName: 'hyperswarm', lockKey: 'hyperswarm' },
{ cloneName: 'udx-native', lockKey: 'udx-native' },
{ cloneName: 'corestore', lockKey: 'corestore' },
{ cloneName: 'compact-encoding', lockKey: 'compact-encoding' },
{ cloneName: 'secret-stream', lockKey: '@hyperswarm/secret-stream' }
]
/**
* @param {Record<string, unknown>} lock
* @param {string} key npm package name (e.g. protomux or @hyperswarm/secret-stream)
*/
function lockedVersion(lock, key) {
const k = 'node_modules/' + key
const p = lock.packages && lock.packages[k]
if (!p || typeof p !== 'object') return ''
return String(/** @type {{ version?: string }} */ (p).version || '').trim()
}
function loadLockfilePackages() {
if (!fs.existsSync(driftConfigPath)) return DEFAULT_LOCK_PACKAGES
try {
const j = JSON.parse(fs.readFileSync(driftConfigPath, 'utf8'))
if (Array.isArray(j.lockfilePackages) && j.lockfilePackages.length) {
return j.lockfilePackages
.map((row) => {
if (!row || typeof row !== 'object') return null
const r = /** @type {{ cloneName?: string, lockKey?: string }} */ (row)
const cn = String(r.cloneName || '').trim()
const lk = String(r.lockKey || r.cloneName || '').trim()
if (!cn || !lk) return null
return { cloneName: cn, lockKey: lk }
})
.filter(Boolean)
}
if (Array.isArray(j.suggestedCriticalRepos)) {
return j.suggestedCriticalRepos
.map((n) => {
const name = String(n || '').trim()
if (!name) return null
return { cloneName: name, lockKey: name }
})
.filter(Boolean)
}
} catch {
/* fall through */
}
return DEFAULT_LOCK_PACKAGES
}
/**
* @returns {Set<string>}
*/
function loadTier1RepoSet() {
if (!fs.existsSync(driftConfigPath)) return new Set()
try {
const j = JSON.parse(fs.readFileSync(driftConfigPath, 'utf8'))
if (!Array.isArray(j.tier1Repos)) return new Set()
return new Set(
j.tier1Repos
.map((v) => String(v || '').trim())
.filter(Boolean)
)
} catch {
return new Set()
}
}
function main() {
if (!fs.existsSync(lockPath)) {
console.error('report-holepunch-lockfile-drift: missing', lockPath)
process.exit(1)
}
const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'))
const pkgs = loadLockfilePackages()
const tier1Repos = loadTier1RepoSet()
const strictTier1Fail =
String(process.env.BARE_OS_HOLEPUNCH_LOCKFILE_DRIFT_TIER1_FAIL || '')
.trim()
.toLowerCase() === '1' ||
String(process.env.BARE_OS_HOLEPUNCH_LOCKFILE_DRIFT_TIER1_FAIL || '')
.trim()
.toLowerCase() === 'true'
const atMs = Date.now()
const rootExists = fs.existsSync(clonesRoot)
/** @type {Record<string, unknown>[]} */
const rows = []
for (const { cloneName, lockKey } of pkgs) {
const lv = lockedVersion(lock, lockKey)
const cloneDir = path.join(clonesRoot, cloneName)
const pj = path.join(cloneDir, 'package.json')
let cloneVersion = ''
if (fs.existsSync(pj)) {
try {
const p = JSON.parse(fs.readFileSync(pj, 'utf8'))
cloneVersion = String(p.version || '').trim()
} catch {
cloneVersion = ''
}
}
const semverMismatch =
lv && cloneVersion && lv !== cloneVersion ? true : false
const tier1 = tier1Repos.has(cloneName)
rows.push({
cloneName,
lockKey,
lockfileVersion: lv || null,
clonePath: cloneDir,
cloneExists: fs.existsSync(cloneDir),
clonePackageJsonVersion: cloneVersion || null,
semverMismatchWithClone: semverMismatch,
tier1
})
}
const tier1MismatchRows = rows.filter(
(r) => r.tier1 && r.semverMismatchWithClone
)
const report = {
schema: 1,
atMs,
lockfilePath: path.relative(root, lockPath),
clonesRoot,
clonesRootExists: rootExists,
driftConfigPath: path.relative(root, driftConfigPath),
note:
'lockfileVersion is the hoisted workspace entry in package-lock.json (lockfile v3). clonePackageJsonVersion is the local clones package.json when present; mismatch is informational until you bump npm deps or refresh clones.',
strictTier1Fail,
tier1MismatchCount: tier1MismatchRows.length,
rows
}
fs.mkdirSync(path.dirname(outJson), { recursive: true })
fs.writeFileSync(outJson, JSON.stringify(report, null, 2) + '\n')
const ndLines = rows.map((r) =>
JSON.stringify({
type: 'bare_os_holepunch_lockfile_drift',
schema: 1,
atMs,
clonesRoot,
...r
})
)
fs.writeFileSync(outNd, ndLines.join('\n') + (ndLines.length ? '\n' : ''))
const dashPath = path.join(root, 'docs/audit/holepunch-lockfile-drift-dashboard.md')
const dashLines = [
'# Holepunch lockfile drift dashboard',
'',
`_Generated at \`${new Date(atMs).toISOString()}\`. Regenerate with \`node scripts/report-holepunch-lockfile-drift.mjs\` (see [scripts/README.md](../../scripts/README.md))._`,
'',
'| Clone (package) | Tier1 | Lockfile version | Clone `package.json` version | Semver mismatch vs clone |',
'| --- | --- | --- | --- | --- |'
]
for (const r of rows) {
dashLines.push(
'| ' +
[
r.cloneName,
r.tier1 ? 'yes' : 'no',
r.lockfileVersion ?? '—',
r.clonePackageJsonVersion ?? '—',
r.semverMismatchWithClone ? 'yes' : 'no'
].join(' | ') +
' |'
)
}
dashLines.push(
'',
'**Note:** `lockfileVersion` is the hoisted workspace entry in `package-lock.json` (lockfile v3). `clonePackageJsonVersion` is the local mirrors `package.json` when present; mismatches are informational unless `BARE_OS_HOLEPUNCH_LOCKFILE_DRIFT_TIER1_FAIL=1`. Workspace packages (`bare-os-booter`, `bare-os-protocol`, `bare-os-seeder`) may nest a newer major (for example **`[email protected]`**) while the root hoist stays on **2.x** for dependents that have not moved.',
'',
`**Tier1 strict mode:** ${strictTier1Fail ? 'enabled' : 'disabled'} · tier1 mismatches: ${tier1MismatchRows.length}.`,
'',
'**Upgrade workflow hint:** expected churn often appears first in `udx-native` and `blind-peering`; when they drift, refresh local clones, review lockfile bumps, then regenerate this dashboard and `holepunch-runtime-compat.json` together before release gating.'
)
fs.writeFileSync(dashPath, dashLines.join('\n') + '\n')
console.log(
'report-holepunch-lockfile-drift: wrote',
path.relative(root, outJson),
'packages=',
rows.length
)
if (strictTier1Fail && tier1MismatchRows.length) {
console.error(
`report-holepunch-lockfile-drift: tier1 mismatch strict mode failed (${tier1MismatchRows.length} mismatches)`
)
process.exit(1)
}
}
main()