import { execFileSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; const root = process.cwd(); const decisionsPath = path.join(root, "assets", "cards", "worker-review-decisions.json"); function git(args) { return execFileSync("git", ["-c", "safe.directory=C:/Users/MD/source/repos/card_game", ...args], { cwd: root, encoding: "utf8", }).trim(); } function lines(output) { return output ? output.split(/\r?\n/).filter(Boolean) : []; } function listRefs() { const raw = git([ "for-each-ref", "--format=%(refname:short)", "refs/heads/codex/cards-*", "refs/remotes/origin/codex/cards-*", ]); const refs = lines(raw); const shortNames = [...new Set(refs.map((ref) => ref.replace(/^origin\//, "")))].sort(); return shortNames.map((shortName) => (refs.includes(shortName) ? shortName : `origin/${shortName}`)); } function listPngs(ref) { try { return lines(git(["ls-tree", "-r", "--name-only", ref, "assets/cards"])).filter((file) => /^assets\/cards\/C\d{4}\/damage-[0-4]\.png$/.test(file), ); } catch { return []; } } function completeCharacters(files) { const stagesByCharacter = new Map(); for (const file of files) { const match = file.match(/^assets\/cards\/(C\d{4})\/damage-([0-4])\.png$/); if (!match) continue; if (!stagesByCharacter.has(match[1])) stagesByCharacter.set(match[1], new Set()); stagesByCharacter.get(match[1]).add(match[2]); } return [...stagesByCharacter] .filter(([, stages]) => [0, 1, 2, 3, 4].every((stage) => stages.has(String(stage)))) .map(([characterId]) => characterId) .sort(); } function loadDecisions() { if (!fs.existsSync(decisionsPath)) return []; const payload = JSON.parse(fs.readFileSync(decisionsPath, "utf8")); return Array.isArray(payload.decisions) ? payload.decisions : []; } function blobRows(ref, characterId) { const prefix = `assets/cards/${characterId}/`; const output = git(["ls-tree", "-r", ref, prefix]); return lines(output) .map((row) => { const match = row.match(/^\d+\s+blob\s+([0-9a-f]+)\s+(.+)$/); return match ? { blob: match[1], path: match[2] } : null; }) .filter(Boolean); } const mainFiles = listPngs("main"); const mainComplete = completeCharacters(mainFiles); const mainCompleteSet = new Set(mainComplete); const mainBlobToPath = new Map(); for (const file of mainFiles) { const row = blobRows("main", file.match(/^assets\/cards\/(C\d{4})\//)[1]).find((item) => item.path === file); if (row && !mainBlobToPath.has(row.blob)) mainBlobToPath.set(row.blob, row.path); } const decisions = loadDecisions(); const rejectedByRefCommitCharacter = new Map( decisions .filter((decision) => decision.decision === "rejected") .map((decision) => [ `${decision.sourceRef}@${decision.sourceCommit}:${decision.characterId}`, decision, ]), ); const branches = []; const candidates = []; const excluded = []; for (const ref of listRefs()) { const commit = git(["rev-parse", ref]); const pngs = listPngs(ref); const complete = completeCharacters(pngs); const newComplete = complete.filter((characterId) => !mainCompleteSet.has(characterId)); const branch = { ref, commit, pngCount: pngs.length, completeCharacterCount: complete.length, newCompleteCharacterCount: newComplete.length, newCompleteCharacters: newComplete, }; branches.push(branch); for (const characterId of newComplete) { const rejection = rejectedByRefCommitCharacter.get(`${ref}@${commit}:${characterId}`); if (rejection) { excluded.push({ ref, commit, characterId, reasonCode: rejection.reasonCode, notes: rejection.notes, }); continue; } const duplicateBlobs = blobRows(ref, characterId) .filter((row) => mainBlobToPath.has(row.blob)) .map((row) => ({ candidatePath: row.path, existingPath: mainBlobToPath.get(row.blob), })); candidates.push({ ref, commit, characterId, duplicateBlobs, needsReview: duplicateBlobs.length > 0 ? "duplicate-check" : "visual-review", }); } } console.log( JSON.stringify( { schemaVersion: 1, main: { pngCount: mainFiles.length, completeCharacterCount: mainComplete.length, }, candidates, excluded, branches, }, null, 2, ), );