@mcptoolshop/style-dataset-lab 3.0.0 → 3.1.0

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 (99) hide show
  1. package/CHANGELOG.md +108 -15
  2. package/CODE_OF_CONDUCT.md +9 -0
  3. package/CONTRIBUTING.md +71 -0
  4. package/README.es.md +99 -8
  5. package/README.fr.md +100 -9
  6. package/README.hi.md +102 -9
  7. package/README.it.md +99 -8
  8. package/README.ja.md +100 -9
  9. package/README.md +64 -1
  10. package/README.pt-BR.md +99 -8
  11. package/README.zh.md +100 -9
  12. package/SECURITY.md +6 -5
  13. package/bin/sdlab.js +289 -62
  14. package/lib/adapters/comfyui-runner.js +11 -15
  15. package/lib/adapters/diffusers-lora.js +7 -2
  16. package/lib/adapters/generic-image-caption.js +6 -1
  17. package/lib/args.js +200 -13
  18. package/lib/batch-compiler.js +7 -3
  19. package/lib/batch-runs.js +162 -31
  20. package/lib/batch-sheet-render.js +5 -2
  21. package/lib/brief-compiler.js +38 -11
  22. package/lib/card.js +11 -4
  23. package/lib/comfyui-output.js +70 -0
  24. package/lib/comfyui.js +100 -6
  25. package/lib/config.js +68 -3
  26. package/lib/deprecation.js +1 -1
  27. package/lib/eval-pack.js +94 -4
  28. package/lib/eval-runs.js +42 -3
  29. package/lib/export.js +144 -34
  30. package/lib/implementation-packs.js +15 -1
  31. package/lib/log.js +41 -4
  32. package/lib/paths.js +41 -3
  33. package/lib/refine-briefs.js +16 -9
  34. package/lib/reingest-selected.js +14 -3
  35. package/lib/reingest.js +15 -1
  36. package/lib/run-summary.js +3 -2
  37. package/lib/runtime-runs.js +59 -9
  38. package/lib/selections.js +54 -2
  39. package/lib/snapshot.js +100 -7
  40. package/lib/split.js +175 -17
  41. package/lib/training-adapters.js +79 -0
  42. package/lib/training-manifests.js +17 -3
  43. package/lib/training-packages.js +29 -28
  44. package/lib/training-profiles.js +10 -0
  45. package/package.json +27 -3
  46. package/scripts/batch-generate.js +72 -20
  47. package/scripts/batch-sheet.js +3 -3
  48. package/scripts/batch-show.js +3 -3
  49. package/scripts/brief-compile.js +8 -8
  50. package/scripts/brief-show.js +4 -3
  51. package/scripts/canon-bind.js +7 -9
  52. package/scripts/card.js +22 -25
  53. package/scripts/compare.js +38 -33
  54. package/scripts/critique-show.js +3 -3
  55. package/scripts/critique.js +3 -3
  56. package/scripts/curate.js +91 -46
  57. package/scripts/doctor.js +3 -5
  58. package/scripts/eligibility.js +17 -14
  59. package/scripts/eval-pack.js +13 -11
  60. package/scripts/eval-run.js +30 -29
  61. package/scripts/export.js +27 -31
  62. package/scripts/generate-controlnet.js +31 -17
  63. package/scripts/generate-identity.js +37 -20
  64. package/scripts/generate-ipadapter.js +22 -16
  65. package/scripts/generate.js +96 -29
  66. package/scripts/implementation-pack.js +19 -14
  67. package/scripts/init.js +115 -17
  68. package/scripts/painterly.js +8 -10
  69. package/scripts/refine.js +3 -3
  70. package/scripts/reingest-selected.js +3 -3
  71. package/scripts/reingest.js +33 -20
  72. package/scripts/run-generate.js +4 -4
  73. package/scripts/run-list.js +3 -3
  74. package/scripts/run-show.js +3 -3
  75. package/scripts/select.js +5 -4
  76. package/scripts/selection-show.js +3 -3
  77. package/scripts/snapshot.js +22 -24
  78. package/scripts/split.js +26 -31
  79. package/scripts/training-manifest.js +29 -24
  80. package/scripts/training-package.js +24 -20
  81. package/scripts/training-profile.js +13 -8
  82. package/scripts/workflow-list.js +2 -2
  83. package/scripts/workflow-show.js +4 -3
  84. package/templates/domains/architecture/inputs/prompts/example-wave.json +32 -0
  85. package/templates/domains/architecture/terminology.json +10 -8
  86. package/templates/domains/character-design/inputs/prompts/example-wave.json +32 -0
  87. package/templates/domains/character-design/project.json +4 -2
  88. package/templates/domains/creature-design/inputs/prompts/example-wave.json +32 -0
  89. package/templates/domains/creature-design/project.json +4 -2
  90. package/templates/domains/creature-design/terminology.json +1 -0
  91. package/templates/domains/game-art/inputs/prompts/example-wave.json +37 -0
  92. package/templates/domains/game-art/project.json +4 -2
  93. package/templates/domains/vehicle-mech/inputs/prompts/example-wave.json +37 -0
  94. package/templates/domains/vehicle-mech/project.json +4 -2
  95. package/scripts/bulk-curate-wave2-5.js +0 -138
  96. package/scripts/bulk-curate-waves11-18.js +0 -413
  97. package/scripts/curate-wave25.js +0 -209
  98. package/scripts/migrate-records.js +0 -141
  99. package/templates/inputs/prompts/example-wave.json +0 -27
