@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.
- package/CHANGELOG.md +28 -0
- package/README.md +70 -24
- package/bin/sdlab.js +23 -0
- package/lib/card.js +145 -0
- package/lib/config.js +144 -0
- package/lib/eligibility.js +110 -0
- package/lib/eval-pack.js +283 -0
- package/lib/export.js +259 -0
- package/lib/snapshot.js +211 -0
- package/lib/split.js +409 -0
- package/package.json +12 -10
- package/scripts/canon-bind.js +1 -87
- package/scripts/card.js +73 -0
- package/scripts/doctor.js +3 -0
- package/scripts/eligibility.js +91 -0
- package/scripts/eval-pack.js +92 -0
- package/scripts/export.js +85 -0
- package/scripts/init.js +3 -0
- package/scripts/snapshot.js +132 -0
- package/scripts/split.js +154 -0
package/scripts/canon-bind.js
CHANGED
|
@@ -20,93 +20,7 @@ import { readFileSync, writeFileSync, readdirSync, existsSync } from 'fs';
|
|
|
20
20
|
import { join } from 'path';
|
|
21
21
|
import { getProjectName } from '../lib/args.js';
|
|
22
22
|
import { REPO_ROOT } from '../lib/paths.js';
|
|
23
|
-
import { loadConstitution, loadLanes, loadRubric, loadTerminology } from '../lib/config.js';
|
|
24
|
-
|
|
25
|
-
// ─── Config-Driven Detection ────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Detect lane from asset ID using project lane config.
|
|
29
|
-
* Tests each lane's id_patterns as regex against the lowercase ID.
|
|
30
|
-
* Returns the first match, or the default_lane.
|
|
31
|
-
*/
|
|
32
|
-
function detectLane(id, prompt, lanesConfig) {
|
|
33
|
-
const lower = id.toLowerCase();
|
|
34
|
-
for (const lane of lanesConfig.lanes) {
|
|
35
|
-
if (!lane.id_patterns || lane.id_patterns.length === 0) continue;
|
|
36
|
-
for (const pattern of lane.id_patterns) {
|
|
37
|
-
if (new RegExp(pattern).test(lower)) return lane.id;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return lanesConfig.default_lane;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Detect group (faction) from asset ID and prompt using terminology config.
|
|
45
|
-
* Mirrors the exact detection order from the original hardcoded detectFaction():
|
|
46
|
-
* 1. Primary ID patterns per group
|
|
47
|
-
* 2. Cross-group patterns (e.g. ^cf_ → detect from ID suffix)
|
|
48
|
-
* 3. Edge defaults (e.g. ^edge_ → compact)
|
|
49
|
-
* 4. Prompt-based fallback patterns
|
|
50
|
-
* 5. Null-faction patterns (generic rejects with no faction)
|
|
51
|
-
* 6. ID fallback patterns (specific reject/borderline IDs)
|
|
52
|
-
*/
|
|
53
|
-
function detectGroup(id, prompt, termConfig) {
|
|
54
|
-
// Explicit ordering for ID-based and prompt-based detection phases.
|
|
55
|
-
// These can differ — e.g. ID checks reach before keth, but prompt
|
|
56
|
-
// checks veshan before keth before orryn before reach.
|
|
57
|
-
const idOrder = termConfig.id_detection_order || Object.keys(termConfig.groups);
|
|
58
|
-
const promptOrder = termConfig.prompt_detection_order || Object.keys(termConfig.groups);
|
|
59
|
-
|
|
60
|
-
// 1. Primary ID patterns per group (id_detection_order)
|
|
61
|
-
for (const groupName of idOrder) {
|
|
62
|
-
const group = termConfig.groups[groupName];
|
|
63
|
-
if (!group?.id_patterns) continue;
|
|
64
|
-
for (const pattern of group.id_patterns) {
|
|
65
|
-
if (new RegExp(pattern).test(id)) return groupName;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// 2. Cross-group patterns
|
|
70
|
-
for (const [pattern, behavior] of Object.entries(termConfig.cross_group_patterns || {})) {
|
|
71
|
-
if (new RegExp(pattern).test(id)) {
|
|
72
|
-
if (behavior === 'detect_from_id_suffix') {
|
|
73
|
-
for (const groupName of idOrder) {
|
|
74
|
-
if (id.includes(groupName)) return groupName;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// 3. Edge defaults
|
|
82
|
-
for (const [pattern, groupName] of Object.entries(termConfig.edge_defaults || {})) {
|
|
83
|
-
if (new RegExp(pattern).test(id)) return groupName;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// 4. Prompt-based fallback (prompt_detection_order)
|
|
87
|
-
if (prompt) {
|
|
88
|
-
const p = prompt.toLowerCase();
|
|
89
|
-
for (const groupName of promptOrder) {
|
|
90
|
-
const group = termConfig.groups[groupName];
|
|
91
|
-
if (!group?.prompt_patterns || group.prompt_patterns.length === 0) continue;
|
|
92
|
-
for (const keyword of group.prompt_patterns) {
|
|
93
|
-
if (new RegExp(keyword).test(p)) return groupName;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// 5. Null-faction patterns (generic rejects — no faction)
|
|
99
|
-
for (const pattern of termConfig.null_faction_patterns || []) {
|
|
100
|
-
if (new RegExp(pattern).test(id)) return null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// 6. ID fallback patterns
|
|
104
|
-
for (const [pattern, groupName] of Object.entries(termConfig.id_fallbacks || {})) {
|
|
105
|
-
if (new RegExp(pattern).test(id)) return groupName;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
23
|
+
import { loadConstitution, loadLanes, loadRubric, loadTerminology, detectLane, detectGroup } from '../lib/config.js';
|
|
110
24
|
|
|
111
25
|
/**
|
|
112
26
|
* Build rationale string from rule's template, interpolating variables.
|
package/scripts/card.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* card.js — Generate dataset cards.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* sdlab card generate --snapshot <id> --split <id> [--project <name>]
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { writeFile } from 'node:fs/promises';
|
|
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 { generateCard } from '../lib/card.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('--')) || 'generate';
|
|
22
|
+
const args = argv.filter(a => a !== subcommand);
|
|
23
|
+
|
|
24
|
+
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 {
|
|
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
|
+
console.log(`\x1b[1msdlab card generate\x1b[0m — ${projectName}`);
|
|
48
|
+
console.log(` Snapshot: ${snapshotId}`);
|
|
49
|
+
console.log(` Split: ${splitId}`);
|
|
50
|
+
console.log('');
|
|
51
|
+
|
|
52
|
+
const { markdown, json } = await generateCard(projectRoot, snapshotId, splitId);
|
|
53
|
+
|
|
54
|
+
// Write to project root
|
|
55
|
+
const mdPath = join(projectRoot, 'dataset-card.md');
|
|
56
|
+
const jsonPath = join(projectRoot, 'dataset-card.json');
|
|
57
|
+
await writeFile(mdPath, markdown);
|
|
58
|
+
await writeFile(jsonPath, JSON.stringify(json, null, 2) + '\n');
|
|
59
|
+
|
|
60
|
+
console.log(` \x1b[32m✓\x1b[0m dataset-card.md`);
|
|
61
|
+
console.log(` \x1b[32m✓\x1b[0m dataset-card.json`);
|
|
62
|
+
|
|
63
|
+
} else {
|
|
64
|
+
throw new Error(`Unknown subcommand: ${subcommand}. Use: generate`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
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
|
+
});
|
|
73
|
+
}
|
package/scripts/doctor.js
CHANGED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* eligibility.js — Audit training/eval eligibility for all project records.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* sdlab eligibility audit [--project <name>]
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { getProjectName } from '../lib/args.js';
|
|
13
|
+
import { REPO_ROOT } from '../lib/paths.js';
|
|
14
|
+
import { loadProjectConfig, loadSelectionProfile } from '../lib/config.js';
|
|
15
|
+
import { evaluateEligibility, categorizeExclusions } from '../lib/eligibility.js';
|
|
16
|
+
|
|
17
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
18
|
+
const projectName = getProjectName(argv);
|
|
19
|
+
const projectRoot = join(REPO_ROOT, 'projects', projectName);
|
|
20
|
+
|
|
21
|
+
const profileIdx = argv.indexOf('--profile');
|
|
22
|
+
const profileId = profileIdx >= 0 ? argv[profileIdx + 1] : null;
|
|
23
|
+
const profile = loadSelectionProfile(projectRoot, profileId);
|
|
24
|
+
const config = loadProjectConfig(projectRoot);
|
|
25
|
+
|
|
26
|
+
const recordsDir = join(projectRoot, 'records');
|
|
27
|
+
const files = (await readdir(recordsDir)).filter(f => f.endsWith('.json')).sort();
|
|
28
|
+
|
|
29
|
+
console.log(`\x1b[1msdlab eligibility audit\x1b[0m — ${projectName}`);
|
|
30
|
+
console.log(` Profile: ${profileId || 'training-default (built-in)'}`);
|
|
31
|
+
console.log(` Records: ${files.length}`);
|
|
32
|
+
console.log('');
|
|
33
|
+
|
|
34
|
+
const eligible = [];
|
|
35
|
+
const excluded = [];
|
|
36
|
+
const nearMiss = []; // Records with exactly 1 failing reason
|
|
37
|
+
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
const raw = await readFile(join(recordsDir, file), 'utf-8');
|
|
40
|
+
const record = JSON.parse(raw);
|
|
41
|
+
|
|
42
|
+
const result = evaluateEligibility(record, profile, config.lanes, config.terminology);
|
|
43
|
+
|
|
44
|
+
if (result.eligible) {
|
|
45
|
+
eligible.push(record.id);
|
|
46
|
+
} else {
|
|
47
|
+
excluded.push({ record_id: record.id, reasons: result.reasons });
|
|
48
|
+
if (result.reasons.length === 1) {
|
|
49
|
+
nearMiss.push({ record_id: record.id, reason: result.reasons[0] });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(` \x1b[32m✓\x1b[0m Eligible: ${eligible.length}`);
|
|
55
|
+
console.log(` \x1b[31m✗\x1b[0m Excluded: ${excluded.length}`);
|
|
56
|
+
console.log('');
|
|
57
|
+
|
|
58
|
+
// Categorize exclusions
|
|
59
|
+
const categories = categorizeExclusions(excluded);
|
|
60
|
+
console.log(' Exclusion breakdown:');
|
|
61
|
+
for (const [cat, count] of Object.entries(categories)) {
|
|
62
|
+
if (count > 0) {
|
|
63
|
+
const bar = '█'.repeat(Math.min(Math.round(count / files.length * 50), 50));
|
|
64
|
+
console.log(` ${cat.padEnd(20)} ${String(count).padStart(4)} ${bar}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Near-miss improvement opportunities
|
|
69
|
+
if (nearMiss.length > 0) {
|
|
70
|
+
console.log(`\n Improvement opportunities (${nearMiss.length} records with 1 failing check):`);
|
|
71
|
+
const byReason = {};
|
|
72
|
+
for (const nm of nearMiss) {
|
|
73
|
+
byReason[nm.reason] = (byReason[nm.reason] || 0) + 1;
|
|
74
|
+
}
|
|
75
|
+
for (const [reason, count] of Object.entries(byReason).sort((a, b) => b[1] - a[1]).slice(0, 5)) {
|
|
76
|
+
console.log(` ${count} records: ${reason}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log('');
|
|
81
|
+
const pct = ((eligible.length / files.length) * 100).toFixed(1);
|
|
82
|
+
console.log(` Eligibility rate: ${pct}% (${eligible.length}/${files.length})`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Direct execution guard
|
|
86
|
+
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
|
+
});
|
|
91
|
+
}
|
|
@@ -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,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
|
+
}
|
package/scripts/init.js
CHANGED
|
@@ -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
|
+
}
|