@mcptoolshop/style-dataset-lab 2.3.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/CODE_OF_CONDUCT.md +9 -0
  3. package/CONTRIBUTING.md +71 -0
  4. package/README.md +82 -9
  5. package/SECURITY.md +6 -5
  6. package/bin/sdlab.js +359 -11
  7. package/lib/adapters/comfyui-runner.js +170 -0
  8. package/lib/adapters/comfyui-workflows.js +163 -0
  9. package/lib/adapters/diffusers-lora.js +7 -2
  10. package/lib/adapters/generic-image-caption.js +6 -1
  11. package/lib/args.js +200 -13
  12. package/lib/batch-compiler.js +122 -0
  13. package/lib/batch-modes.js +111 -0
  14. package/lib/batch-runs.js +237 -0
  15. package/lib/batch-sheet-render.js +209 -0
  16. package/lib/brief-compiler.js +625 -0
  17. package/lib/brief-render.js +205 -0
  18. package/lib/card.js +11 -4
  19. package/lib/comfyui.js +100 -6
  20. package/lib/config.js +68 -3
  21. package/lib/critique-engine.js +413 -0
  22. package/lib/critique-render.js +171 -0
  23. package/lib/deprecation.js +1 -1
  24. package/lib/eval-pack.js +94 -4
  25. package/lib/eval-runs.js +42 -3
  26. package/lib/export.js +144 -34
  27. package/lib/generated-provenance.js +87 -0
  28. package/lib/implementation-packs.js +15 -1
  29. package/lib/log.js +41 -4
  30. package/lib/paths.js +72 -3
  31. package/lib/refine-briefs.js +336 -0
  32. package/lib/reingest-selected.js +184 -0
  33. package/lib/reingest.js +15 -1
  34. package/lib/run-summary.js +132 -0
  35. package/lib/runtime-runs.js +218 -0
  36. package/lib/selections.js +375 -0
  37. package/lib/snapshot.js +100 -7
  38. package/lib/split.js +175 -17
  39. package/lib/training-manifests.js +17 -3
  40. package/lib/training-packages.js +28 -14
  41. package/lib/workflow-profiles.js +161 -0
  42. package/package.json +13 -4
  43. package/scripts/batch-generate.js +176 -0
  44. package/scripts/batch-sheet.js +46 -0
  45. package/scripts/batch-show.js +86 -0
  46. package/scripts/brief-compile.js +134 -0
  47. package/scripts/brief-show.js +53 -0
  48. package/scripts/canon-bind.js +7 -9
  49. package/scripts/card.js +22 -25
  50. package/scripts/compare.js +38 -33
  51. package/scripts/critique-show.js +50 -0
  52. package/scripts/critique.js +70 -0
  53. package/scripts/curate.js +91 -46
  54. package/scripts/doctor.js +172 -6
  55. package/scripts/eligibility.js +17 -14
  56. package/scripts/eval-pack.js +13 -11
  57. package/scripts/eval-run.js +30 -29
  58. package/scripts/export.js +27 -31
  59. package/scripts/generate-controlnet.js +31 -17
  60. package/scripts/generate-identity.js +37 -20
  61. package/scripts/generate-ipadapter.js +22 -16
  62. package/scripts/generate.js +68 -13
  63. package/scripts/implementation-pack.js +19 -14
  64. package/scripts/init.js +137 -17
  65. package/scripts/painterly.js +8 -10
  66. package/scripts/refine.js +119 -0
  67. package/scripts/reingest-selected.js +78 -0
  68. package/scripts/reingest.js +37 -18
  69. package/scripts/run-generate.js +76 -0
  70. package/scripts/run-list.js +58 -0
  71. package/scripts/run-show.js +63 -0
  72. package/scripts/select.js +119 -0
  73. package/scripts/selection-show.js +88 -0
  74. package/scripts/snapshot.js +22 -24
  75. package/scripts/split.js +26 -31
  76. package/scripts/training-manifest.js +29 -24
  77. package/scripts/training-package.js +24 -20
  78. package/scripts/training-profile.js +13 -8
  79. package/scripts/workflow-list.js +61 -0
  80. package/scripts/workflow-show.js +97 -0
  81. package/templates/domains/architecture/inputs/prompts/example-wave.json +32 -0
  82. package/templates/domains/architecture/terminology.json +10 -8
  83. package/templates/domains/architecture/workflows/profiles/environment-moodboard.json +62 -0
  84. package/templates/domains/architecture/workflows/profiles/interior-variant-pack.json +60 -0
  85. package/templates/domains/character-design/inputs/prompts/example-wave.json +32 -0
  86. package/templates/domains/character-design/project.json +4 -2
  87. package/templates/domains/character-design/workflows/profiles/character-portrait-set.json +61 -0
  88. package/templates/domains/character-design/workflows/profiles/expression-sheet.json +59 -0
  89. package/templates/domains/creature-design/inputs/prompts/example-wave.json +32 -0
  90. package/templates/domains/creature-design/project.json +4 -2
  91. package/templates/domains/creature-design/terminology.json +1 -0
  92. package/templates/domains/creature-design/workflows/profiles/creature-portrait-set.json +61 -0
  93. package/templates/domains/creature-design/workflows/profiles/silhouette-exploration.json +58 -0
  94. package/templates/domains/game-art/inputs/prompts/example-wave.json +37 -0
  95. package/templates/domains/game-art/project.json +4 -2
  96. package/templates/domains/game-art/workflows/profiles/character-portrait-set.json +62 -0
  97. package/templates/domains/game-art/workflows/profiles/environment-moodboard.json +61 -0
  98. package/templates/domains/vehicle-mech/inputs/prompts/example-wave.json +37 -0
  99. package/templates/domains/vehicle-mech/project.json +4 -2
  100. package/templates/domains/vehicle-mech/workflows/profiles/variant-pack.json +60 -0
  101. package/templates/domains/vehicle-mech/workflows/profiles/vehicle-silhouette-pack.json +59 -0
  102. package/scripts/bulk-curate-wave2-5.js +0 -138
  103. package/scripts/bulk-curate-waves11-18.js +0 -413
  104. package/scripts/curate-wave25.js +0 -209
  105. package/scripts/migrate-records.js +0 -141
  106. package/templates/inputs/prompts/example-wave.json +0 -27
