#!/usr/bin/env tsx // The one-core Phase 3 slice 4a measurement: what `getSettings()` ANSWERS, for // every settings.json this repo can point at, printed deterministically so two // runs can be diffed. // // WHY THIS IS A SCRIPT AND NOT A TEST, same as phase1-numbers.ts next door. // Slice 4a replaces ten hand-written sanitizers with one zod schema. The claim // it makes is "nothing an operator has configured reads differently", and the // only way to check that is to parse the REAL files — the live corpus's // settings.json, the shipped example, the e2e fixture — before and after, and // diff two files. A test would have to carry the operator's configuration. // // NEVER WRITES A MEASURED FILE. Each target is copied to a scratch directory // under os.tmpdir(); the read and the write-back both happen on the copy, which // is deleted afterwards. (It measures the WRITE side too since slice 4a's // review: what `writeSettings(getSettings())` puts on disk.) // // NEVER BOOT AN EDITOR FOR THIS. `getSettings` is called in-process, offline; // instrumentation.ts is not loaded, so no runner, sweep or scheduler is armed. // // ONE PROCESS PER FILE, because `getPaths()` memoises its answer at module // scope: `SETTINGS_FILE` has to be set before `lib/settings.ts` is imported, so // a second file needs a second process. The parent below spawns itself once per // target; the child prints one block. // // Usage, from the repo root: // node_modules/.bin/tsx plans/tools/phase3-settings-numbers.ts // node_modules/.bin/tsx plans/tools/phase3-settings-numbers.ts live=/abs/settings.json // // Each argv entry is `label=path`. Given any, they REPLACE the default list; // the label is what the output names, so a file copied elsewhere (an archived // "before" example, say) can still be diffed against its original line for line. import { spawnSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const REPO = path.resolve(HERE, "..", ".."); type Target = { label: string; file: string }; // THE LIVE CORPUS'S settings.json, not this checkout's. A worktree carries its // own copy, and the file that matters is the one the editor actually runs on. // Overridable so the script is not pinned to one machine's layout. function liveSettingsFile(): string { return ( process.env.LIVE_SETTINGS_FILE ?? path.join( path.dirname(REPO), "yt-dlp-transcript-browser", "settings.json", ) ); } function defaultTargets(): Target[] { const out: Target[] = [ { label: "live", file: liveSettingsFile() }, { label: "example", file: path.join(REPO, "settings.json.example") }, ]; const fixtures = path.join(REPO, "editor", "e2e", "fixtures"); for (const name of fs.readdirSync(fixtures).sort()) { if (!/settings.*\.json$/i.test(name)) continue; out.push({ label: `fixture:${name}`, file: path.join(fixtures, name) }); } return out; } // Sort every object's keys so the output is diffable regardless of the order a // sanitizer (or a schema) happens to build its result in. Arrays keep their // order — in settings.json order IS data (workers are priority-ordered, and an // auto-queue tree's children compete in the order they are listed). function sortedKeys(_key: string, value: unknown): unknown { if (!value || typeof value !== "object" || Array.isArray(value)) return value; const src = value as Record; const out: Record = {}; for (const k of Object.keys(src).sort()) out[k] = src[k]; return out; } // READ, THEN WRITE — BOTH AGAINST A SCRATCH COPY. The target is copied into a // fresh directory under os.tmpdir() and SETTINGS_FILE points at the copy, so // `writeSettings` — which writes `getPaths().settingsFile` — can never touch // the file being measured (the live settings.json included). The read is of // identical bytes; the write side is what a save of that reading puts on disk. async function child(file: string): Promise { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "phase3-settings-")); const scratch = path.join(dir, "settings.json"); fs.copyFileSync(file, scratch); process.env.SETTINGS_FILE = scratch; process.env.TRANSCRIPTS_DIR = dir; try { const { getSettings, writeSettings } = await import( "../../common/lib/settings" ); const read = getSettings(); console.log(JSON.stringify(read, sortedKeys, 2)); console.log("### written by writeSettings(getSettings())"); try { await writeSettings(read); const written = JSON.parse(fs.readFileSync(scratch, "utf8")); console.log(JSON.stringify(written, sortedKeys, 2)); } catch (e) { console.log(`WRITE THREW: ${(e as Error).message}`); } } finally { fs.rmSync(dir, { recursive: true, force: true }); } } function parent(targets: Target[]): void { console.log("# one-core phase 3 slice 4a — getSettings() and writeSettings() over every settings file"); console.log(""); for (const { label, file } of targets) { console.log(`## ${label}`); if (!fs.existsSync(file)) { console.log("MISSING"); console.log(""); continue; } const res = spawnSync( process.execPath, [ path.join(REPO, "node_modules", "tsx", "dist", "cli.mjs"), fileURLToPath(import.meta.url), ], { cwd: REPO, encoding: "utf8", env: { ...process.env, PHASE3_SETTINGS_TARGET: file, // The snooze sanitizer compares against the clock, and a storage // probe would shell out. Neither is settings data; neither is read // here. (`sanitizeSnooze` self-clears a lapsed snooze, which is // stable as long as nothing is snoozed — noted, not worked around.) TZ: "UTC", }, }, ); if (res.status !== 0) { console.log(`FAILED status=${res.status}`); console.log(res.stderr.trim()); } else { console.log(res.stdout.trimEnd()); } console.log(""); } } const target = process.env.PHASE3_SETTINGS_TARGET; if (target) { await child(target); } else { const args = process.argv.slice(2); const targets: Target[] = args.length ? args.map((a) => { const eq = a.indexOf("="); if (eq < 0) return { label: path.basename(a), file: path.resolve(a) }; return { label: a.slice(0, eq), file: path.resolve(a.slice(eq + 1)) }; }) : defaultTargets(); parent(targets); }