1// Compares two parity-tour logs and reports every field that differs, step2// by step. Usage:3//4// node compare.mjs baseline.json candidate.json [--ignore path,path,...]5//6// Ignore paths are dot-joined suffixes matched against the full path of a7// leaf value (for example "tileStats.requested" or "domNodes"). The exit8// code is 0 when the logs agree on everything not ignored, 1 otherwise.9import { readFileSync } from "node:fs";1011const [, , baselinePath, candidatePath, ...rest] = process.argv;12if (!baselinePath || !candidatePath) {13 console.error("usage: node compare.mjs baseline.json candidate.json [--ignore a,b]");14 process.exit(2);15}16const ignoreFlag = rest.indexOf("--ignore");17const ignored = ignoreFlag >= 0 ? rest[ignoreFlag + 1].split(",") : [];1819const baseline = JSON.parse(readFileSync(baselinePath, "utf8"));20const candidate = JSON.parse(readFileSync(candidatePath, "utf8"));2122const isIgnored = (path) => ignored.some(23 (suffix) => path === suffix || path.endsWith(`.${suffix}`),24);2526function* leaves(value, path = "") {27 if (value !== null && typeof value === "object") {28 const keys = Array.isArray(value)29 ? value.map((_, index) => index)30 : Object.keys(value).sort();31 if (keys.length === 0) {32 yield [path, Array.isArray(value) ? "[]" : "{}"];33 return;34 }35 for (const key of keys) {36 yield* leaves(value[key], path ? `${path}.${key}` : String(key));37 }38 return;39 }40 yield [path, JSON.stringify(value)];41}4243function diffStep(name, left, right) {44 const a = new Map(leaves(left));45 const b = new Map(leaves(right));46 const problems = [];47 for (const [path, value] of a) {48 if (isIgnored(path)) continue;49 if (!b.has(path)) problems.push(` ${path}: ${value} → (missing)`);50 else if (b.get(path) !== value) problems.push(` ${path}: ${value} → ${b.get(path)}`);51 }52 for (const [path, value] of b) {53 if (!a.has(path) && !isIgnored(path)) problems.push(` ${path}: (missing) → ${value}`);54 }55 if (problems.length > 0) {56 console.log(`step ${name}:`);57 for (const problem of problems) console.log(problem);58 }59 return problems.length;60}6162let total = 0;63const baselineSteps = baseline.steps.map((step) => step.name);64const candidateSteps = candidate.steps.map((step) => step.name);65if (JSON.stringify(baselineSteps) !== JSON.stringify(candidateSteps)) {66 console.log("step lists differ:");67 console.log(` baseline: ${baselineSteps.join(", ")}`);68 console.log(` candidate: ${candidateSteps.join(", ")}`);69 total += 1;70}71for (const step of baseline.steps) {72 const twin = candidate.steps.find((other) => other.name === step.name);73 if (twin) total += diffStep(step.name, step.snapshot, twin.snapshot);74}7576if (total === 0) {77 console.log(`identical across ${baseline.steps.length} steps` +78 (ignored.length > 0 ? ` (ignoring ${ignored.join(", ")})` : ""));79} else {80 console.log(`${total} differences`);81 process.exit(1);82}