Files
PaperClipAI/scripts/sleep-qualification/setup-otel.mjs
T
2026-11-03 17:41:15 -06:00

126 lines
7.7 KiB
JavaScript

import { lstat, mkdtemp, mkdir, readFile, readlink, rename, rm, symlink, unlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { randomUUID } from 'node:crypto';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { fileURLToPath, pathToFileURL } from 'node:url';
// Explicit opt-in test setup. No manifests, lockfiles or other worktrees change.
const server = fileURLToPath(new URL('../../server/', import.meta.url));
export const otelPeerNames = ['@opentelemetry/sdk-node', '@opentelemetry/auto-instrumentations-node', '@opentelemetry/exporter-trace-otlp-http', '@opentelemetry/resources', '@opentelemetry/semantic-conventions'];
const ledgerPath = root => path.join(root, 'node_modules', '.paperclip-sleep-otel-links.json');
async function entryAt(destination) {
try { return await lstat(destination); } catch (error) { if (error.code !== 'ENOENT') throw error; }
}
async function versionAt(root, name) {
try { return JSON.parse(await readFile(path.join(root, 'node_modules', name, 'package.json'), 'utf8')).version; }
catch (error) { if (error.code !== 'ENOENT') throw error; return undefined; }
}
async function readOwnership(root) {
const file = ledgerPath(root), entry = await entryAt(file);
if (!entry) return [];
if (!entry.isFile() || entry.isSymbolicLink()) throw new Error('OTel ownership ledger must be a regular file');
const record = JSON.parse(await readFile(file, 'utf8'));
if (record.version !== 1 || record.serverDirectory !== root || !Array.isArray(record.links)
|| record.links.length > otelPeerNames.length || new Set(record.links.map(link => link.name)).size !== record.links.length
|| record.links.some(link => !otelPeerNames.includes(link.name) || typeof link.target !== 'string'
|| !path.isAbsolute(link.target) || path.resolve(link.target) !== link.target
|| !link.target.endsWith(path.join('node_modules', link.name)))) throw new Error('Invalid OTel ownership ledger');
return record.links;
}
async function writeOwnership(root, links) {
const file = ledgerPath(root);
if (!links.length) { await rm(file, { force: true }); return; }
await mkdir(path.dirname(file), { recursive: true });
const temporary = `${file}.${randomUUID()}.tmp`;
try {
await writeFile(temporary, JSON.stringify({ version: 1, serverDirectory: root, links }, null, 2), { flag: 'wx' });
await rename(temporary, file);
} finally { await rm(temporary, { force: true }); }
}
async function ownsLink(destination, record) {
const entry = await entryAt(destination);
return Boolean(record && entry?.isSymbolicLink()
&& path.resolve(path.dirname(destination), await readlink(destination)) === record.target);
}
async function installWithNpm(directory) {
await new Promise((resolve, reject) => {
const child = spawn('npm', ['install', '--prefix', directory, '--ignore-scripts', '--no-audit', '--no-fund', '--prefer-offline'], { stdio: 'inherit', shell: false });
child.once('error', reject); child.once('exit', code => code === 0 ? resolve() : reject(new Error(`OTel fixture installation failed (${code})`)));
});
}
/** Only the exact recorded symlink may be removed; install prefixes retain evidence. */
export async function cleanupSleepOtelLinks({ serverDirectory = server } = {}) {
const root = path.resolve(serverDirectory), records = await readOwnership(root), remaining = [], removed = [], errors = [];
for (const record of records) {
const destination = path.join(root, 'node_modules', record.name);
if (!await entryAt(destination)) continue;
if (!await ownsLink(destination, record)) {
remaining.push(record); errors.push(new Error(`Refusing to clean unrelated ${record.name}`)); continue;
}
await unlink(destination); removed.push(record.name);
}
await writeOwnership(root, remaining);
if (errors.length) throw new AggregateError(errors, 'Some OTel destinations no longer match owned links');
return { removed, installPrefixes: [...new Set(records.map(record => path.dirname(path.dirname(path.dirname(record.target)))))], boundary: 'Only recorded matching symlinks removed; temporary install prefixes are not deleted' };
}
export async function setupSleepOtelPeers({ serverDirectory = server, temporaryRoot = tmpdir(), install = installWithNpm } = {}) {
const root = path.resolve(serverDirectory), manifest = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
const owned = await readOwnership(root), missing = [];
for (const name of otelPeerNames) {
const exact = manifest.peerDependencies[name];
if (!/^\d+\.\d+\.\d+$/.test(exact ?? '')) throw new Error(`Exact existing peer version required for ${name}`);
const installed = await versionAt(root, name);
if (installed && installed !== exact) throw new Error(`Installed ${name} does not match ${exact}; refusing to replace it`);
if (installed) continue;
const destination = path.join(root, 'node_modules', name);
if (await entryAt(destination) && !await ownsLink(destination, owned.find(link => link.name === name))) {
throw new Error(`Refusing to replace unrelated ${name}; restore its target or verify ownership before removing the unrecorded entry`);
}
missing.push(name);
}
if (!missing.length) return { linked: [], alreadyInstalled: true };
const directory = await mkdtemp(path.join(temporaryRoot, 'paperclip-sleep-otel-'));
let linked = 0;
try {
await writeFile(path.join(directory, 'package.json'), JSON.stringify({ private: true, dependencies: Object.fromEntries(otelPeerNames.map(name => [name, manifest.peerDependencies[name]])) }, null, 2));
await install(directory);
const resolved = Object.fromEntries(await Promise.all(otelPeerNames.map(async name => [name, await versionAt(directory, name)])));
for (const name of otelPeerNames) if (resolved[name] !== manifest.peerDependencies[name]) throw new Error(`Resolved ${name} differs from pinned peer`);
const records = [...owned];
for (const name of missing) {
const destination = path.join(root, 'node_modules', name), previous = owned.find(record => record.name === name);
await mkdir(path.dirname(destination), { recursive: true });
if (await entryAt(destination)) {
if (!await ownsLink(destination, previous)) throw new Error(`OTel destination changed; refusing to replace ${name}`);
await unlink(destination);
}
const target = path.join(directory, 'node_modules', name);
await symlink(target, destination, 'dir'); linked++;
const updated = [...records.filter(record => record.name !== name), { name, target }];
try { await writeOwnership(root, updated); }
catch (error) {
if (await ownsLink(destination, { name, target })) { await unlink(destination); linked--; }
throw error;
}
records.splice(0, records.length, ...updated);
}
const receipt = { version: 1, directory, packages: resolved, linked: missing, ownershipLedger: ledgerPath(root), cleanup: 'node scripts/sleep-qualification/setup-otel.mjs --cleanup' };
await writeFile(path.join(directory, 'qualification-setup.json'), JSON.stringify(receipt, null, 2));
return receipt;
} catch (error) {
// A prefix with published links stays available for explicit owned cleanup.
if (!linked) await rm(directory, { recursive: true, force: true });
throw error;
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const args = process.argv.slice(2);
if (args.length > 1 || (args.length && args[0] !== '--cleanup')) throw new Error('Usage: node scripts/sleep-qualification/setup-otel.mjs [--cleanup]');
const receipt = args[0] === '--cleanup' ? await cleanupSleepOtelLinks() : await setupSleepOtelPeers();
process.stdout.write(JSON.stringify(receipt) + '\n');
}