#!/usr/bin/env node // Usage: node agents/darkwing/work/queue-a2/map-check.mjs [QUEUE.md] [MAP] // // Run before genesis. The map names the QUEUE.md blob it was built from. // This lists every table line that differs from that blob, then every row // whose piece, owner or issues no longer match the map. Reads only; the // one git call is `cat-file`. Exit 0 no drift, 1 drift, 4 usage. import { execFileSync } from "node:child_process"; import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const top = join(dirname(fileURLToPath(import.meta.url)), "../../../.."); const { parseMigrationMap } = await import(join(top, "packages/queue/src/queue.mjs")); // Table lines by id: `| N | ...` rows, then the parked table's items // numbered on from the highest row, as the map numbers them. export function tableLines(text) { const out = new Map(); const items = []; let parked = false; for (const line of text.split("\n")) { if (line.startsWith("## ")) parked = line.startsWith("## Parked"); const m = /^\| (\d+) \|/.exec(line); if (m && !parked) out.set(Number(m[1]), line); else if (parked && line.startsWith("| ") && !line.startsWith("| Item ") && !line.startsWith("|---")) items.push(line); } let next = Math.max(0, ...out.keys()) + 1; for (const line of items) out.set(next++, line); return out; } const cells = (line) => line.slice(2, -2).split(" | "); export function check(queueText, mapText, oldText) { const drift = []; const now = tableLines(queueText); const then = tableLines(oldText); for (const id of new Set([...now.keys(), ...then.keys()])) { if (now.get(id) !== then.get(id)) drift.push(`row ${id}: ${!then.has(id) ? "added" : !now.has(id) ? "removed" : "changed"} since the map's QUEUE.md blob`); } const map = parseMigrationMap(mapText); const byId = new Map(map.rows.map((r) => [r.id, r])); for (const [id, line] of now) { const r = byId.get(id); if (!r) { drift.push(`row ${id}: in QUEUE.md, not in the map`); continue; } const c = cells(line); if (!/^\d+$/.test(c[0])) { if (c[0] !== r.piece) drift.push(`row ${id}: parked item ${JSON.stringify(c[0])} is not the map's piece`); continue; } if (c[1] !== r.piece) drift.push(`row ${id}: piece differs from the map`); const owner = /^[a-z][a-z0-9-]*/.exec(c[2])?.[0]; if (owner !== r.owner) drift.push(`row ${id}: owner ${owner} in QUEUE.md, ${r.owner} in the map`); const issues = [...c[3].matchAll(/#(\d+)/g)].map((x) => Number(x[1])); if (issues.join() !== r.issues.join()) drift.push(`row ${id}: issues ${issues.join(",") || "none"} in QUEUE.md, ${r.issues.join(",") || "none"} in the map`); } for (const id of byId.keys()) if (!now.has(id)) drift.push(`row ${id}: in the map, not in QUEUE.md`); return drift; } if (process.argv[1] === fileURLToPath(import.meta.url)) { const args = process.argv.slice(2); if (args.length > 2) { console.error("usage: map-check.mjs [QUEUE.md] [MAP]"); process.exit(4); } const queueText = readFileSync(args[0] ?? join(top, "docs/plans/QUEUE.md"), "utf8"); const mapText = readFileSync(args[1] ?? join(top, "agents/darkwing/work/queue-migration-map.md"), "utf8"); const blob = /QUEUE\.md` blob `([0-9a-f]{40})`/.exec(mapText)?.[1]; if (!blob) { console.error("the map names no QUEUE.md blob"); process.exit(1); } const oldText = execFileSync("git", ["-C", top, "cat-file", "blob", blob]).toString("utf8"); const drift = check(queueText, mapText, oldText); for (const d of drift) console.log(d); console.log(drift.length ? `${drift.length} differences; update the map before genesis` : `ok: QUEUE.md matches the map (blob ${blob.slice(0, 8)})`); process.exit(drift.length ? 1 : 0); }