Track worker card review decisions

This commit is contained in:
2026-06-17 04:16:55 +09:00
parent da83b41784
commit 11a814fd13
2 changed files with 186 additions and 0 deletions

View 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,
),
);