@mcptoolshop/style-dataset-lab 2.0.0 → 2.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.
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * split.js — Build, show, and audit dataset splits.
5
+ *
6
+ * Usage:
7
+ * sdlab split build [--snapshot <id>] [--profile <name>] [--project <name>]
8
+ * sdlab split list [--project <name>]
9
+ * sdlab split show <split-id> [--project <name>]
10
+ * sdlab split audit <split-id> [--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 { loadSplitProfile } from '../lib/config.js';
18
+ import { listSnapshots } from '../lib/snapshot.js';
19
+ import { createSplit, loadSplit, loadSplitAudit, listSplits } from '../lib/split.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('--')) || 'list';
25
+ const args = argv.filter(a => a !== subcommand);
26
+
27
+ if (subcommand === 'build') {
28
+ // Find snapshot ID
29
+ let snapshotId;
30
+ const snapIdx = args.indexOf('--snapshot');
31
+ if (snapIdx >= 0) {
32
+ snapshotId = args[snapIdx + 1];
33
+ } else {
34
+ // Use latest snapshot
35
+ const snapshots = await listSnapshots(projectRoot);
36
+ if (snapshots.length === 0) {
37
+ throw new Error('No snapshots found. Run: sdlab snapshot create');
38
+ }
39
+ snapshotId = snapshots[snapshots.length - 1].id;
40
+ }
41
+
42
+ const profileIdx = args.indexOf('--profile');
43
+ const profileId = profileIdx >= 0 ? args[profileIdx + 1] : null;
44
+ const profile = loadSplitProfile(projectRoot, profileId);
45
+
46
+ console.log(`\x1b[1msdlab split build\x1b[0m — ${projectName}`);
47
+ console.log(` Snapshot: ${snapshotId}`);
48
+ console.log(` Profile: ${profileId || 'balanced-default (built-in)'}`);
49
+ console.log(` Strategy: ${profile.strategy}`);
50
+ console.log(` Ratios: ${profile.train_ratio}/${profile.val_ratio}/${profile.test_ratio}`);
51
+ console.log(` Seed: ${profile.seed}`);
52
+ console.log('');
53
+
54
+ const result = await createSplit(projectRoot, snapshotId, profile);
55
+
56
+ console.log(` \x1b[32m✓\x1b[0m Split: ${result.splitId}`);
57
+ console.log(` Train: ${result.train}`);
58
+ console.log(` Val: ${result.val}`);
59
+ console.log(` Test: ${result.test}`);
60
+
61
+ // Show audit summary
62
+ const audit = await loadSplitAudit(projectRoot, result.splitId);
63
+ console.log('');
64
+ if (audit.leakage_check.passed) {
65
+ console.log(` \x1b[32m✓\x1b[0m No subject leakage detected`);
66
+ } else {
67
+ console.log(` \x1b[31m✗\x1b[0m Subject leakage detected:`);
68
+ for (const issue of audit.leakage_check.issues) {
69
+ console.log(` family="${issue.family}" (${issue.record_count} records) → ${issue.leaked_to.join(', ')}`);
70
+ }
71
+ }
72
+
73
+ console.log(` Families: ${audit.family_count} (train=${audit.families_per_split.train}, val=${audit.families_per_split.val}, test=${audit.families_per_split.test})`);
74
+
75
+ } else if (subcommand === 'list') {
76
+ const splits = await listSplits(projectRoot);
77
+ if (splits.length === 0) {
78
+ console.log('No splits found. Run: sdlab split build');
79
+ return;
80
+ }
81
+ console.log(`\x1b[1mSplits\x1b[0m — ${projectName}\n`);
82
+ for (const s of splits) {
83
+ console.log(` ${s.id} snap=${s.snapshot_id} train=${s.train} val=${s.val} test=${s.test} families=${s.families} ${s.created_at}`);
84
+ }
85
+
86
+ } else if (subcommand === 'show') {
87
+ const splitId = args.find(a => a.startsWith('split-'));
88
+ if (!splitId) throw new Error('Usage: sdlab split show <split-id>');
89
+
90
+ const manifest = await loadSplit(projectRoot, splitId);
91
+ console.log(`\x1b[1mSplit\x1b[0m — ${splitId}\n`);
92
+ console.log(` Created: ${manifest.created_at}`);
93
+ console.log(` Snapshot: ${manifest.snapshot_id}`);
94
+ console.log(` Records: ${manifest.counts.total_records}`);
95
+ console.log(` Families: ${manifest.counts.total_families}`);
96
+ console.log(` Train: ${manifest.counts.train}`);
97
+ console.log(` Val: ${manifest.counts.val}`);
98
+ console.log(` Test: ${manifest.counts.test}`);
99
+ console.log('');
100
+ console.log(' Profile:');
101
+ for (const [k, v] of Object.entries(manifest.profile)) {
102
+ console.log(` ${k}: ${JSON.stringify(v)}`);
103
+ }
104
+ if (manifest.warnings.length > 0) {
105
+ console.log('');
106
+ console.log(' Warnings:');
107
+ for (const w of manifest.warnings) {
108
+ console.log(` ⚠ ${w}`);
109
+ }
110
+ }
111
+
112
+ } else if (subcommand === 'audit') {
113
+ const splitId = args.find(a => a.startsWith('split-'));
114
+ if (!splitId) throw new Error('Usage: sdlab split audit <split-id>');
115
+
116
+ const audit = await loadSplitAudit(projectRoot, splitId);
117
+ console.log(`\x1b[1mSplit audit\x1b[0m — ${splitId}\n`);
118
+
119
+ // Leakage check
120
+ if (audit.leakage_check.passed) {
121
+ console.log(` \x1b[32m✓\x1b[0m Leakage check: PASSED`);
122
+ } else {
123
+ console.log(` \x1b[31m✗\x1b[0m Leakage check: FAILED`);
124
+ for (const issue of audit.leakage_check.issues) {
125
+ console.log(` family="${issue.family}" (${issue.record_count} records) leaked to: ${issue.leaked_to.join(', ')}`);
126
+ }
127
+ }
128
+
129
+ // Family distribution
130
+ console.log(`\n Families: ${audit.family_count}`);
131
+ console.log(` Train: ${audit.families_per_split.train}`);
132
+ console.log(` Val: ${audit.families_per_split.val}`);
133
+ console.log(` Test: ${audit.families_per_split.test}`);
134
+
135
+ // Lane balance
136
+ console.log('\n Lane balance:');
137
+ console.log(` ${'Lane'.padEnd(20)} ${'Total'.padStart(6)} ${'Train%'.padStart(8)} ${'Val%'.padStart(8)} ${'Test%'.padStart(8)}`);
138
+ console.log(` ${'─'.repeat(52)}`);
139
+ for (const [lane, b] of Object.entries(audit.lane_balance).sort((a, b) => b[1].total - a[1].total)) {
140
+ console.log(` ${lane.padEnd(20)} ${String(b.total).padStart(6)} ${String(b.train_pct + '%').padStart(8)} ${String(b.val_pct + '%').padStart(8)} ${String(b.test_pct + '%').padStart(8)}`);
141
+ }
142
+
143
+ } else {
144
+ throw new Error(`Unknown subcommand: ${subcommand}. Use: build, list, show, audit`);
145
+ }
146
+ }
147
+
148
+ // Direct execution guard
149
+ if (process.argv[1] && (process.argv[1].endsWith('split.js') || process.argv[1].endsWith('split'))) {
150
+ run().catch((err) => {
151
+ console.error(err.message || err);
152
+ process.exit(1);
153
+ });
154
+ }