@@ -0,0 +1,60 @@
1
+ {
2
+ "workflow_id": "variant-pack",
3
+ "label": "Vehicle Variant Pack",
4
+ "description": "Generate controlled design variants for a named vehicle subject.",
5
+ "lane_id": "exterior",
6
+ "subject_mode": "required",
7
+ "output_mode": "variant_pack",
8
+ "output_count": 4,
9
+ "required_reference_roles": ["anchor"],
10
+ "preferred_reference_tags": ["exterior", "approved"],
11
+ "prompt_strategy": {
12
+ "style_prefix": [
13
+ "vehicle design variant concept art",
14
+ "consistent form language",
15
+ "controlled variation"
16
+ ],
17
+ "structure": "style_prefix, subject_identity, lane_goal, canon_focus, controlled_variation",
18
+ "must_include": [
19
+ "vehicle identity",
20
+ "form language continuity",
21
+ "material palette"
22
+ ],
23
+ "optional_blocks": [
24
+ "color scheme note",
25
+ "loadout variant note"
26
+ ]
27
+ },
28
+ "negative_strategy": {
29
+ "must_avoid": [
30
+ "silhouette drift from reference",
31
+ "wrong faction materials",
32
+ "scale inconsistency"
33
+ ],
34
+ "lane_specific_avoid": [
35
+ "interior focus"
36
+ ],
37
+ "subject_specific_avoid": []
38
+ },
39
+ "canon_focus": [
40
+ "silhouette",
41
+ "material_language",
42
+ "palette",
43
+ "faction_read",
44
+ "scale_logic"
45
+ ],
46
+ "drift_guards": [
47
+ "preserve core silhouette from reference",
48
+ "preserve faction material language",
49
+ "preserve scale relationship"
50
+ ],
51
+ "runtime_defaults": {
52
+ "adapter_target": "comfyui",
53
+ "width": 1024,
54
+ "height": 1024,
55
+ "steps": 30,
56
+ "cfg": 6.5,
57
+ "sampler": "dpmpp_2m",
58
+ "seed_mode": "increment"
59
+ }
60
+ }
@@ -0,0 +1,59 @@
1
+ {
2
+ "workflow_id": "vehicle-silhouette-pack",
3
+ "label": "Vehicle Silhouette Pack",
4
+ "description": "Explore vehicle silhouette variations and form language.",
5
+ "lane_id": "exterior",
6
+ "subject_mode": "optional",
7
+ "output_mode": "silhouette_sheet",
8
+ "output_count": 6,
9
+ "required_reference_roles": [],
10
+ "preferred_reference_tags": ["exterior", "silhouette"],
11
+ "prompt_strategy": {
12
+ "style_prefix": [
13
+ "vehicle silhouette concept art",
14
+ "strong form language",
15
+ "high contrast shape exploration"
16
+ ],
17
+ "structure": "style_prefix, lane_goal, canon_focus, controlled_variation",
18
+ "must_include": [
19
+ "vehicle form language",
20
+ "silhouette variety",
21
+ "scale read"
22
+ ],
23
+ "optional_blocks": [
24
+ "faction note",
25
+ "role note"
26
+ ]
27
+ },
28
+ "negative_strategy": {
29
+ "must_avoid": [
30
+ "uniform silhouettes",
31
+ "over-detailed rendering",
32
+ "unrealistic proportions"
33
+ ],
34
+ "lane_specific_avoid": [
35
+ "interior focus",
36
+ "background complexity"
37
+ ],
38
+ "subject_specific_avoid": []
39
+ },
40
+ "canon_focus": [
41
+ "silhouette",
42
+ "scale_logic",
43
+ "faction_read",
44
+ "material_language"
45
+ ],
46
+ "drift_guards": [
47
+ "silhouettes must be readable at thumbnail scale",
48
+ "form language must stay within project vocabulary"
49
+ ],
50
+ "runtime_defaults": {
51
+ "adapter_target": "comfyui",
52
+ "width": 1024,
53
+ "height": 1024,
54
+ "steps": 30,
55
+ "cfg": 6.5,
56
+ "sampler": "dpmpp_2m",
57
+ "seed_mode": "random"
58
+ }
59
+ }
@@ -1,138 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Bulk curation for waves 2-5.
5
- * Applies rules-based initial curation, then individual overrides.
6
- * Must still visually verify — this is a first pass, not final judgment.
7
- */
8
-
9
- import { readFile, writeFile, rename, readdir, mkdir } from "node:fs/promises";
10
- import { join } from "node:path";
11
-
12
- const REPO_ROOT = new URL("..", import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1");
13
- const GAME = process.argv.find((a, i) => process.argv[i - 1] === '--game') || 'star-freight';
14
- const GAME_ROOT = join(REPO_ROOT, 'projects', GAME);
15
-
16
- const DEFAULT_APPROVED_SCORES = {
17
- silhouette_clarity: 0.8, palette_adherence: 0.8, material_fidelity: 0.8,
18
- faction_read: 0.8, wear_level: 0.8, style_consistency: 0.7,
19
- clothing_logic: 0.8, composition: 0.8,
20
- };
21
-
22
- const REJECT_EXPLANATIONS = {
23
- "REJECT2_fantasy_knight": { explanation: "Full medieval plate armor, heraldic tabard, broadsword and shield. Fantasy not sci-fi. Violates every art pillar.", failures: ["generic_scifi", "wrong_material"], scores: { silhouette_clarity: 0.8, palette_adherence: 0.2, material_fidelity: 0.4, faction_read: 0.0, wear_level: 0.3, style_consistency: 0.4, clothing_logic: 0.1, composition: 0.8 } },
24
- "REJECT2_cyberpunk_f": { explanation: "Neon hair, glowing circuit tattoos, shiny PVC bodysuit, LED accessories. Cyberpunk nightclub not cargo bay. Violates COL-003, CLO-003.", failures: ["wrong_palette", "costume_not_clothes"], scores: { silhouette_clarity: 0.8, palette_adherence: 0.1, material_fidelity: 0.3, faction_read: 0.0, wear_level: 0.1, style_consistency: 0.3, clothing_logic: 0.1, composition: 0.8 } },
25
- "REJECT2_too_sexy_f": { explanation: "Skin-tight form-fitting uniform emphasizing figure, styled hair, makeup, high heels. Cosplay not clothing. Violates CLO-003, MAT-002.", failures: ["costume_not_clothes", "too_clean"], scores: { silhouette_clarity: 0.8, palette_adherence: 0.5, material_fidelity: 0.3, faction_read: 0.3, wear_level: 0.1, style_consistency: 0.4, clothing_logic: 0.2, composition: 0.8 } },
26
- "REJECT2_warhammer_m": { explanation: "Bulky powered exoskeleton with imperial eagles, purity seals, gothic elements, chainsaw sword. Warhammer 40k power fantasy. Violates all five art pillars.", failures: ["generic_scifi", "too_heroic", "costume_not_clothes"], scores: { silhouette_clarity: 0.9, palette_adherence: 0.2, material_fidelity: 0.5, faction_read: 0.0, wear_level: 0.2, style_consistency: 0.4, clothing_logic: 0.1, composition: 0.8 } },
27
- "REJECT3_veshan_cute": { explanation: "Cute baby reptilian with big round eyes, pastel skin, tiny adorable outfit. Mascot character not warrior culture. Violates faction identity completely.", failures: ["faction_unreadable", "costume_not_clothes"], scores: { silhouette_clarity: 0.7, palette_adherence: 0.2, material_fidelity: 0.3, faction_read: 0.1, wear_level: 0.1, style_consistency: 0.3, clothing_logic: 0.2, composition: 0.8 } },
28
- "REJECT3_veshan_robot": { explanation: "Reptilian in sleek chrome powered armor covering every scale, glowing visor, jet thrusters. Mecha pilot not Veshan warrior. No forge marks, no imperfections. Violates MAT-001, CLO-002.", failures: ["generic_scifi", "wrong_material", "too_clean"], scores: { silhouette_clarity: 0.8, palette_adherence: 0.3, material_fidelity: 0.2, faction_read: 0.2, wear_level: 0.1, style_consistency: 0.5, clothing_logic: 0.2, composition: 0.8 } },
29
- "REJECT3_generic_lizard": { explanation: "Generic lizard person in brown tunic. No crest ridge, no brow ridges, no tail, round eyes, smooth skin, human proportions. Human in lizard mask, not an alien species.", failures: ["faction_unreadable", "wrong_material"], scores: { silhouette_clarity: 0.7, palette_adherence: 0.4, material_fidelity: 0.3, faction_read: 0.1, wear_level: 0.5, style_consistency: 0.5, clothing_logic: 0.3, composition: 0.8 } },
30
- "REJECT4_clean_pirate": { explanation: "Dashing pirate in pristine white shirt, embroidered vest, polished boots, jeweled rapier. Romance novel cover not lawless frontier. Violates MAT-002, CLO-003.", failures: ["too_clean", "costume_not_clothes"], scores: { silhouette_clarity: 0.8, palette_adherence: 0.3, material_fidelity: 0.3, faction_read: 0.2, wear_level: 0.1, style_consistency: 0.4, clothing_logic: 0.2, composition: 0.8 } },
31
- "REJECT4_matching_gang": { explanation: "Perfectly matching black tactical gear — same brand, same condition, all pristine. Video game loadout not Sable Reach. Nothing mismatched, nothing salvaged, nothing stolen.", failures: ["too_clean", "generic_scifi"], scores: { silhouette_clarity: 0.8, palette_adherence: 0.4, material_fidelity: 0.5, faction_read: 0.1, wear_level: 0.2, style_consistency: 0.5, clothing_logic: 0.2, composition: 0.8 } },
32
- "REJECT4_space_cowboy": { explanation: "Old west cowboy outfit in space — chaps, Stetson, plaid shirt, spurs. Genre costume at a sci-fi convention, not lived-in sci-fi. Violates CLO-003.", failures: ["costume_not_clothes", "faction_unreadable"], scores: { silhouette_clarity: 0.8, palette_adherence: 0.3, material_fidelity: 0.4, faction_read: 0.0, wear_level: 0.5, style_consistency: 0.3, clothing_logic: 0.1, composition: 0.8 } },
33
- "REJECT5_all_factions_salad": { explanation: "Designer outfit smoothly blending all faction aesthetics into one harmonious garment. Nothing is torn, stolen, or improvised. Violates every faction boundary rule.", failures: ["faction_unreadable", "too_clean"], scores: { silhouette_clarity: 0.8, palette_adherence: 0.3, material_fidelity: 0.5, faction_read: 0.1, wear_level: 0.3, style_consistency: 0.5, clothing_logic: 0.2, composition: 0.8 } },
34
- "REJECT5_no_faction_generic": { explanation: "Plain grey jumpsuit with zero cultural markers, no patches, no tools, no wear. Default human in default outfit. Nothing to learn from this.", failures: ["faction_unreadable"], scores: { silhouette_clarity: 0.7, palette_adherence: 0.4, material_fidelity: 0.3, faction_read: 0.0, wear_level: 0.3, style_consistency: 0.5, clothing_logic: 0.2, composition: 0.8 } },
35
- "REJECT5_luxury_scifi": { explanation: "Tailored suit, gold watch, polished shoes, groomed hair. Corporate luxury executive. Never loaded cargo, never fixed a reactor. Not working-class space.", failures: ["too_clean", "costume_not_clothes"], scores: { silhouette_clarity: 0.8, palette_adherence: 0.4, material_fidelity: 0.4, faction_read: 0.1, wear_level: 0.1, style_consistency: 0.5, clothing_logic: 0.2, composition: 0.8 } },
36
- "REJECT6_fashion_model_f": { explanation: "Haute couture space-themed evening gown with crystals and high heels. Fashion runway not shipboard life. Violates every art pillar.", failures: ["costume_not_clothes", "too_clean"], scores: { silhouette_clarity: 0.8, palette_adherence: 0.1, material_fidelity: 0.2, faction_read: 0.0, wear_level: 0.0, style_consistency: 0.3, clothing_logic: 0.0, composition: 0.8 } },
37
- "REJECT6_steampunk_m": { explanation: "Victorian steampunk with top hat, brass goggles, gears and cogs, steam-powered prosthetic. Retro-futurism cosplay not lived-in sci-fi. Wrong aesthetic entirely.", failures: ["costume_not_clothes", "generic_scifi"], scores: { silhouette_clarity: 0.8, palette_adherence: 0.3, material_fidelity: 0.4, faction_read: 0.0, wear_level: 0.4, style_consistency: 0.3, clothing_logic: 0.1, composition: 0.8 } },
38
- "REJECT6_identical_twins": { explanation: "Two identical figures in matching pristine uniforms. Violates single character rule and too-clean rule. Looks like mannequins or clones.", failures: ["bad_composition", "too_clean"], scores: { silhouette_clarity: 0.5, palette_adherence: 0.6, material_fidelity: 0.5, faction_read: 0.5, wear_level: 0.1, style_consistency: 0.5, clothing_logic: 0.4, composition: 0.2 } },
39
- "REJECT7_veshan_human_clothes": { explanation: "Veshan in Compact-style pressed uniform designed for human body. Fights against scale ridges and digitigrade legs. Wrong material vocabulary for species.", failures: ["wrong_material", "faction_unreadable"], scores: { silhouette_clarity: 0.7, palette_adherence: 0.5, material_fidelity: 0.2, faction_read: 0.2, wear_level: 0.6, style_consistency: 0.6, clothing_logic: 0.2, composition: 0.8 } },
40
- "REJECT7_cute_dragon": { explanation: "Adorable small dragon with sparkly eyes, tiny wings, pink pastel scales, cute crown and cape with stars. Mascot toy not warrior civilization member.", failures: ["faction_unreadable", "costume_not_clothes"], scores: { silhouette_clarity: 0.7, palette_adherence: 0.1, material_fidelity: 0.2, faction_read: 0.0, wear_level: 0.1, style_consistency: 0.2, clothing_logic: 0.1, composition: 0.8 } },
41
- "REJECT8_superhero_f": { explanation: "Glossy red and gold spandex bodysuit with cape, mask, power pose, stylized emblem. Superhero costume not clothing. Wrong genre entirely.", failures: ["costume_not_clothes", "generic_scifi"], scores: { silhouette_clarity: 0.8, palette_adherence: 0.1, material_fidelity: 0.2, faction_read: 0.0, wear_level: 0.0, style_consistency: 0.2, clothing_logic: 0.0, composition: 0.7 } },
42
- "REJECT8_zombified_m": { explanation: "Rotting grey skin, milky eyes, bloodied torn uniform. Horror zombie genre not lived-in sci-fi. Wrong genre.", failures: ["generic_scifi"], scores: { silhouette_clarity: 0.7, palette_adherence: 0.3, material_fidelity: 0.4, faction_read: 0.3, wear_level: 0.5, style_consistency: 0.3, clothing_logic: 0.3, composition: 0.7 } },
43
- "REJECT9_invisible_m": { explanation: "Cloaking suit making the character transparent. Cannot judge style of something invisible. Science fiction special effect not costume.", failures: ["generic_scifi"], scores: { silhouette_clarity: 0.1, palette_adherence: 0.2, material_fidelity: 0.1, faction_read: 0.0, wear_level: 0.0, style_consistency: 0.3, clothing_logic: 0.1, composition: 0.5 } },
44
- "REJECT9_alien_xenomorph": { explanation: "Biomechanical Xenomorph creature from the Alien franchise. No clothing, existing IP not original design. Wrong in every way.", failures: ["generic_scifi", "faction_unreadable"], scores: { silhouette_clarity: 0.8, palette_adherence: 0.2, material_fidelity: 0.3, faction_read: 0.0, wear_level: 0.0, style_consistency: 0.3, clothing_logic: 0.0, composition: 0.7 } },
45
- };
46
-
47
- async function main() {
48
- const recordsDir = join(GAME_ROOT, "records");
49
- const files = await readdir(recordsDir);
50
-
51
- let approved = 0;
52
- let rejected = 0;
53
- let total = 0;
54
-
55
- for (const file of files.filter(f => f.endsWith(".json")).sort()) {
56
- const path = join(recordsDir, file);
57
- const record = JSON.parse(await readFile(path, "utf-8"));
58
-
59
- // Skip already curated
60
- if (record.judgment) continue;
61
-
62
- total++;
63
- const id = record.id;
64
- const baseId = id.replace(/_v[12]$/, "");
65
-
66
- // Determine status
67
- let status, explanation, failures = [], scores;
68
-
69
- if (REJECT_EXPLANATIONS[baseId]) {
70
- const rej = REJECT_EXPLANATIONS[baseId];
71
- status = "rejected";
72
- explanation = rej.explanation;
73
- failures = rej.failures;
74
- scores = rej.scores;
75
- } else if (baseId.startsWith("BORDERLINE_")) {
76
- status = "borderline";
77
- explanation = `Almost on-style but fails in a subtle way that teaches nuance. See subject ID for specific issue.`;
78
- scores = { silhouette_clarity: 0.7, palette_adherence: 0.65, material_fidelity: 0.65, faction_read: 0.6, wear_level: 0.6, style_consistency: 0.65, clothing_logic: 0.6, composition: 0.75 };
79
- } else {
80
- // Default approve with standard scores
81
- status = "approved";
82
- explanation = `On-style gritty space costume. Faction-appropriate material vocabulary and wear level. Reads as a working person in a spacefaring civilization.`;
83
- scores = { ...DEFAULT_APPROVED_SCORES };
84
-
85
- // Adjust explanation by faction prefix
86
- if (id.startsWith("c2_")) {
87
- explanation = `Compact faction crew. Regulation shipboard clothing with appropriate wear and institutional identity markers. Muted palette, functional design.`;
88
- } else if (id.startsWith("v3_")) {
89
- explanation = `Veshan faction. Forged material vocabulary with overlapping scales, dark bronze plate, leather underpadding. Martial culture expressed through gear.`;
90
- scores.material_fidelity = 0.85;
91
- } else if (id.startsWith("sr4_")) {
92
- explanation = `Sable Reach outlaw. Salvaged, stolen, and improvised gear. Nothing matches, nothing is new, everything tells a survival story.`;
93
- scores.faction_read = 0.85;
94
- scores.wear_level = 0.9;
95
- } else if (id.startsWith("cf_")) {
96
- explanation = `Cross-faction comparison subject. Faction-appropriate costume for the specified role, useful for teaching same-role-different-faction visual discrimination.`;
97
- }
98
- }
99
-
100
- // Move image
101
- const oldPath = join(GAME_ROOT, record.asset_path);
102
- const newDir = `outputs/${status}`;
103
- const newPath = `${newDir}/${id}.png`;
104
- await mkdir(join(GAME_ROOT, newDir), { recursive: true });
105
-
106
- try {
107
- await rename(oldPath, join(GAME_ROOT, newPath));
108
- } catch {
109
- console.log(` skip ${id} — image not found at ${record.asset_path}`);
110
- continue;
111
- }
112
-
113
- // Update record
114
- record.asset_path = newPath;
115
- record.judgment = {
116
- status,
117
- reviewer: "bulk_curate_v1",
118
- reviewed_at: new Date().toISOString(),
119
- explanation,
120
- criteria_scores: scores,
121
- failure_modes: failures,
122
- improvement_notes: null,
123
- confidence: status === "rejected" ? 0.95 : 0.75, // bulk approvals get lower confidence
124
- };
125
-
126
- await writeFile(path, JSON.stringify(record, null, 2));
127
-
128
- if (status === "approved") approved++;
129
- else rejected++;
130
-
131
- const icon = status === "approved" ? "\x1b[32m✓\x1b[0m" : "\x1b[31m✗\x1b[0m";
132
- console.log(`${icon} ${id} → ${status}`);
133
- }
134
-
135
- console.log(`\n${total} curated: ${approved} approved, ${rejected} rejected`);
136
- }
137
-
138
- main().catch(console.error);