package/scripts/card.js CHANGED
@@ -4,43 +4,44 @@
4
4
  * card.js — Generate dataset cards.
5
5
  *
6
6
  * Usage:
7
- * sdlab card generate --snapshot <id> --split <id> [--project <name>]
7
+ * sdlab card generate [--snapshot <id>] [--split <id>] [--project <name>]
8
8
  */
9
9
 
10
10
  import { join } from 'node:path';
11
11
  import { writeFile } from 'node:fs/promises';
12
- import { getProjectName } from '../lib/args.js';
12
+ import { parseArgs, getProjectName } from '../lib/args.js';
13
13
  import { REPO_ROOT } from '../lib/paths.js';
14
+ import { inputError, handleCliError } from '../lib/errors.js';
14
15
  import { listSnapshots } from '../lib/snapshot.js';
15
16
  import { listSplits } from '../lib/split.js';
16
17
  import { generateCard } from '../lib/card.js';
17
18
 
18
19
  export async function run(argv = process.argv.slice(2)) {
19
- const projectName = getProjectName(argv);
20
+ const { flags, positionals } = parseArgs(argv, {
21
+ flags: {
22
+ project: { type: 'string' },
23
+ snapshot: { type: 'string' },
24
+ split: { type: 'string' },
25
+ },
26
+ deprecated: { game: 'project' },
27
+ });
28
+
29
+ const projectName = flags.project || getProjectName(argv);
20
30
  const projectRoot = join(REPO_ROOT, 'projects', projectName);
21
- const subcommand = argv.find(a => !a.startsWith('--')) || 'generate';
22
- const args = argv.filter(a => a !== subcommand);
31
+ const subcommand = positionals[0] || 'generate';
23
32
 
24
33
  if (subcommand === 'generate') {
25
- // Find snapshot
26
- let snapshotId;
27
- const snapIdx = args.indexOf('--snapshot');
28
- if (snapIdx >= 0) {
29
- snapshotId = args[snapIdx + 1];
30
- } else {
34
+ let snapshotId = flags.snapshot;
35
+ if (!snapshotId) {
31
36
  const snapshots = await listSnapshots(projectRoot);
32
- if (snapshots.length === 0) throw new Error('No snapshots found. Run: sdlab snapshot create');
37
+ if (snapshots.length === 0) throw inputError('INPUT_NO_SNAPSHOT', 'No snapshots found. Run: sdlab snapshot create');
33
38
  snapshotId = snapshots[snapshots.length - 1].id;
34
39
  }
35
40
 
36
- // Find split
37
- let splitId;
38
- const splitIdx = args.indexOf('--split');
39
- if (splitIdx >= 0) {
40
- splitId = args[splitIdx + 1];
41
- } else {
41
+ let splitId = flags.split;
42
+ if (!splitId) {
42
43
  const splits = await listSplits(projectRoot);
43
- if (splits.length === 0) throw new Error('No splits found. Run: sdlab split build');
44
+ if (splits.length === 0) throw inputError('INPUT_NO_SPLIT', 'No splits found. Run: sdlab split build');
44
45
  splitId = splits[splits.length - 1].id;
45
46
  }
46
47
 
@@ -51,7 +52,6 @@ export async function run(argv = process.argv.slice(2)) {
51
52
 
52
53
  const { markdown, json } = await generateCard(projectRoot, snapshotId, splitId);
53
54
 
54
- // Write to project root
55
55
  const mdPath = join(projectRoot, 'dataset-card.md');
56
56
  const jsonPath = join(projectRoot, 'dataset-card.json');
57
57
  await writeFile(mdPath, markdown);
@@ -61,13 +61,10 @@ export async function run(argv = process.argv.slice(2)) {
61
61
  console.log(` \x1b[32m✓\x1b[0m dataset-card.json`);
62
62
 
63
63
  } else {
64
- throw new Error(`Unknown subcommand: ${subcommand}. Use: generate`);
64
+ throw inputError('INPUT_BAD_SUBCOMMAND', `Unknown subcommand: ${subcommand}. Use: generate`);
65
65
  }
66
66
  }
67
67
 
68
68
  if (process.argv[1] && (process.argv[1].endsWith('card.js') || process.argv[1].endsWith('card'))) {
69
- run().catch((err) => {
70
- console.error(err.message || err);
71
- process.exit(1);
72
- });
69
+ run().catch(handleCliError);
73
70
  }
@@ -8,62 +8,70 @@
8
8
  * sdlab compare iron_sword_base iron_sword_icon_mid a "Better silhouette" --project star-freight
9
9
  */
10
10
 
11
- import { readFile, writeFile, readdir, mkdir } from "node:fs/promises";
11
+ import { writeFile, readdir, mkdir, readFile } from "node:fs/promises";
12
12
  import { join } from "node:path";
13
- import { getProjectName } from "../lib/args.js";
13
+ import { parseArgs, getProjectName } from "../lib/args.js";
14
14
  import { REPO_ROOT } from "../lib/paths.js";
15
-
16
- function getFlagValue(args, flag) {
17
- const prefix = `--${flag}=`;
18
- for (const a of args) {
19
- if (a.startsWith(prefix)) return a.slice(prefix.length);
20
- }
21
- const idx = args.indexOf(`--${flag}`);
22
- if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
23
- return undefined;
24
- }
15
+ import { readJsonFile } from "../lib/config.js";
16
+ import { inputError, handleCliError } from "../lib/errors.js";
25
17
 
26
18
  export async function run(argv = process.argv.slice(2)) {
27
- const projectName = getProjectName(argv);
28
- const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
29
-
30
- // Strip --project/--game and their values from positional parsing
31
- const args = argv.filter((a, i) => {
32
- if (a === '--project' || a === '--game') return false;
33
- if (i > 0 && (argv[i - 1] === '--project' || argv[i - 1] === '--game')) return false;
34
- return true;
19
+ const { flags, positionals } = parseArgs(argv, {
20
+ flags: {
21
+ project: { type: 'string' },
22
+ scores: { type: 'string' },
23
+ },
24
+ deprecated: { game: 'project' },
35
25
  });
36
26
 
37
- const [assetAId, assetBId, winner, ...reasonParts] = args;
27
+ const projectName = flags.project || getProjectName(argv);
28
+ const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
29
+
30
+ const [assetAId, assetBId, winner, ...reasonParts] = positionals;
38
31
 
39
32
  if (!assetAId || !assetBId || !winner) {
40
- throw new Error("Usage: sdlab compare <asset_a_id> <asset_b_id> <a|b|tie> <reasoning>");
33
+ throw inputError(
34
+ 'INPUT_MISSING_ARGS',
35
+ 'Usage: sdlab compare <asset_a_id> <asset_b_id> <a|b|tie> <reasoning>'
36
+ );
41
37
  }
42
38
 
43
39
  if (!["a", "b", "tie"].includes(winner)) {
44
- throw new Error(`Invalid winner: ${winner}. Must be a, b, or tie.`);
40
+ throw inputError(
41
+ 'INPUT_BAD_WINNER',
42
+ `Invalid winner: ${winner}. Must be a, b, or tie.`
43
+ );
45
44
  }
46
45
 
47
- const reasoning = reasonParts.filter(r => !r.startsWith('--')).join(" ") || null;
46
+ const reasoning = reasonParts.join(" ") || null;
48
47
 
49
48
  // Load records to get paths
50
49
  const recordA = await loadRecord(GAME_ROOT, assetAId);
51
50
  const recordB = await loadRecord(GAME_ROOT, assetBId);
52
51
 
53
52
  if (!recordA || !recordB) {
54
- throw new Error("Could not load one or both records.");
53
+ throw inputError('INPUT_UNKNOWN_RECORD', 'Could not load one or both records.');
55
54
  }
56
55
 
57
56
  // Parse optional scores
58
- const scoresStr = getFlagValue(args, "scores");
57
+ const scoresStr = flags.scores;
59
58
  const criteria_scores = {};
60
59
  if (scoresStr) {
61
60
  for (const pair of scoresStr.split(",")) {
62
61
  const [k, v] = pair.split(":");
63
62
  // Format: "silhouette:0.9/0.6" → { silhouette: { a: 0.9, b: 0.6 } }
64
63
  if (k && v) {
65
- const [aScore, bScore] = v.split("/");
66
- criteria_scores[k.trim()] = { a: parseFloat(aScore), b: parseFloat(bScore) };
64
+ const [aStr, bStr] = v.split("/");
65
+ const aScore = parseFloat(aStr);
66
+ const bScore = parseFloat(bStr);
67
+ if (!Number.isFinite(aScore) || !Number.isFinite(bScore)) {
68
+ throw inputError(
69
+ 'INPUT_BAD_SCORE',
70
+ `Score for "${k.trim()}" is not numeric: "${v}"`,
71
+ 'Use the form --scores silhouette:0.9/0.6,palette:0.8/0.7'
72
+ );
73
+ }
74
+ criteria_scores[k.trim()] = { a: aScore, b: bScore };
67
75
  }
68
76
  }
69
77
  }
@@ -101,7 +109,7 @@ export async function run(argv = process.argv.slice(2)) {
101
109
  async function loadRecord(GAME_ROOT, id) {
102
110
  try {
103
111
  const path = join(GAME_ROOT, `records/${id}.json`);
104
- return JSON.parse(await readFile(path, "utf-8"));
112
+ return await readJsonFile(path);
105
113
  } catch {
106
114
  console.error(`Record not found: records/${id}.json`);
107
115
  return null;
@@ -119,8 +127,5 @@ async function countComparisons(GAME_ROOT) {
119
127
 
120
128
  // Direct execution guard
121
129
  if (process.argv[1] && (process.argv[1].endsWith('compare.js') || process.argv[1].endsWith('compare'))) {
122
- run().catch((err) => {
123
- console.error(err.message || err);
124
- process.exit(1);
125
- });
130
+ run().catch(handleCliError);
126
131
  }
@@ -9,7 +9,7 @@
9
9
  * sdlab critique show --project star-freight --run run_2026-04-15_001 --md
10
10
  */
11
11
 
12
- import { parseArgs } from '../lib/args.js';
12
+ import { parseArgs, getProjectName } from '../lib/args.js';
13
13
  import { getProjectRoot } from '../lib/paths.js';
14
14
  import { loadCritique } from '../lib/critique-engine.js';
15
15
  import { renderCritiqueMarkdown, renderCritiqueText } from '../lib/critique-render.js';
@@ -17,7 +17,7 @@ import { renderCritiqueMarkdown, renderCritiqueText } from '../lib/critique-rend
17
17
  export async function run(argv = process.argv.slice(2)) {
18
18
  const { flags } = parseArgs(argv, {
19
19
  flags: {
20
- project: { type: 'string', default: 'star-freight' },
20
+ project: { type: 'string' },
21
21
  run: { type: 'string' },
22
22
  json: { type: 'boolean' },
23
23
  md: { type: 'boolean' },
@@ -35,7 +35,7 @@ export async function run(argv = process.argv.slice(2)) {
35
35
  return;
36
36
  }
37
37
 
38
- const projectName = flags.project;
38
+ const projectName = flags.project || getProjectName(argv);
39
39
  const projectRoot = getProjectRoot(projectName);
40
40
 
41
41
  const report = loadCritique(projectRoot, flags.run);
@@ -8,7 +8,7 @@
8
8
  * sdlab critique --project star-freight --run run_2026-04-15_001 --json
9
9
  */
10
10
 
11
- import { parseArgs } from '../lib/args.js';
11
+ import { parseArgs, getProjectName } from '../lib/args.js';
12
12
  import { getProjectRoot } from '../lib/paths.js';
13
13
  import { critiqueRun, saveCritique } from '../lib/critique-engine.js';
14
14
  import { renderCritiqueMarkdown, renderCritiqueText } from '../lib/critique-render.js';
@@ -17,7 +17,7 @@ import { info } from '../lib/log.js';
17
17
  export async function run(argv = process.argv.slice(2)) {
18
18
  const { flags } = parseArgs(argv, {
19
19
  flags: {
20
- project: { type: 'string', default: 'star-freight' },
20
+ project: { type: 'string' },
21
21
  run: { type: 'string' },
22
22
  json: { type: 'boolean' },
23
23
  },
@@ -33,7 +33,7 @@ export async function run(argv = process.argv.slice(2)) {
33
33
  return;
34
34
  }
35
35
 
36
- const projectName = flags.project;
36
+ const projectName = flags.project || getProjectName(argv);
37
37
  const projectRoot = getProjectRoot(projectName);
38
38
 
39
39
  console.log(`\x1b[1mstyle-dataset-lab\x1b[0m critique`);
package/scripts/curate.js CHANGED
@@ -7,59 +7,73 @@
7
7
  * node scripts/curate.js <asset_id> approved "Clean silhouette, correct palette" --scores silhouette:0.9,palette:0.8
8
8
  * node scripts/curate.js <asset_id> rejected "3D render look, alpha halo" --failures alpha_halo,3d_render_look
9
9
  * node scripts/curate.js --list (show uncurated candidates)
10
- * sdlab curate <asset_id> approved "explanation" --project star-freight
10
+ * sdlab curate <asset_id> approved "explanation" --project star-freight [--dry-run]
11
11
  */
12
12
 
13
- import { readFile, writeFile, rename, readdir, mkdir } from "node:fs/promises";
13
+ import { writeFile, rename, readdir, mkdir, readFile } from "node:fs/promises";
14
14
  import { join } from "node:path";
15
- import { getProjectName } from "../lib/args.js";
15
+ import { parseArgs, getProjectName } from "../lib/args.js";
16
16
  import { REPO_ROOT } from "../lib/paths.js";
17
-
18
- function getFlagValue(args, flag) {
19
- const prefix = `--${flag}=`;
20
- for (const a of args) {
21
- if (a.startsWith(prefix)) return a.slice(prefix.length);
22
- }
23
- const idx = args.indexOf(`--${flag}`);
24
- if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
25
- return undefined;
26
- }
17
+ import { readJsonFile } from "../lib/config.js";
18
+ import { inputError, runtimeError, handleCliError } from "../lib/errors.js";
19
+ import { result } from "../lib/log.js";
27
20
 
28
21
  export async function run(argv = process.argv.slice(2)) {
29
- const projectName = getProjectName(argv);
30
- const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
31
-
32
- // Strip --project/--game and their values from positional parsing
33
- const args = argv.filter((a, i) => {
34
- if (a === '--project' || a === '--game') return false;
35
- if (i > 0 && (argv[i - 1] === '--project' || argv[i - 1] === '--game')) return false;
36
- return true;
22
+ const { flags, positionals } = parseArgs(argv, {
23
+ flags: {
24
+ project: { type: 'string' },
25
+ scores: { type: 'string' },
26
+ failures: { type: 'string' },
27
+ notes: { type: 'string' },
28
+ list: { type: 'boolean', default: false },
29
+ 'dry-run': { type: 'boolean', default: false },
30
+ },
31
+ deprecated: { game: 'project' },
37
32
  });
38
33
 
39
- if (args.includes("--list")) {
34
+ const projectName = flags.project || getProjectName(argv);
35
+ const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
36
+ const dryRun = flags['dry-run'] || flags.dryRun;
37
+
38
+ if (flags.list) {
40
39
  return listUncurated(GAME_ROOT);
41
40
  }
42
41
 
43
- const [assetId, status, explanation, ...rest] = args;
42
+ const [assetId, status, explanation] = positionals;
44
43
 
45
44
  if (!assetId || !status || !explanation) {
46
- throw new Error("Usage: sdlab curate <asset_id> <approved|rejected|borderline> <explanation> [--scores k:v,...] [--failures f1,f2]");
45
+ throw inputError(
46
+ 'INPUT_MISSING_ARGS',
47
+ 'Usage: sdlab curate <asset_id> <approved|rejected|borderline> <explanation> [--scores k:v,...] [--failures f1,f2] [--dry-run]'
48
+ );
47
49
  }
48
50
 
49
51
  if (!["approved", "rejected", "borderline"].includes(status)) {
50
- throw new Error(`Invalid status: ${status}. Must be approved, rejected, or borderline.`);
52
+ throw inputError(
53
+ 'INPUT_BAD_STATUS',
54
+ `Invalid status: ${status}. Must be approved, rejected, or borderline.`
55
+ );
51
56
  }
52
57
 
53
- // Parse optional flags
54
- const scoresStr = getFlagValue(rest, "scores");
55
- const failuresStr = getFlagValue(rest, "failures");
56
- const notesStr = getFlagValue(rest, "notes");
58
+ const scoresStr = flags.scores;
59
+ const failuresStr = flags.failures;
60
+ const notesStr = flags.notes;
57
61
 
58
62
  const criteria_scores = {};
59
63
  if (scoresStr) {
60
64
  for (const pair of scoresStr.split(",")) {
61
65
  const [k, v] = pair.split(":");
62
- if (k && v) criteria_scores[k.trim()] = parseFloat(v);
66
+ if (k && v) {
67
+ const score = parseFloat(v);
68
+ if (!Number.isFinite(score)) {
69
+ throw inputError(
70
+ 'INPUT_BAD_SCORE',
71
+ `Score value for "${k.trim()}" is not a number: "${v}"`,
72
+ 'Use the form --scores silhouette:0.9,palette:0.8'
73
+ );
74
+ }
75
+ criteria_scores[k.trim()] = score;
76
+ }
63
77
  }
64
78
  }
65
79
 
@@ -69,17 +83,44 @@ export async function run(argv = process.argv.slice(2)) {
69
83
  const recordPath = join(GAME_ROOT, `records/${assetId}.json`);
70
84
  let record;
71
85
  try {
72
- record = JSON.parse(await readFile(recordPath, "utf-8"));
73
- } catch {
74
- throw new Error(`Record not found: ${recordPath}`);
86
+ record = await readJsonFile(recordPath);
87
+ } catch (err) {
88
+ if (err && err.code === 'INPUT_FILE_NOT_FOUND') {
89
+ throw inputError('INPUT_UNKNOWN_RECORD', `Record not found: ${recordPath}`);
90
+ }
91
+ throw err;
75
92
  }
76
93
 
77
- // Update record first (write before move to avoid orphaned images)
78
- const oldPath = join(GAME_ROOT, record.asset_path);
94
+ const originalAssetPath = record.asset_path;
95
+ const oldPath = join(GAME_ROOT, originalAssetPath);
79
96
  const newDir = `outputs/${status}`;
80
97
  const newPath = `${newDir}/${assetId}.png`;
98
+ const newFullPath = join(GAME_ROOT, newPath);
99
+
100
+ if (dryRun) {
101
+ console.log(`\x1b[33m(dry-run)\x1b[0m ${assetId}`);
102
+ console.log(` status: ${status}`);
103
+ console.log(` move: ${originalAssetPath} -> ${newPath}`);
104
+ console.log(` note: ${explanation}`);
105
+ if (failure_modes.length) console.log(` failures: ${failure_modes.join(", ")}`);
106
+ return;
107
+ }
108
+
81
109
  await mkdir(join(GAME_ROOT, newDir), { recursive: true });
82
110
 
111
+ // Move image FIRST — if the rename fails we still have a clean record.
112
+ try {
113
+ await rename(oldPath, newFullPath);
114
+ } catch (err) {
115
+ throw runtimeError(
116
+ 'RUNTIME_MOVE_FAILED',
117
+ `Could not move ${oldPath} to ${newPath}: ${err.message}`,
118
+ 'Check that the source image exists and the destination is writable.',
119
+ err
120
+ );
121
+ }
122
+
123
+ // Now safely update the record. If writing fails, try to restore the image.
83
124
  record.asset_path = newPath;
84
125
  record.judgment = {
85
126
  status,
@@ -92,16 +133,23 @@ export async function run(argv = process.argv.slice(2)) {
92
133
  confidence: 0.9,
93
134
  };
94
135
 
95
- await writeFile(recordPath, JSON.stringify(record, null, 2));
96
-
97
- // Move image file
98
136
  try {
99
- await rename(oldPath, join(GAME_ROOT, newPath));
100
- } catch {
101
- throw new Error(`Could not move ${oldPath} to ${newPath}`);
137
+ await writeFile(recordPath, JSON.stringify(record, null, 2));
138
+ } catch (err) {
139
+ // Attempt rollback: put the image back.
140
+ try { await rename(newFullPath, oldPath); } catch {}
141
+ throw runtimeError(
142
+ 'RUNTIME_WRITE_FAILED',
143
+ `Could not write record ${recordPath}: ${err.message}`,
144
+ null,
145
+ err
146
+ );
102
147
  }
103
148
 
104
- console.log(`\x1b[32m✓\x1b[0m ${assetId} → ${status}`);
149
+ // Final artifact path — always shown, even under --quiet.
150
+ result('curate', `${assetId} → ${status}`);
151
+ result(` image: ${newPath}`);
152
+ result(` record: records/${assetId}.json`);
105
153
  console.log(` ${explanation}`);
106
154
  if (failure_modes.length) console.log(` Failures: ${failure_modes.join(", ")}`);
107
155
  }
@@ -135,8 +183,5 @@ async function listUncurated(GAME_ROOT) {
135
183
 
136
184
  // Direct execution guard
137
185
  if (process.argv[1] && (process.argv[1].endsWith('curate.js') || process.argv[1].endsWith('curate'))) {
138
- run().catch((err) => {
139
- console.error(err.message || err);
140
- process.exit(1);
141
- });
186
+ run().catch(handleCliError);
142
187
  }
package/scripts/doctor.js CHANGED
@@ -15,6 +15,7 @@ import { readFileSync, readdirSync, existsSync } from 'node:fs';
15
15
  import { join } from 'node:path';
16
16
  import { getProjectName } from '../lib/args.js';
17
17
  import { REPO_ROOT } from '../lib/paths.js';
18
+ import { runtimeError, handleCliError } from '../lib/errors.js';
18
19
 
19
20
  const REQUIRED_CONFIG_FILES = [
20
21
  'project.json',
@@ -484,14 +485,11 @@ export async function run(argv = process.argv.slice(2)) {
484
485
  console.log(`═══ ${status} — ${passes} passed, ${failures} failed, ${warnings} warnings ═══`);
485
486
 
486
487
  if (failures > 0) {
487
- throw new Error(`Project "${projectName}" has ${failures} issue(s).`);
488
+ throw runtimeError('PROJECT_UNHEALTHY', `Project "${projectName}" has ${failures} issue(s).`);
488
489
  }
489
490
  }
490
491
 
491
492
  // Direct execution guard
492
493
  if (process.argv[1] && (process.argv[1].endsWith('doctor.js') || process.argv[1].endsWith('doctor'))) {
493
- run().catch((err) => {
494
- console.error(err.message || err);
495
- process.exit(1);
496
- });
494
+ run().catch(handleCliError);
497
495
  }
@@ -4,36 +4,44 @@
4
4
  * eligibility.js — Audit training/eval eligibility for all project records.
5
5
  *
6
6
  * Usage:
7
- * sdlab eligibility audit [--project <name>]
7
+ * sdlab eligibility audit [--project <name>] [--profile <id>]
8
8
  */
9
9
 
10
10
  import { readdir, readFile } from 'node:fs/promises';
11
11
  import { join } from 'node:path';
12
- import { getProjectName } from '../lib/args.js';
12
+ import { parseArgs, getProjectName } from '../lib/args.js';
13
13
  import { REPO_ROOT } from '../lib/paths.js';
14
14
  import { loadProjectConfig, loadSelectionProfile } from '../lib/config.js';
15
+ import { handleCliError } from '../lib/errors.js';
15
16
  import { evaluateEligibility, categorizeExclusions } from '../lib/eligibility.js';
16
17
 
17
18
  export async function run(argv = process.argv.slice(2)) {
18
- const projectName = getProjectName(argv);
19
+ const { flags } = parseArgs(argv, {
20
+ flags: {
21
+ project: { type: 'string' },
22
+ profile: { type: 'string' },
23
+ },
24
+ deprecated: { game: 'project' },
25
+ allowUnknown: true,
26
+ });
27
+
28
+ const projectName = flags.project || getProjectName(argv);
19
29
  const projectRoot = join(REPO_ROOT, 'projects', projectName);
20
30
 
21
- const profileIdx = argv.indexOf('--profile');
22
- const profileId = profileIdx >= 0 ? argv[profileIdx + 1] : null;
23
- const profile = loadSelectionProfile(projectRoot, profileId);
31
+ const profile = loadSelectionProfile(projectRoot, flags.profile);
24
32
  const config = loadProjectConfig(projectRoot);
25
33
 
26
34
  const recordsDir = join(projectRoot, 'records');
27
35
  const files = (await readdir(recordsDir)).filter(f => f.endsWith('.json')).sort();
28
36
 
29
37
  console.log(`\x1b[1msdlab eligibility audit\x1b[0m — ${projectName}`);
30
- console.log(` Profile: ${profileId || 'training-default (built-in)'}`);
38
+ console.log(` Profile: ${flags.profile || 'training-default (built-in)'}`);
31
39
  console.log(` Records: ${files.length}`);
32
40
  console.log('');
33
41
 
34
42
  const eligible = [];
35
43
  const excluded = [];
36
- const nearMiss = []; // Records with exactly 1 failing reason
44
+ const nearMiss = [];
37
45
 
38
46
  for (const file of files) {
39
47
  const raw = await readFile(join(recordsDir, file), 'utf-8');
@@ -55,7 +63,6 @@ export async function run(argv = process.argv.slice(2)) {
55
63
  console.log(` \x1b[31m✗\x1b[0m Excluded: ${excluded.length}`);
56
64
  console.log('');
57
65
 
58
- // Categorize exclusions
59
66
  const categories = categorizeExclusions(excluded);
60
67
  console.log(' Exclusion breakdown:');
61
68
  for (const [cat, count] of Object.entries(categories)) {
@@ -65,7 +72,6 @@ export async function run(argv = process.argv.slice(2)) {
65
72
  }
66
73
  }
67
74
 
68
- // Near-miss improvement opportunities
69
75
  if (nearMiss.length > 0) {
70
76
  console.log(`\n Improvement opportunities (${nearMiss.length} records with 1 failing check):`);
71
77
  const byReason = {};
@@ -84,8 +90,5 @@ export async function run(argv = process.argv.slice(2)) {
84
90
 
85
91
  // Direct execution guard
86
92
  if (process.argv[1] && (process.argv[1].endsWith('eligibility.js') || process.argv[1].endsWith('eligibility'))) {
87
- run().catch((err) => {
88
- console.error(err.message || err);
89
- process.exit(1);
90
- });
93
+ run().catch(handleCliError);
91
94
  }
@@ -11,15 +11,20 @@
11
11
 
12
12
  import { readFile } from 'node:fs/promises';
13
13
  import { join } from 'node:path';
14
- import { getProjectName } from '../lib/args.js';
14
+ import { parseArgs, getProjectName } from '../lib/args.js';
15
15
  import { REPO_ROOT } from '../lib/paths.js';
16
+ import { inputError, handleCliError } from '../lib/errors.js';
16
17
  import { buildEvalPack, listEvalPacks, loadEvalPack } from '../lib/eval-pack.js';
17
18
 
18
19
  export async function run(argv = process.argv.slice(2)) {
19
- const projectName = getProjectName(argv);
20
+ const { flags, positionals } = parseArgs(argv, {
21
+ flags: { project: { type: 'string' } },
22
+ deprecated: { game: 'project' },
23
+ });
24
+
25
+ const projectName = flags.project || getProjectName(argv);
20
26
  const projectRoot = join(REPO_ROOT, 'projects', projectName);
21
- const subcommand = argv.find(a => !a.startsWith('--')) || 'list';
22
- const args = argv.filter(a => a !== subcommand);
27
+ const subcommand = positionals[0] || 'list';
23
28
 
24
29
  if (subcommand === 'build') {
25
30
  console.log(`\x1b[1msdlab eval-pack build\x1b[0m — ${projectName}\n`);
@@ -48,8 +53,8 @@ export async function run(argv = process.argv.slice(2)) {
48
53
  }
49
54
 
50
55
  } else if (subcommand === 'show') {
51
- const evalId = args.find(a => a.startsWith('eval-'));
52
- if (!evalId) throw new Error('Usage: sdlab eval-pack show <eval-id>');
56
+ const evalId = positionals.find(a => a.startsWith('eval-'));
57
+ if (!evalId) throw inputError('INPUT_MISSING_ARGS', 'Usage: sdlab eval-pack show <eval-id>');
53
58
 
54
59
  const manifest = await loadEvalPack(projectRoot, evalId);
55
60
  console.log(`\x1b[1mEval pack\x1b[0m — ${evalId}\n`);
@@ -80,13 +85,10 @@ export async function run(argv = process.argv.slice(2)) {
80
85
  }
81
86
 
82
87
  } else {
83
- throw new Error(`Unknown subcommand: ${subcommand}. Use: build, list, show`);
88
+ throw inputError('INPUT_BAD_SUBCOMMAND', `Unknown subcommand: ${subcommand}. Use: build, list, show`);
84
89
  }
85
90
  }
86
91
 
87
92
  if (process.argv[1] && (process.argv[1].endsWith('eval-pack.js') || process.argv[1].endsWith('eval-pack'))) {
88
- run().catch((err) => {
89
- console.error(err.message || err);
90
- process.exit(1);
91
- });
93
+ run().catch(handleCliError);
92
94
  }