From 11a814fd13c42f63e2e3b34c1a2308d0f463558c Mon Sep 17 00:00:00 2001 From: Wickedness Date: Wed, 17 Jun 2026 04:16:55 +0900 Subject: [PATCH] Track worker card review decisions --- assets/cards/worker-review-decisions.json | 29 ++++ tools/list-worker-card-candidates.mjs | 157 ++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 assets/cards/worker-review-decisions.json create mode 100644 tools/list-worker-card-candidates.mjs diff --git a/assets/cards/worker-review-decisions.json b/assets/cards/worker-review-decisions.json new file mode 100644 index 0000000..fe17fac --- /dev/null +++ b/assets/cards/worker-review-decisions.json @@ -0,0 +1,29 @@ +{ + "schemaVersion": 1, + "decisions": [ + { + "characterId": "C0098", + "sourceRef": "codex/cards-C0100-C0051", + "sourceCommit": "717ceeef100774df81357443fe892c213b934e72", + "decision": "rejected", + "reasonCode": "weak_damage_progression", + "notes": "Damage stages are technically complete, but damage-1 and damage-2 are near-duplicates, and damage-3 and damage-4 also read as near-duplicates. The set does not clearly communicate cumulative damage escalation." + }, + { + "characterId": "C0279", + "sourceRef": "codex/cards-C0280-C0191", + "sourceCommit": "a3a3a50b3bee04913b4efd0e9ddc6a3fece29637", + "decision": "rejected", + "reasonCode": "duplicate_asset", + "notes": "damage-4 is byte-identical to the existing main asset assets/cards/C0999/damage-4.png, so the character set fails uniqueness review." + }, + { + "characterId": "C0280", + "sourceRef": "codex/cards-C0280-C0191", + "sourceCommit": "a3a3a50b3bee04913b4efd0e9ddc6a3fece29637", + "decision": "rejected", + "reasonCode": "identity_drift", + "notes": "The later damage stages drift away from the baseline identity; damage-3 is byte-identical to C0100 damage-3 from another worker branch. The set must be regenerated before main integration." + } + ] +} diff --git a/tools/list-worker-card-candidates.mjs b/tools/list-worker-card-candidates.mjs new file mode 100644 index 0000000..2ce745d --- /dev/null +++ b/tools/list-worker-card-candidates.mjs @@ -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, + ), +);