@mcptoolshop/style-dataset-lab 2.0.0 → 2.2.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.
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * eval-pack.js — Build and inspect canon-aware eval packs.
5
+ *
6
+ * Usage:
7
+ * sdlab eval-pack build [--project <name>]
8
+ * sdlab eval-pack list [--project <name>]
9
+ * sdlab eval-pack show <eval-id> [--project <name>]
10
+ */
11
+
12
+ import { readFile } from 'node:fs/promises';
13
+ import { join } from 'node:path';
14
+ import { getProjectName } from '../lib/args.js';
15
+ import { REPO_ROOT } from '../lib/paths.js';
16
+ import { buildEvalPack, listEvalPacks, loadEvalPack } from '../lib/eval-pack.js';
17
+
18
+ export async function run(argv = process.argv.slice(2)) {
19
+ const projectName = getProjectName(argv);
20
+ const projectRoot = join(REPO_ROOT, 'projects', projectName);
21
+ const subcommand = argv.find(a => !a.startsWith('--')) || 'list';
22
+ const args = argv.filter(a => a !== subcommand);
23
+
24
+ if (subcommand === 'build') {
25
+ console.log(`\x1b[1msdlab eval-pack build\x1b[0m — ${projectName}\n`);
26
+
27
+ const result = await buildEvalPack(projectRoot);
28
+
29
+ console.log(` \x1b[32m✓\x1b[0m Eval pack: ${result.evalId}`);
30
+ console.log('');
31
+ console.log(' Tasks:');
32
+ for (const [task, count] of Object.entries(result.tasks)) {
33
+ console.log(` ${task.padEnd(24)} ${count} records`);
34
+ }
35
+ const total = Object.values(result.tasks).reduce((s, n) => s + n, 0);
36
+ console.log(` ${'─'.repeat(36)}`);
37
+ console.log(` ${'total'.padEnd(24)} ${total} records`);
38
+
39
+ } else if (subcommand === 'list') {
40
+ const packs = await listEvalPacks(projectRoot);
41
+ if (packs.length === 0) {
42
+ console.log('No eval packs found. Run: sdlab eval-pack build');
43
+ return;
44
+ }
45
+ console.log(`\x1b[1mEval packs\x1b[0m — ${projectName}\n`);
46
+ for (const p of packs) {
47
+ console.log(` ${p.id} total=${p.total} lane=${p.tasks.lane_coverage} drift=${p.tasks.forbidden_drift} gold=${p.tasks.anchor_gold} cont=${p.tasks.subject_continuity} ${p.created_at}`);
48
+ }
49
+
50
+ } 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>');
53
+
54
+ const manifest = await loadEvalPack(projectRoot, evalId);
55
+ console.log(`\x1b[1mEval pack\x1b[0m — ${evalId}\n`);
56
+ console.log(` Created: ${manifest.created_at}`);
57
+ console.log(` Project: ${manifest.project}`);
58
+ console.log('');
59
+ console.log(' Tasks:');
60
+ for (const [task, count] of Object.entries(manifest.task_counts)) {
61
+ console.log(` ${task.padEnd(24)} ${count} records`);
62
+ }
63
+ console.log(`\n Total: ${manifest.total_records} records`);
64
+
65
+ // Show sample from each task file
66
+ const evalDir = join(projectRoot, 'eval-packs', evalId);
67
+ for (const taskFile of ['lane-coverage', 'forbidden-drift', 'anchor-gold', 'subject-continuity']) {
68
+ const raw = await readFile(join(evalDir, `${taskFile}.jsonl`), 'utf-8');
69
+ const entries = raw.trim().split('\n').filter(Boolean).map(l => JSON.parse(l));
70
+ if (entries.length > 0) {
71
+ console.log(`\n ${taskFile} (first 3):`);
72
+ for (const entry of entries.slice(0, 3)) {
73
+ const summary = entry.record_id || entry.subject_name;
74
+ const detail = entry.pass_ratio != null ? `ratio=${entry.pass_ratio}` :
75
+ entry.violations ? `violations=${entry.violations.length}` :
76
+ `records=${entry.record_count}`;
77
+ console.log(` ${summary} ${detail}`);
78
+ }
79
+ }
80
+ }
81
+
82
+ } else {
83
+ throw new Error(`Unknown subcommand: ${subcommand}. Use: build, list, show`);
84
+ }
85
+ }
86
+
87
+ 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
+ });
92
+ }
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * eval-run.js — Create, score, and inspect eval runs.
5
+ *
6
+ * Usage:
7
+ * sdlab eval-run create --manifest <id> --eval-pack <id> [--project <name>]
8
+ * sdlab eval-run score <eval-run-id> --outputs <path> [--project <name>]
9
+ * sdlab eval-run show <eval-run-id> [--project <name>]
10
+ * sdlab eval-run list [--project <name>]
11
+ */
12
+
13
+ import { readFile } from 'node:fs/promises';
14
+ import { join } from 'node:path';
15
+ import { getProjectName } from '../lib/args.js';
16
+ import { REPO_ROOT } from '../lib/paths.js';
17
+ import { listTrainingManifests } from '../lib/training-manifests.js';
18
+ import { listEvalPacks } from '../lib/eval-pack.js';
19
+ import { createEvalRun, scoreEvalRun, loadEvalRun, listEvalRuns } from '../lib/eval-runs.js';
20
+
21
+ export async function run(argv = process.argv.slice(2)) {
22
+ const projectName = getProjectName(argv);
23
+ const projectRoot = join(REPO_ROOT, 'projects', projectName);
24
+ const subcommand = argv.find(a => !a.startsWith('--') && !a.startsWith('er-')) || 'list';
25
+ const args = argv.filter(a => a !== subcommand);
26
+
27
+ if (subcommand === 'create') {
28
+ // Find manifest
29
+ let manifestId;
30
+ const manifestIdx = args.indexOf('--manifest');
31
+ if (manifestIdx >= 0) {
32
+ manifestId = args[manifestIdx + 1];
33
+ } else {
34
+ const manifests = await listTrainingManifests(projectRoot);
35
+ if (manifests.length === 0) throw new Error('No training manifests. Run: sdlab training-manifest create');
36
+ manifestId = manifests[manifests.length - 1].id;
37
+ }
38
+
39
+ // Find eval pack
40
+ let evalPackId;
41
+ const evalIdx = args.indexOf('--eval-pack');
42
+ if (evalIdx >= 0) {
43
+ evalPackId = args[evalIdx + 1];
44
+ } else {
45
+ const packs = await listEvalPacks(projectRoot);
46
+ if (packs.length === 0) throw new Error('No eval packs. Run: sdlab eval-pack build');
47
+ evalPackId = packs[packs.length - 1].id;
48
+ }
49
+
50
+ console.log(`\x1b[1msdlab eval-run create\x1b[0m — ${projectName}`);
51
+ console.log(` Manifest: ${manifestId}`);
52
+ console.log(` Eval pack: ${evalPackId}`);
53
+ console.log('');
54
+
55
+ const result = await createEvalRun(projectRoot, manifestId, evalPackId);
56
+ console.log(` \x1b[32m✓\x1b[0m Eval run: ${result.evalRunId}`);
57
+ console.log(` Status: created (use "sdlab eval-run score" to score outputs)`);
58
+
59
+ } else if (subcommand === 'score') {
60
+ const evalRunId = args.find(a => a.startsWith('er-'));
61
+ if (!evalRunId) throw new Error('Usage: sdlab eval-run score <eval-run-id> --outputs <path>');
62
+
63
+ const outputsIdx = args.indexOf('--outputs');
64
+ if (outputsIdx < 0) throw new Error('--outputs <path> is required (JSONL with record_id, accept, notes)');
65
+ const outputsPath = args[outputsIdx + 1];
66
+
67
+ console.log(`\x1b[1msdlab eval-run score\x1b[0m — ${evalRunId}\n`);
68
+
69
+ const { scorecard } = await scoreEvalRun(projectRoot, evalRunId, outputsPath);
70
+
71
+ const verdict = scorecard.overall_verdict === 'pass'
72
+ ? `\x1b[32mPASS\x1b[0m` : `\x1b[31mFAIL\x1b[0m`;
73
+ console.log(` Verdict: ${verdict} (${(scorecard.overall_score * 100).toFixed(1)}%)`);
74
+ console.log(` Outputs: ${scorecard.output_count} (${scorecard.accepted_count} accepted, ${scorecard.rejected_count} rejected)`);
75
+ console.log('');
76
+ console.log(' Tasks:');
77
+ for (const [name, task] of Object.entries(scorecard.tasks)) {
78
+ const status = task.passed ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m';
79
+ console.log(` ${status} ${name.padEnd(24)} ${(task.score * 100).toFixed(1)}%`);
80
+ }
81
+
82
+ } else if (subcommand === 'show') {
83
+ const evalRunId = args.find(a => a.startsWith('er-'));
84
+ if (!evalRunId) throw new Error('Usage: sdlab eval-run show <eval-run-id>');
85
+
86
+ const run = await loadEvalRun(projectRoot, evalRunId);
87
+ console.log(`\x1b[1mEval run\x1b[0m — ${evalRunId}\n`);
88
+ console.log(` Created: ${run.created_at}`);
89
+ console.log(` Status: ${run.status}`);
90
+ console.log(` Manifest: ${run.training_manifest_id}`);
91
+ console.log(` Eval pack: ${run.eval_pack_id}`);
92
+ console.log(` Profile: ${run.training_profile_id}`);
93
+ console.log(` Adapter: ${run.adapter_target}`);
94
+
95
+ if (run.scorecard) {
96
+ console.log('');
97
+ console.log(` Verdict: ${run.scorecard.overall_verdict.toUpperCase()}`);
98
+ console.log(` Score: ${(run.scorecard.overall_score * 100).toFixed(1)}%`);
99
+ console.log(` Outputs: ${run.scorecard.output_count} (${run.scorecard.accepted_count} accepted)`);
100
+ }
101
+
102
+ } else if (subcommand === 'list') {
103
+ const runs = await listEvalRuns(projectRoot);
104
+ if (runs.length === 0) {
105
+ console.log('No eval runs found. Run: sdlab eval-run create');
106
+ return;
107
+ }
108
+ console.log(`\x1b[1mEval runs\x1b[0m — ${projectName}\n`);
109
+ for (const r of runs) {
110
+ const verdict = r.verdict ? ` verdict=${r.verdict}` : '';
111
+ console.log(` ${r.id} manifest=${r.manifest} status=${r.status}${verdict} ${r.created_at}`);
112
+ }
113
+
114
+ } else {
115
+ throw new Error(`Unknown subcommand: ${subcommand}. Use: create, score, show, list`);
116
+ }
117
+ }
118
+
119
+ if (process.argv[1] && (process.argv[1].endsWith('eval-run.js') || process.argv[1].endsWith('eval-run'))) {
120
+ run().catch((err) => { console.error(err.message || err); process.exit(1); });
121
+ }
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * export.js — Build and list versioned export packages.
5
+ *
6
+ * Usage:
7
+ * sdlab export build [--snapshot <id>] [--split <id>] [--profile <name>] [--copy] [--project <name>]
8
+ * sdlab export list [--project <name>]
9
+ */
10
+
11
+ import { join } from 'node:path';
12
+ import { getProjectName } from '../lib/args.js';
13
+ import { REPO_ROOT } from '../lib/paths.js';
14
+ import { listSnapshots } from '../lib/snapshot.js';
15
+ import { listSplits } from '../lib/split.js';
16
+ import { buildExport, listExports, loadExport } from '../lib/export.js';
17
+
18
+ export async function run(argv = process.argv.slice(2)) {
19
+ const projectName = getProjectName(argv);
20
+ const projectRoot = join(REPO_ROOT, 'projects', projectName);
21
+ const subcommand = argv.find(a => !a.startsWith('--') && a !== 'true') || 'list';
22
+ const args = argv.filter(a => a !== subcommand);
23
+
24
+ if (subcommand === 'build') {
25
+ // Find snapshot
26
+ let snapshotId;
27
+ const snapIdx = args.indexOf('--snapshot');
28
+ if (snapIdx >= 0) {
29
+ snapshotId = args[snapIdx + 1];
30
+ } else {
31
+ const snapshots = await listSnapshots(projectRoot);
32
+ if (snapshots.length === 0) throw new Error('No snapshots found. Run: sdlab snapshot create');
33
+ snapshotId = snapshots[snapshots.length - 1].id;
34
+ }
35
+
36
+ // Find split
37
+ let splitId;
38
+ const splitIdx = args.indexOf('--split');
39
+ if (splitIdx >= 0) {
40
+ splitId = args[splitIdx + 1];
41
+ } else {
42
+ const splits = await listSplits(projectRoot);
43
+ if (splits.length === 0) throw new Error('No splits found. Run: sdlab split build');
44
+ splitId = splits[splits.length - 1].id;
45
+ }
46
+
47
+ const profileIdx = args.indexOf('--profile');
48
+ const profileId = profileIdx >= 0 ? args[profileIdx + 1] : null;
49
+ const copy = args.includes('--copy');
50
+
51
+ console.log(`\x1b[1msdlab export build\x1b[0m — ${projectName}`);
52
+ console.log(` Snapshot: ${snapshotId}`);
53
+ console.log(` Split: ${splitId}`);
54
+ console.log(` Profile: ${profileId || 'canonical-package (built-in)'}`);
55
+ console.log(` Images: ${copy ? 'copy' : 'symlink'}`);
56
+ console.log('');
57
+
58
+ const result = await buildExport(projectRoot, snapshotId, splitId, { profileId, copy });
59
+
60
+ console.log(` \x1b[32m✓\x1b[0m Export: ${result.exportId}`);
61
+ console.log(` Records: ${result.recordCount}`);
62
+ console.log(` Images: ${result.imageCount}`);
63
+
64
+ } else if (subcommand === 'list') {
65
+ const exports = await listExports(projectRoot);
66
+ if (exports.length === 0) {
67
+ console.log('No exports found. Run: sdlab export build');
68
+ return;
69
+ }
70
+ console.log(`\x1b[1mExports\x1b[0m — ${projectName}\n`);
71
+ for (const e of exports) {
72
+ console.log(` ${e.id} snap=${e.snapshot_id} split=${e.split_id} records=${e.records} images=${e.images} ${e.created_at}`);
73
+ }
74
+
75
+ } else {
76
+ throw new Error(`Unknown subcommand: ${subcommand}. Use: build, list`);
77
+ }
78
+ }
79
+
80
+ if (process.argv[1] && (process.argv[1].endsWith('export.js') || process.argv[1].endsWith('export'))) {
81
+ run().catch((err) => {
82
+ console.error(err.message || err);
83
+ process.exit(1);
84
+ });
85
+ }
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * implementation-pack.js — Build and inspect implementation example packs.
5
+ *
6
+ * Usage:
7
+ * sdlab implementation-pack build --manifest <id> [--project <name>]
8
+ * sdlab implementation-pack show <impl-id> [--project <name>]
9
+ * sdlab implementation-pack list [--project <name>]
10
+ */
11
+
12
+ import { join } from 'node:path';
13
+ import { getProjectName } from '../lib/args.js';
14
+ import { REPO_ROOT } from '../lib/paths.js';
15
+ import { listTrainingManifests } from '../lib/training-manifests.js';
16
+ import { buildImplementationPack, listImplementationPacks, loadImplementationPack } from '../lib/implementation-packs.js';
17
+
18
+ export async function run(argv = process.argv.slice(2)) {
19
+ const projectName = getProjectName(argv);
20
+ const projectRoot = join(REPO_ROOT, 'projects', projectName);
21
+ const subcommand = argv.find(a => !a.startsWith('--') && !a.startsWith('impl-') && !a.startsWith('tm-')) || 'list';
22
+ const args = argv.filter(a => a !== subcommand);
23
+
24
+ if (subcommand === 'build') {
25
+ let manifestId;
26
+ const manifestIdx = args.indexOf('--manifest');
27
+ if (manifestIdx >= 0) {
28
+ manifestId = args[manifestIdx + 1];
29
+ } else {
30
+ const manifests = await listTrainingManifests(projectRoot);
31
+ if (manifests.length === 0) throw new Error('No training manifests. Run: sdlab training-manifest create');
32
+ manifestId = manifests[manifests.length - 1].id;
33
+ }
34
+
35
+ console.log(`\x1b[1msdlab implementation-pack build\x1b[0m — ${projectName}`);
36
+ console.log(` Manifest: ${manifestId}`);
37
+ console.log('');
38
+
39
+ const result = await buildImplementationPack(projectRoot, manifestId);
40
+
41
+ console.log(` \x1b[32m✓\x1b[0m Implementation pack: ${result.implId}`);
42
+ console.log(` Prompt examples: ${result.prompts}`);
43
+ console.log(` Known failures: ${result.failures}`);
44
+ console.log(` Subject continuity: ${result.subjects}`);
45
+
46
+ } else if (subcommand === 'show') {
47
+ const implId = args.find(a => a.startsWith('impl-'));
48
+ if (!implId) throw new Error('Usage: sdlab implementation-pack show <impl-id>');
49
+
50
+ const pack = await loadImplementationPack(projectRoot, implId);
51
+ console.log(`\x1b[1mImplementation pack\x1b[0m — ${implId}\n`);
52
+ console.log(` Created: ${pack.created_at}`);
53
+ console.log(` Manifest: ${pack.training_manifest_id}`);
54
+ console.log(` Profile: ${pack.training_profile_id}`);
55
+ console.log(` Prompts: ${pack.counts.prompt_examples}`);
56
+ console.log(` Failures: ${pack.counts.known_failures}`);
57
+ console.log(` Subjects: ${pack.counts.subject_examples}`);
58
+ console.log(` Lanes: ${pack.counts.lanes_covered}`);
59
+
60
+ } else if (subcommand === 'list') {
61
+ const packs = await listImplementationPacks(projectRoot);
62
+ if (packs.length === 0) {
63
+ console.log('No implementation packs. Run: sdlab implementation-pack build');
64
+ return;
65
+ }
66
+ console.log(`\x1b[1mImplementation packs\x1b[0m — ${projectName}\n`);
67
+ for (const p of packs) {
68
+ console.log(` ${p.id} manifest=${p.manifest} prompts=${p.prompts} failures=${p.failures} ${p.created_at}`);
69
+ }
70
+
71
+ } else {
72
+ throw new Error(`Unknown subcommand: ${subcommand}. Use: build, show, list`);
73
+ }
74
+ }
75
+
76
+ if (process.argv[1] && (process.argv[1].endsWith('implementation-pack.js') || process.argv[1].endsWith('implementation-pack'))) {
77
+ run().catch((err) => { console.error(err.message || err); process.exit(1); });
78
+ }
package/scripts/init.js CHANGED
@@ -32,6 +32,14 @@ const PROJECT_DIRS = [
32
32
  'outputs/painterly',
33
33
  'outputs/painterly-test',
34
34
  'exports',
35
+ 'snapshots',
36
+ 'splits',
37
+ 'eval-packs',
38
+ 'training/profiles',
39
+ 'training/manifests',
40
+ 'training/packages',
41
+ 'training/eval-runs',
42
+ 'training/implementations',
35
43
  ];
