Files
longbridge__developers/scripts/opencli/url-diff.ts
twosugar 1fadfe2234 fix(migrate): remove dead code in opencli scripts and add A/B/C severity bucketing
- url-diff.ts: removed unused normalizeUrl (pathOnly is the live path).
- snapshot.ts: removed orphan dirname import and dead pngPath variable.
- report.ts: added A/B/C severity classification per spec §9.7. Each
  route is labeled [A] blocking / [B] discuss / [C] clean based on
  URL, DOM, interaction, and visual signals. Bucket totals added
  to the summary header.
Ruling in
.superpowers/sdd/2026-08-17-astro-migration-stage-1/progress.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-17 19:56:54 +08:00

142 lines
4.0 KiB
TypeScript

#!/usr/bin/env bun
/**
* url-diff.ts — Compute symmetric difference between two sitemap URL sets.
*
* Usage:
* bun run scripts/opencli/url-diff.ts <old-sitemap.xml> <new-sitemap.xml>
* bun run scripts/opencli/url-diff.ts --help
*
* Exit codes:
* 0 — URL sets are identical (A △ B = ∅) — hard gate passes
* 1 — Sets differ; reports added/removed URLs to stdout
*
* This is the T18 URL gate:
* spec §5.5 / §9.2 require URL set A △ B = ∅ before promotion.
*/
import { parseSitemap } from "./crawl-routes.ts";
function printHelp() {
console.log(`
url-diff.ts — compute symmetric difference between two sitemap URL sets.
Usage:
bun run scripts/opencli/url-diff.ts <old-sitemap.xml> <new-sitemap.xml> [options]
bun run scripts/opencli/url-diff.ts --help
Arguments:
<old-sitemap.xml> Baseline sitemap (e.g. vitepress dist/sitemap.xml)
<new-sitemap.xml> Candidate sitemap (e.g. astro dist/sitemap-index.xml)
Options:
--json Output machine-readable JSON instead of human-readable text
--help Show this help
Exit codes:
0 URL sets match (A △ B = ∅) — T18 URL hard gate passes
1 Mismatch detected — inspect output for added/removed URLs
`);
}
/** Strip origin so we compare only paths — handles baseline/candidate origin mismatch. */
function pathOnly(url: string): string {
try {
return new URL(url).pathname.replace(/\/$/, "") || "/";
} catch {
return url.trim().replace(/\/$/, "");
}
}
export interface UrlDiffResult {
same: boolean;
oldCount: number;
newCount: number;
onlyInOld: string[];
onlyInNew: string[];
}
/**
* Compare two sets of URLs (from parsed sitemaps).
* Comparison is path-only (ignores origin differences between vitepress / astro dev).
*/
export function diffUrls(oldUrls: string[], newUrls: string[]): UrlDiffResult {
const oldPaths = new Set(oldUrls.map(pathOnly));
const newPaths = new Set(newUrls.map(pathOnly));
const onlyInOld = [...oldPaths].filter((p) => !newPaths.has(p)).sort();
const onlyInNew = [...newPaths].filter((p) => !oldPaths.has(p)).sort();
return {
same: onlyInOld.length === 0 && onlyInNew.length === 0,
oldCount: oldPaths.size,
newCount: newPaths.size,
onlyInOld,
onlyInNew,
};
}
async function main() {
const args = process.argv.slice(2);
if (args.includes("--help") || args.length === 0) {
printHelp();
process.exit(0);
}
const jsonMode = args.includes("--json");
const positional = args.filter((a) => !a.startsWith("--"));
if (positional.length < 2) {
console.error("Error: two sitemap paths required. Run with --help for usage.");
process.exit(1);
}
const [oldPath, newPath] = positional;
let oldXml: string;
let newXml: string;
try {
oldXml = await Bun.file(oldPath).text();
} catch {
console.error(`Error: cannot read old sitemap: ${oldPath}`);
process.exit(1);
}
try {
newXml = await Bun.file(newPath).text();
} catch {
console.error(`Error: cannot read new sitemap: ${newPath}`);
process.exit(1);
}
const oldUrls = parseSitemap(oldXml);
const newUrls = parseSitemap(newXml);
const result = diffUrls(oldUrls, newUrls);
if (jsonMode) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(`Old sitemap: ${oldUrls.length} URLs`);
console.log(`New sitemap: ${newUrls.length} URLs`);
if (result.same) {
console.log("✓ URL sets match — A △ B = ∅");
} else {
if (result.onlyInOld.length > 0) {
console.log(`\nMissing in new (${result.onlyInOld.length} URLs):`);
result.onlyInOld.forEach((u) => console.log(` - ${u}`));
}
if (result.onlyInNew.length > 0) {
console.log(`\nExtra in new (${result.onlyInNew.length} URLs):`);
result.onlyInNew.forEach((u) => console.log(` + ${u}`));
}
console.log(`\n✗ URL mismatch — T18 gate fails`);
}
}
process.exit(result.same ? 0 : 1);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});