@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,176 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * batch-generate.js — Execute a batch production mode.
5
+ *
6
+ * Usage:
7
+ * sdlab batch generate --mode expression-sheet --subject captain-orryn --project star-freight
8
+ * sdlab batch generate --mode environment-board --theme freeport-night --project star-freight
9
+ * sdlab batch generate --mode silhouette-pack --project star-freight --dry-run
10
+ */
11
+
12
+ import { parseArgs, getProjectName } from '../lib/args.js';
13
+ import { getProjectRoot, getRunsDir } from '../lib/paths.js';
14
+ import { compileBatch } from '../lib/batch-compiler.js';
15
+ import { getBatchMode } from '../lib/batch-modes.js';
16
+ import {
17
+ createBatchDir,
18
+ executeBatchRuns,
19
+ saveBatchManifest,
20
+ } from '../lib/batch-runs.js';
21
+ import { renderSheetHTML, saveSheet } from '../lib/batch-sheet-render.js';
22
+ import { info, result } from '../lib/log.js';
23
+ import { join } from 'node:path';
24
+ import { writeFile } from 'node:fs/promises';
25
+
26
+ export async function run(argv = process.argv.slice(2)) {
27
+ const { flags } = parseArgs(argv, {
28
+ flags: {
29
+ project: { type: 'string' },
30
+ mode: { type: 'string' },
31
+ subject: { type: 'string' },
32
+ theme: { type: 'string' },
33
+ asset: { type: 'string' },
34
+ 'dry-run': { type: 'boolean' },
35
+ json: { type: 'boolean' },
36
+ },
37
+ deprecated: { game: 'project' },
38
+ });
39
+
40
+ if (!flags.mode) {
41
+ console.log('Usage: sdlab batch generate --mode <id> [--subject <id>] [--theme <label>] [--project <name>]');
42
+ console.log('');
43
+ console.log('Options:');
44
+ console.log(' --mode <id> Batch mode (required)');
45
+ console.log(' --subject <id> Subject for subject-driven modes');
46
+ console.log(' --theme <label> Theme label for environment modes');
47
+ console.log(' --asset <id> Training asset reference');
48
+ console.log(' --dry-run Prepare batch without submitting to ComfyUI');
49
+ console.log(' --json Output manifest as JSON');
50
+ return;
51
+ }
52
+
53
+ const projectName = flags.project || getProjectName(argv);
54
+ const projectRoot = getProjectRoot(projectName);
55
+ const dryRun = flags['dry-run'] || false;
56
+
57
+ // Load mode for display
58
+ const mode = getBatchMode(projectRoot, flags.mode);
59
+
60
+ console.log(`\x1b[1mstyle-dataset-lab\x1b[0m batch generate`);
61
+ console.log(` Project: ${projectName}`);
62
+ console.log(` Mode: ${mode.label} (${mode.mode_id})`);
63
+ console.log(` Type: ${mode.batch_type}`);
64
+ console.log(` Slots: ${mode.variant_plan.length}`);
65
+ if (flags.subject) console.log(` Subject: ${flags.subject}`);
66
+ if (flags.theme) console.log(` Theme: ${flags.theme}`);
67
+ if (dryRun) console.log(' ⚠ DRY RUN');
68
+ console.log('');
69
+
70
+ // 1. Compile batch briefs
71
+ const { baseBrief, slotBriefs } = await compileBatch({
72
+ projectRoot,
73
+ projectId: projectName,
74
+ modeId: flags.mode,
75
+ subjectId: flags.subject,
76
+ theme: flags.theme,
77
+ assetRef: flags.asset,
78
+ });
79
+
80
+ // 2. Create batch directory
81
+ const { batchId, batchDir } = await createBatchDir(projectRoot);
82
+ info('batch-generate', `Batch: ${batchId}`);
83
+
84
+ // 3. Execute runs
85
+ const batchStartMs = Date.now();
86
+ const results = await executeBatchRuns({
87
+ projectRoot,
88
+ projectName,
89
+ batchDir,
90
+ mode,
91
+ slotBriefs,
92
+ dryRun,
93
+ });
94
+ const batchElapsedMs = Date.now() - batchStartMs;
95
+ if (!dryRun && results.length > 0) {
96
+ const avgSec = Math.round(batchElapsedMs / results.length / 1000);
97
+ const totalSec = Math.round(batchElapsedMs / 1000);
98
+ info('batch-generate', `Completed ${results.length} slot(s) in ${totalSec}s (avg ${avgSec}s/slot)`);
99
+ }
100
+
101
+ // 4. Build manifest
102
+ const manifest = {
103
+ batch_id: batchId,
104
+ project_id: projectName,
105
+ mode_id: mode.mode_id,
106
+ created_at: new Date().toISOString(),
107
+ brief_ids: results.map(r => r.brief_id),
108
+ run_ids: results.map(r => r.run_id),
109
+ slots: results.map(r => ({
110
+ slot_id: r.slot_id,
111
+ label: r.label,
112
+ brief_id: r.brief_id,
113
+ run_id: r.run_id,
114
+ selected_output: r.selected_output,
115
+ })),
116
+ sheet_paths: [],
117
+ summary: {
118
+ total_slots: results.length,
119
+ successful_slots: results.filter(r => r.selected_output).length,
120
+ dry_run: dryRun,
121
+ },
122
+ };
123
+
124
+ if (flags.subject) manifest.subject_id = flags.subject;
125
+ if (flags.theme) manifest.theme = flags.theme;
126
+
127
+ // 5. Render sheet
128
+ const sheetHtml = renderSheetHTML({ manifest, mode, projectRoot, batchDir });
129
+ const sheetPath = await saveSheet(batchDir, 'primary-sheet.html', sheetHtml);
130
+ manifest.sheet_paths.push('sheets/primary-sheet.html');
131
+
132
+ // 6. Save manifest + summary
133
+ await saveBatchManifest(batchDir, manifest);
134
+
135
+ // Write summary.md
136
+ const summaryLines = [
137
+ `# Batch: ${batchId}`,
138
+ '',
139
+ `**Mode:** ${mode.label} (${mode.mode_id})`,
140
+ `**Type:** ${mode.batch_type}`,
141
+ `**Project:** ${projectName}`,
142
+ ];
143
+ if (manifest.subject_id) summaryLines.push(`**Subject:** ${manifest.subject_id}`);
144
+ if (manifest.theme) summaryLines.push(`**Theme:** ${manifest.theme}`);
145
+ summaryLines.push(`**Created:** ${manifest.created_at}`);
146
+ if (dryRun) summaryLines.push('**⚠ DRY RUN**');
147
+ summaryLines.push('');
148
+ summaryLines.push('## Slots');
149
+ summaryLines.push('');
150
+ summaryLines.push('| Slot | Run | Output |');
151
+ summaryLines.push('|------|-----|--------|');
152
+ for (const r of results) {
153
+ summaryLines.push(`| ${r.label} | ${r.run_id} | ${r.selected_output || '—'} |`);
154
+ }
155
+ summaryLines.push('');
156
+ await writeFile(join(batchDir, 'summary.md'), summaryLines.join('\n'));
157
+
158
+ // 7. Output
159
+ console.log('');
160
+ if (flags.json) {
161
+ console.log(JSON.stringify(manifest, null, 2));
162
+ } else {
163
+ console.log(`Batch: ${batchId}`);
164
+ console.log(`Slots: ${results.length}`);
165
+ console.log('');
166
+ for (const r of results) {
167
+ const status = r.selected_output ? '✓' : '○';
168
+ console.log(` ${status} ${r.label.padEnd(24)} → ${r.run_id} ${r.selected_output || '(dry run)'}`);
169
+ }
170
+ console.log('');
171
+ // Primary artifact paths — always print (survives --quiet) so pipelines
172
+ // can parse the output location.
173
+ result(`Sheet: batches/${batchId}/sheets/primary-sheet.html`);
174
+ result(`Manifest: batches/${batchId}/manifest.json`);
175
+ }
176
+ }
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * batch-sheet.js — Re-render a batch sheet from saved manifest.
5
+ *
6
+ * Usage:
7
+ * sdlab batch sheet batch_2025-06-30_001 --project star-freight
8
+ */
9
+
10
+ import { parseArgs, getProjectName } from '../lib/args.js';
11
+ import { getProjectRoot } from '../lib/paths.js';
12
+ import { loadBatchManifest } from '../lib/batch-runs.js';
13
+ import { getBatchMode } from '../lib/batch-modes.js';
14
+ import { renderSheetHTML, saveSheet } from '../lib/batch-sheet-render.js';
15
+ import { getBatchesDir } from '../lib/batch-runs.js';
16
+ import { join } from 'node:path';
17
+ import { inputError } from '../lib/errors.js';
18
+ import { info } from '../lib/log.js';
19
+
20
+ export async function run(argv = process.argv.slice(2)) {
21
+ const { flags, positionals } = parseArgs(argv, {
22
+ flags: {
23
+ project: { type: 'string' },
24
+ batch: { type: 'string' },
25
+ },
26
+ deprecated: { game: 'project' },
27
+ });
28
+
29
+ const projectName = flags.project || getProjectName(argv);
30
+ const projectRoot = getProjectRoot(projectName);
31
+ const batchId = positionals[0] || flags.batch;
32
+
33
+ if (!batchId) {
34
+ console.log('Usage: sdlab batch sheet <batch-id> [--project <name>]');
35
+ return;
36
+ }
37
+
38
+ const manifest = loadBatchManifest(projectRoot, batchId);
39
+ const mode = getBatchMode(projectRoot, manifest.mode_id);
40
+ const batchDir = join(getBatchesDir(projectRoot), batchId);
41
+
42
+ const html = renderSheetHTML({ manifest, mode, projectRoot, batchDir });
43
+ const sheetPath = await saveSheet(batchDir, 'primary-sheet.html', html);
44
+
45
+ info(`Sheet written: ${sheetPath}`);
46
+ }
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * batch-show.js — List batches or show batch details.
5
+ *
6
+ * Usage:
7
+ * sdlab batch show --project star-freight # list all batches
8
+ * sdlab batch show batch_2025-06-30_001 # show specific batch
9
+ * sdlab batch show batch_2025-06-30_001 --json # as JSON
10
+ */
11
+
12
+ import { parseArgs, getProjectName } from '../lib/args.js';
13
+ import { getProjectRoot } from '../lib/paths.js';
14
+ import { listBatches, loadBatchManifest } from '../lib/batch-runs.js';
15
+
16
+ export async function run(argv = process.argv.slice(2)) {
17
+ const { flags, positionals } = parseArgs(argv, {
18
+ flags: {
19
+ project: { type: 'string' },
20
+ batch: { type: 'string' },
21
+ json: { type: 'boolean' },
22
+ },
23
+ deprecated: { game: 'project' },
24
+ });
25
+
26
+ const projectName = flags.project || getProjectName(argv);
27
+ const projectRoot = getProjectRoot(projectName);
28
+ const batchId = positionals[0] || flags.batch;
29
+
30
+ // List mode
31
+ if (!batchId) {
32
+ const batches = listBatches(projectRoot);
33
+ if (batches.length === 0) {
34
+ console.log('No batches found.');
35
+ return;
36
+ }
37
+ console.log(`\x1b[1mstyle-dataset-lab\x1b[0m batches (${projectName})`);
38
+ console.log('');
39
+ for (const id of batches) {
40
+ try {
41
+ const manifest = loadBatchManifest(projectRoot, id);
42
+ const mode = manifest.mode_id || '?';
43
+ const slots = manifest.summary?.total_slots || manifest.slots?.length || 0;
44
+ const ok = manifest.summary?.successful_slots ?? '?';
45
+ const dry = manifest.summary?.dry_run ? ' [dry]' : '';
46
+ const subject = manifest.subject_id ? ` (${manifest.subject_id})` : '';
47
+ console.log(` ${id} ${mode.padEnd(24)} ${ok}/${slots} slots${dry}${subject}`);
48
+ } catch {
49
+ console.log(` ${id} (manifest unreadable)`);
50
+ }
51
+ }
52
+ console.log('');
53
+ return;
54
+ }
55
+
56
+ // Detail mode
57
+ const manifest = loadBatchManifest(projectRoot, batchId);
58
+
59
+ if (flags.json) {
60
+ console.log(JSON.stringify(manifest, null, 2));
61
+ return;
62
+ }
63
+
64
+ console.log(`\x1b[1mstyle-dataset-lab\x1b[0m batch ${batchId}`);
65
+ console.log('');
66
+ console.log(` Mode: ${manifest.mode_id}`);
67
+ console.log(` Project: ${manifest.project_id}`);
68
+ if (manifest.subject_id) console.log(` Subject: ${manifest.subject_id}`);
69
+ if (manifest.theme) console.log(` Theme: ${manifest.theme}`);
70
+ console.log(` Created: ${manifest.created_at}`);
71
+ if (manifest.summary?.dry_run) console.log(' ⚠ DRY RUN');
72
+ console.log('');
73
+ console.log(' Slots:');
74
+ for (const slot of manifest.slots || []) {
75
+ const status = slot.selected_output ? '✓' : '○';
76
+ console.log(` ${status} ${(slot.label || slot.slot_id).padEnd(24)} run=${slot.run_id} ${slot.selected_output || ''}`);
77
+ }
78
+ console.log('');
79
+ if (manifest.sheet_paths?.length) {
80
+ console.log(' Sheets:');
81
+ for (const p of manifest.sheet_paths) {
82
+ console.log(` ${p}`);
83
+ }
84
+ console.log('');
85
+ }
86
+ }
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * brief-compile.js — Compile a generation brief from project truth.
5
+ *
6
+ * Usage:
7
+ * sdlab brief compile --project star-freight --workflow character-portrait-set --subject kael_maren
8
+ * sdlab brief compile --project star-freight --workflow environment-moodboard
9
+ * sdlab brief compile --project star-freight --workflow character-portrait-set --subject kael_maren --reference sf_001,sf_018
10
+ * sdlab brief compile --project star-freight --workflow character-portrait-set --subject kael_maren --output-count 8 --json
11
+ */
12
+
13
+ import { parseArgs, getProjectName, parseNumberFlag } from '../lib/args.js';
14
+ import { getProjectRoot } from '../lib/paths.js';
15
+ import { compileBrief, saveCompiledBrief } from '../lib/brief-compiler.js';
16
+ import { info } from '../lib/log.js';
17
+
18
+ export async function run(argv = process.argv.slice(2)) {
19
+ const { flags } = parseArgs(argv, {
20
+ flags: {
21
+ project: { type: 'string' },
22
+ workflow: { type: 'string' },
23
+ subject: { type: 'string' },
24
+ asset: { type: 'string' },
25
+ 'implementation-pack': { type: 'string' },
26
+ reference: { type: 'string' },
27
+ 'output-count': { type: 'string' },
28
+ width: { type: 'string' },
29
+ height: { type: 'string' },
30
+ steps: { type: 'string' },
31
+ cfg: { type: 'string' },
32
+ sampler: { type: 'string' },
33
+ 'seed-mode': { type: 'string' },
34
+ json: { type: 'boolean' },
35
+ 'dry-run': { type: 'boolean' },
36
+ },
37
+ deprecated: { game: 'project' },
38
+ });
39
+
40
+ if (!flags.workflow) {
41
+ console.log('Usage: sdlab brief compile --workflow <id> [--subject <id>] [--project <name>]');
42
+ console.log('');
43
+ console.log('Options:');
44
+ console.log(' --workflow <id> Workflow profile to use (required)');
45
+ console.log(' --subject <id> Subject ID for subject-driven workflows');
46
+ console.log(' --asset <id> Training asset reference');
47
+ console.log(' --implementation-pack <id> Implementation pack reference');
48
+ console.log(' --reference <ids> Comma-separated reference record IDs');
49
+ console.log(' --output-count <n> Override output count');
50
+ console.log(' --width <px> Override width');
51
+ console.log(' --height <px> Override height');
52
+ console.log(' --steps <n> Override steps');
53
+ console.log(' --cfg <n> Override CFG scale');
54
+ console.log(' --sampler <name> Override sampler');
55
+ console.log(' --seed-mode <mode> Override seed mode (fixed|increment|random)');
56
+ console.log(' --json Output raw JSON');
57
+ console.log(' --dry-run Preview without saving');
58
+ return;
59
+ }
60
+
61
+ const projectName = flags.project || getProjectName(argv);
62
+ const projectRoot = getProjectRoot(projectName);
63
+
64
+ // Parse reference IDs
65
+ const referenceIds = flags.reference
66
+ ? flags.reference.split(',').map(s => s.trim()).filter(Boolean)
67
+ : undefined;
68
+
69
+ // Parse overrides
70
+ const overrides = {};
71
+ if (flags['output-count']) overrides.output_count = parseNumberFlag('output-count', flags['output-count'], { int: true, min: 1 });
72
+ if (flags.width) overrides.width = parseNumberFlag('width', flags.width, { int: true, min: 1 });
73
+ if (flags.height) overrides.height = parseNumberFlag('height', flags.height, { int: true, min: 1 });
74
+ if (flags.steps) overrides.steps = parseNumberFlag('steps', flags.steps, { int: true, min: 1 });
75
+ if (flags.cfg) overrides.cfg = parseNumberFlag('cfg', flags.cfg, { min: 0 });
76
+ if (flags.sampler) overrides.sampler = flags.sampler;
77
+ if (flags['seed-mode']) overrides.seed_mode = flags['seed-mode'];
78
+
79
+ const brief = await compileBrief({
80
+ projectRoot,
81
+ projectId: projectName,
82
+ workflowId: flags.workflow,
83
+ subjectId: flags.subject,
84
+ assetRef: flags.asset,
85
+ implementationPackRef: flags['implementation-pack'],
86
+ referenceIds,
87
+ overrides,
88
+ });
89
+
90
+ if (flags.json) {
91
+ console.log(JSON.stringify(brief, null, 2));
92
+ if (!flags['dry-run']) {
93
+ await saveCompiledBrief(projectRoot, brief);
94
+ }
95
+ return;
96
+ }
97
+
98
+ if (!flags['dry-run']) {
99
+ const { jsonPath, mdPath } = await saveCompiledBrief(projectRoot, brief);
100
+ info(`\x1b[1msdlab brief compile\x1b[0m — ${projectName}\n`);
101
+ info(` Brief ID: ${brief.brief_id}`);
102
+ info(` Workflow: ${brief.workflow_id}`);
103
+ info(` Lane: ${brief.lane_id}`);
104
+ if (brief.subject_id) info(` Subject: ${brief.subject_id}`);
105
+ info(` Outputs: ${brief.expected_outputs.output_count}× ${brief.expected_outputs.output_mode}`);
106
+ info('');
107
+ info(` Saved: ${jsonPath}`);
108
+ info(` ${mdPath}`);
109
+ } else {
110
+ info(`\x1b[1msdlab brief compile\x1b[0m — dry run\n`);
111
+ info(` Brief ID: ${brief.brief_id}`);
112
+ info(` Workflow: ${brief.workflow_id}`);
113
+ info(` Lane: ${brief.lane_id}`);
114
+ if (brief.subject_id) info(` Subject: ${brief.subject_id}`);
115
+ info(` Outputs: ${brief.expected_outputs.output_count}× ${brief.expected_outputs.output_mode}`);
116
+ info('');
117
+ info(' Prompt:');
118
+ info(` ${brief.prompt}`);
119
+ info('');
120
+ info(' Negative:');
121
+ info(` ${brief.negative_prompt}`);
122
+ info('');
123
+ if (brief.drift_warnings?.length > 0) {
124
+ info(' Drift warnings:');
125
+ for (const w of brief.drift_warnings) {
126
+ info(` \x1b[33m⚠\x1b[0m ${w}`);
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ if (process.argv[1] && (process.argv[1].endsWith('brief-compile.js') || process.argv[1].endsWith('brief-compile'))) {
133
+ run().catch((err) => { console.error(err.message || err); process.exit(1); });
134
+ }
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * brief-show.js — Show a compiled brief.
5
+ *
6
+ * Usage:
7
+ * sdlab brief show brief_2026-04-15_001 --project star-freight
8
+ * sdlab brief show brief_2026-04-15_001 --json
9
+ * sdlab brief show brief_2026-04-15_001 --md
10
+ */
11
+
12
+ import { parseArgs, getProjectName } from '../lib/args.js';
13
+ import { getProjectRoot } from '../lib/paths.js';
14
+ import { loadCompiledBrief } from '../lib/brief-compiler.js';
15
+ import { renderBriefText, renderBriefMarkdown } from '../lib/brief-render.js';
16
+
17
+ export async function run(argv = process.argv.slice(2)) {
18
+ const { flags, positionals } = parseArgs(argv, {
19
+ flags: {
20
+ project: { type: 'string' },
21
+ json: { type: 'boolean' },
22
+ md: { type: 'boolean' },
23
+ },
24
+ positionals: ['briefId'],
25
+ deprecated: { game: 'project' },
26
+ });
27
+
28
+ const briefId = positionals[0];
29
+ if (!briefId) {
30
+ console.log('Usage: sdlab brief show <brief-id> [--project <name>] [--json] [--md]');
31
+ return;
32
+ }
33
+
34
+ const projectName = flags.project || getProjectName(argv);
35
+ const projectRoot = getProjectRoot(projectName);
36
+ const brief = await loadCompiledBrief(projectRoot, briefId);
37
+
38
+ if (flags.json) {
39
+ console.log(JSON.stringify(brief, null, 2));
40
+ return;
41
+ }
42
+
43
+ if (flags.md) {
44
+ console.log(renderBriefMarkdown(brief));
45
+ return;
46
+ }
47
+
48
+ console.log(renderBriefText(brief));
49
+ }
50
+
51
+ if (process.argv[1] && (process.argv[1].endsWith('brief-show.js') || process.argv[1].endsWith('brief-show'))) {
52
+ run().catch((err) => { console.error(err.message || err); process.exit(1); });
53
+ }
@@ -21,6 +21,7 @@ import { join } from 'path';
21
21
  import { getProjectName } from '../lib/args.js';
22
22
  import { REPO_ROOT } from '../lib/paths.js';
23
23
  import { loadConstitution, loadLanes, loadRubric, loadTerminology, detectLane, detectGroup } from '../lib/config.js';
24
+ import { inputError, handleCliError } from '../lib/errors.js';
24
25
 
25
26
  /**
26
27
  * Build rationale string from rule's template, interpolating variables.
@@ -162,10 +163,10 @@ export async function run(argv = process.argv.slice(2)) {
162
163
 
163
164
  // Load project config — fail loudly if missing
164
165
  if (!existsSync(join(PROJECT_ROOT, 'constitution.json'))) {
165
- throw new Error(
166
- `No constitution.json found in ${PROJECT_ROOT}.\n` +
167
- `Canon binding requires per-project config files.\n` +
168
- `Run: sdlab init ${projectName} --domain <domain>`
166
+ throw inputError(
167
+ 'INPUT_MISSING_CONFIG',
168
+ `No constitution.json found in ${PROJECT_ROOT}.`,
169
+ `Canon binding requires per-project config files. Run: sdlab init ${projectName} --domain <domain>`
169
170
  );
170
171
  }
171
172
 
@@ -175,7 +176,7 @@ export async function run(argv = process.argv.slice(2)) {
175
176
  const termConfig = loadTerminology(PROJECT_ROOT);
176
177
 
177
178
  if (!constitutionConfig.rules || constitutionConfig.rules.length === 0) {
178
- throw new Error(`constitution.json has no rules — cannot bind.`);
179
+ throw inputError('INPUT_EMPTY_CONFIG', `constitution.json has no rules — cannot bind.`);
179
180
  }
180
181
 
181
182
  const constitutionRules = constitutionConfig.rules;
@@ -280,8 +281,5 @@ export async function run(argv = process.argv.slice(2)) {
280
281
 
281
282
  // Direct execution guard
282
283
  if (process.argv[1] && (process.argv[1].endsWith('canon-bind.js') || process.argv[1].endsWith('canon-bind'))) {
283
- run().catch((err) => {
284
- console.error(err.message || err);
285
- process.exit(1);
286
- });
284
+ run().catch(handleCliError);
287
285
  }
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
  }