36
44
 
37
45
  function isValidName(name) {
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * reingest.js — Re-ingest generated outputs as new project records.
5
+ *
6
+ * Usage:
7
+ * sdlab reingest generated --source <dir> --manifest <id> [--dry-run] [--project <name>]
8
+ * sdlab reingest audit [--project <name>]
9
+ */
10
+
11
+ import { join } from 'node:path';
12
+ import { getProjectName } from '../lib/args.js';
13
+ import { REPO_ROOT } from '../lib/paths.js';
14
+ import { reingestGenerated, auditReingest } from '../lib/reingest.js';
15
+
16
+ export async function run(argv = process.argv.slice(2)) {
17
+ const projectName = getProjectName(argv);
18
+ const projectRoot = join(REPO_ROOT, 'projects', projectName);
19
+ const subcommand = argv.find(a => !a.startsWith('--')) || 'audit';
20
+ const args = argv.filter(a => a !== subcommand);
21
+
22
+ if (subcommand === 'generated') {
23
+ const sourceIdx = args.indexOf('--source');
24
+ if (sourceIdx < 0) throw new Error('--source <dir> is required (directory of generated images)');
25
+ const sourcePath = args[sourceIdx + 1];
26
+
27
+ const manifestIdx = args.indexOf('--manifest');
28
+ if (manifestIdx < 0) throw new Error('--manifest <id> is required (training manifest that produced the outputs)');
29
+ const manifestId = args[manifestIdx + 1];
30
+
31
+ const dryRun = args.includes('--dry-run');
32
+
33
+ console.log(`\x1b[1msdlab reingest generated\x1b[0m — ${projectName}`);
34
+ console.log(` Source: ${sourcePath}`);
35
+ console.log(` Manifest: ${manifestId}`);
36
+ if (dryRun) console.log(` Mode: DRY RUN (no records written)`);
37
+ console.log('');
38
+
39
+ const result = await reingestGenerated(projectRoot, sourcePath, manifestId, { dryRun });
40
+
41
+ console.log(` \x1b[32m✓\x1b[0m Created: ${result.created} records`);
42
+ if (result.skipped > 0) {
43
+ console.log(` Skipped: ${result.skipped} (already exist)`);
44
+ }
45
+ if (result.created > 0) {
46
+ console.log('');
47
+ console.log(' Next steps:');
48
+ console.log(' 1. Review: sdlab curate <id> approved/rejected "explanation"');
49
+ console.log(' 2. Bind: sdlab bind --project ' + projectName);
50
+ console.log(' 3. Then eligible for future snapshots');
51
+ }
52
+
53
+ } else if (subcommand === 'audit') {
54
+ console.log(`\x1b[1msdlab reingest audit\x1b[0m — ${projectName}\n`);
55
+
56
+ const result = await auditReingest(projectRoot);
57
+
58
+ if (result.total === 0) {
59
+ console.log(' No re-ingested records found (gen_* prefix).');
60
+ return;
61
+ }
62
+
63
+ console.log(` Re-ingested records: ${result.total}`);
64
+ console.log(` Reviewed: ${result.reviewed}`);
65
+ console.log(` Unreviewed: ${result.unreviewed}`);
66
+ console.log(` Canon-bound: ${result.bound}`);
67
+ console.log(` Unbound: ${result.unbound}`);
68
+
69
+ if (result.unreviewed > 0) {
70
+ console.log(`\n \x1b[33m⚠\x1b[0m ${result.unreviewed} records awaiting review`);
71
+ }
72
+ if (result.unbound > 0) {
73
+ console.log(` \x1b[33m⚠\x1b[0m ${result.unbound} reviewed records not yet canon-bound`);
74
+ }
75
+ if (result.unreviewed === 0 && result.unbound === 0) {
76
+ console.log(`\n \x1b[32m✓\x1b[0m All re-ingested records are reviewed and canon-bound`);
77
+ }
78
+
79
+ } else {
80
+ throw new Error(`Unknown subcommand: ${subcommand}. Use: generated, audit`);
81
+ }
82
+ }
83
+
84
+ if (process.argv[1] && (process.argv[1].endsWith('reingest.js') || process.argv[1].endsWith('reingest'))) {
85
+ run().catch((err) => { console.error(err.message || err); process.exit(1); });
86
+ }
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * snapshot.js — Create, list, show, and diff dataset snapshots.
5
+ *
6
+ * Usage:
7
+ * sdlab snapshot create [--profile <name>] [--project <name>]
8
+ * sdlab snapshot list [--project <name>]
9
+ * sdlab snapshot show <snapshot-id> [--project <name>]
10
+ * sdlab snapshot diff <id-a> <id-b> [--project <name>]
11
+ */
12
+
13
+ import { readFile } from 'node:fs/promises';
14
+ import { join } from 'node:path';
15
+ import { existsSync } from 'node:fs';
16
+ import { getProjectName } from '../lib/args.js';
17
+ import { REPO_ROOT } from '../lib/paths.js';
18
+ import { loadSelectionProfile } from '../lib/config.js';
19
+ import { createSnapshot, loadSnapshot, listSnapshots, diffSnapshots } from '../lib/snapshot.js';
20
+ import { evaluateEligibility } from '../lib/eligibility.js';
21
+ import { categorizeExclusions } from '../lib/eligibility.js';
22
+
23
+ export async function run(argv = process.argv.slice(2)) {
24
+ const projectName = getProjectName(argv);
25
+ const projectRoot = join(REPO_ROOT, 'projects', projectName);
26
+ const subcommand = argv.find(a => !a.startsWith('--')) || 'list';
27
+
28
+ // Strip subcommand from argv for flag parsing
29
+ const args = argv.filter(a => a !== subcommand);
30
+
31
+ if (subcommand === 'create') {
32
+ const profileIdx = args.indexOf('--profile');
33
+ const profileId = profileIdx >= 0 ? args[profileIdx + 1] : null;
34
+ const profile = loadSelectionProfile(projectRoot, profileId);
35
+
36
+ console.log(`\x1b[1msdlab snapshot create\x1b[0m — ${projectName}`);
37
+ console.log(` Profile: ${profileId || 'training-default (built-in)'}`);
38
+ console.log('');
39
+
40
+ const result = await createSnapshot(projectRoot, profile);
41
+
42
+ console.log(` \x1b[32m✓\x1b[0m Snapshot: ${result.snapshotId}`);
43
+ console.log(` Included: ${result.included}`);
44
+ console.log(` Excluded: ${result.excluded}`);
45
+
46
+ // Read and display summary
47
+ const summary = JSON.parse(
48
+ await readFile(join(projectRoot, 'snapshots', result.snapshotId, 'summary.json'), 'utf-8')
49
+ );
50
+ console.log('\n Lane distribution:');
51
+ for (const [lane, count] of Object.entries(summary.lane_distribution).sort((a, b) => b[1] - a[1])) {
52
+ console.log(` ${lane}: ${count}`);
53
+ }
54
+ console.log('\n Faction distribution:');
55
+ for (const [f, c] of Object.entries(summary.faction_distribution).sort((a, b) => b[1] - a[1])) {
56
+ console.log(` ${f}: ${c}`);
57
+ }
58
+
59
+ // Read and categorize exclusions
60
+ const excludedRaw = await readFile(
61
+ join(projectRoot, 'snapshots', result.snapshotId, 'excluded.jsonl'), 'utf-8'
62
+ );
63
+ const excluded = excludedRaw.trim().split('\n').filter(Boolean).map(l => JSON.parse(l));
64
+ const categories = categorizeExclusions(excluded);
65
+ console.log('\n Exclusion reasons:');
66
+ for (const [cat, count] of Object.entries(categories)) {
67
+ if (count > 0) console.log(` ${cat}: ${count}`);
68
+ }
69
+
70
+ } else if (subcommand === 'list') {
71
+ const snapshots = await listSnapshots(projectRoot);
72
+ if (snapshots.length === 0) {
73
+ console.log('No snapshots found. Run: sdlab snapshot create');
74
+ return;
75
+ }
76
+ console.log(`\x1b[1mSnapshots\x1b[0m — ${projectName}\n`);
77
+ for (const s of snapshots) {
78
+ console.log(` ${s.id} included=${s.included} excluded=${s.excluded} fp=${s.fingerprint} ${s.created_at}`);
79
+ }
80
+
81
+ } else if (subcommand === 'show') {
82
+ const snapshotId = args.find(a => a.startsWith('snap-'));
83
+ if (!snapshotId) throw new Error('Usage: sdlab snapshot show <snapshot-id>');
84
+
85
+ const manifest = await loadSnapshot(projectRoot, snapshotId);
86
+ console.log(`\x1b[1mSnapshot\x1b[0m — ${snapshotId}\n`);
87
+ console.log(` Created: ${manifest.created_at}`);
88
+ console.log(` Project: ${manifest.project_name}`);
89
+ console.log(` Frozen: ${manifest.frozen}`);
90
+ console.log(` Records: ${manifest.counts.total_records}`);
91
+ console.log(` Included: ${manifest.counts.included}`);
92
+ console.log(` Excluded: ${manifest.counts.excluded}`);
93
+ console.log(` Config fingerprint: ${manifest.config_fingerprint}`);
94
+ console.log('');
95
+ console.log(' Selection profile:');
96
+ for (const [k, v] of Object.entries(manifest.selection_profile)) {
97
+ console.log(` ${k}: ${JSON.stringify(v)}`);
98
+ }
99
+
100
+ } else if (subcommand === 'diff') {
101
+ const snapIds = args.filter(a => a.startsWith('snap-'));
102
+ if (snapIds.length < 2) throw new Error('Usage: sdlab snapshot diff <id-a> <id-b>');
103
+
104
+ const diff = await diffSnapshots(projectRoot, snapIds[0], snapIds[1]);
105
+ console.log(`\x1b[1mSnapshot diff\x1b[0m — ${snapIds[0]} vs ${snapIds[1]}\n`);
106
+ console.log(` ${snapIds[0]}: ${diff.countA} records`);
107
+ console.log(` ${snapIds[1]}: ${diff.countB} records`);
108
+ console.log(` Shared: ${diff.shared.length}`);
109
+ console.log(` Added in ${snapIds[1]}: ${diff.added.length}`);
110
+ console.log(` Removed from ${snapIds[0]}: ${diff.removed.length}`);
111
+
112
+ if (diff.added.length > 0 && diff.added.length <= 20) {
113
+ console.log('\n Added:');
114
+ for (const id of diff.added) console.log(` + ${id}`);
115
+ }
116
+ if (diff.removed.length > 0 && diff.removed.length <= 20) {
117
+ console.log('\n Removed:');
118
+ for (const id of diff.removed) console.log(` - ${id}`);
119
+ }
120
+
121
+ } else {
122
+ throw new Error(`Unknown subcommand: ${subcommand}. Use: create, list, show, diff`);
123
+ }
124
+ }
125
+
126
+ // Direct execution guard
127
+ if (process.argv[1] && (process.argv[1].endsWith('snapshot.js') || process.argv[1].endsWith('snapshot'))) {
128
+ run().catch((err) => {
129
+ console.error(err.message || err);
130
+ process.exit(1);
131
+ });
132
+ }