Track worker card review decisions
This commit is contained in:
157
tools/list-worker-card-candidates.mjs
Normal file
157
tools/list-worker-card-candidates.mjs
Normal file
@@ -0,0 +1,157 @@
|
||||
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,
|
||||
),
|
||||
);
|
||||
Reference in New Issue
Block a user