@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.
- package/CHANGELOG.md +52 -0
- package/README.es.md +137 -108
- package/README.fr.md +137 -108
- package/README.hi.md +137 -108
- package/README.it.md +137 -108
- package/README.ja.md +137 -108
- package/README.md +87 -91
- package/README.pt-BR.md +137 -108
- package/README.zh.md +137 -108
- package/bin/sdlab.js +49 -0
- package/lib/adapters/diffusers-lora.js +110 -0
- package/lib/adapters/generic-image-caption.js +94 -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/eval-runs.js +285 -0
- package/lib/export.js +259 -0
- package/lib/implementation-packs.js +316 -0
- package/lib/reingest.js +131 -0
- package/lib/snapshot.js +211 -0
- package/lib/split.js +409 -0
- package/lib/training-manifests.js +172 -0
- package/lib/training-packages.js +241 -0
- package/lib/training-profiles.js +97 -0
- package/package.json +12 -10
- package/scripts/canon-bind.js +1 -87
- package/scripts/card.js +73 -0
- package/scripts/doctor.js +8 -0
- package/scripts/eligibility.js +91 -0
- package/scripts/eval-pack.js +92 -0
- package/scripts/eval-run.js +121 -0
- package/scripts/export.js +85 -0
- package/scripts/implementation-pack.js +78 -0
- package/scripts/init.js +8 -0
- package/scripts/reingest.js +86 -0
- package/scripts/snapshot.js +132 -0
- package/scripts/split.js +154 -0
- package/scripts/training-manifest.js +119 -0
- package/scripts/training-package.js +84 -0
- package/scripts/training-profile.js +63 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Training package builder.
|
|
3
|
+
*
|
|
4
|
+
* Builds trainer-ready dataset packages from training manifests.
|
|
5
|
+
* Adapters transform the canonical layout for specific training targets
|
|
6
|
+
* but never mutate inclusion or split truth.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFile, writeFile, mkdir, readdir, symlink, copyFile } from 'node:fs/promises';
|
|
10
|
+
import { join, relative } from 'node:path';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
13
|
+
import { loadTrainingManifest } from './training-manifests.js';
|
|
14
|
+
import { loadTrainingProfile } from './training-profiles.js';
|
|
15
|
+
import { loadSplitPartition } from './split.js';
|
|
16
|
+
import { loadRecord } from './records.js';
|
|
17
|
+
import { loadProjectConfig, detectLane, detectGroup } from './config.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate a training package ID: tp-YYYYMMDD-HHMMSS-XXXX
|
|
21
|
+
*/
|
|
22
|
+
function generatePackageId() {
|
|
23
|
+
const now = new Date();
|
|
24
|
+
const date = now.toISOString().replace(/[-:T]/g, '').slice(0, 14);
|
|
25
|
+
const rand = randomBytes(2).toString('hex');
|
|
26
|
+
return `tp-${date.slice(0, 8)}-${date.slice(8, 14)}-${rand}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function sha256File(filePath) {
|
|
30
|
+
const data = await readFile(filePath);
|
|
31
|
+
return createHash('sha256').update(data).digest('hex');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Load an adapter module by name.
|
|
36
|
+
*/
|
|
37
|
+
async function loadAdapter(adapterTarget) {
|
|
38
|
+
const adapterPath = `./adapters/${adapterTarget}.js`;
|
|
39
|
+
try {
|
|
40
|
+
return await import(adapterPath);
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Adapter "${adapterTarget}" not found. Available: generic-image-caption, diffusers-lora`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build a training package from a manifest.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} projectRoot
|
|
52
|
+
* @param {string} manifestId
|
|
53
|
+
* @param {Object} options — { copy?, adapterOverride? }
|
|
54
|
+
* @returns {Promise<{packageId: string, records: number, images: number}>}
|
|
55
|
+
*/
|
|
56
|
+
export async function buildTrainingPackage(projectRoot, manifestId, options = {}) {
|
|
57
|
+
const manifest = await loadTrainingManifest(projectRoot, manifestId);
|
|
58
|
+
const profile = await loadTrainingProfile(projectRoot, manifest.training_profile_id);
|
|
59
|
+
const config = loadProjectConfig(projectRoot);
|
|
60
|
+
const adapterTarget = options.adapterOverride || manifest.adapter_target;
|
|
61
|
+
const adapter = await loadAdapter(adapterTarget);
|
|
62
|
+
|
|
63
|
+
const recordsDir = join(projectRoot, 'records');
|
|
64
|
+
const packageId = generatePackageId();
|
|
65
|
+
const packageDir = join(projectRoot, 'training', 'packages', packageId);
|
|
66
|
+
|
|
67
|
+
// Load split partitions
|
|
68
|
+
const partitions = {};
|
|
69
|
+
let totalRecords = 0;
|
|
70
|
+
let totalImages = 0;
|
|
71
|
+
|
|
72
|
+
for (const partition of ['train', 'val', 'test']) {
|
|
73
|
+
const entries = await loadSplitPartition(projectRoot, manifest.source_split_id, partition);
|
|
74
|
+
const records = [];
|
|
75
|
+
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
const record = await loadRecord(recordsDir, entry.record_id);
|
|
78
|
+
if (!record) continue;
|
|
79
|
+
|
|
80
|
+
// Filter by profile's eligible lanes
|
|
81
|
+
const prompt = record.provenance?.prompt || '';
|
|
82
|
+
const lane = detectLane(record.id, prompt, config.lanes);
|
|
83
|
+
if (profile.eligible_lanes.length > 0 && !profile.eligible_lanes.includes(lane)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const group = detectGroup(record.id, prompt, config.terminology);
|
|
88
|
+
records.push({ record, lane, group });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
partitions[partition] = records;
|
|
92
|
+
totalRecords += records.length;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Let the adapter build the package layout
|
|
96
|
+
const adapterResult = await adapter.buildPackage({
|
|
97
|
+
packageDir,
|
|
98
|
+
partitions,
|
|
99
|
+
profile,
|
|
100
|
+
manifest,
|
|
101
|
+
config,
|
|
102
|
+
projectRoot,
|
|
103
|
+
copy: options.copy || false,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
totalImages = adapterResult.imageCount || 0;
|
|
107
|
+
|
|
108
|
+
// Generate checksums for all files in the package
|
|
109
|
+
const checksumEntries = [];
|
|
110
|
+
const allFiles = await collectFiles(packageDir);
|
|
111
|
+
for (const filePath of allFiles.sort()) {
|
|
112
|
+
const relPath = relative(packageDir, filePath);
|
|
113
|
+
if (relPath === 'checksums.txt') continue;
|
|
114
|
+
const hash = await sha256File(filePath);
|
|
115
|
+
checksumEntries.push(`SHA256 (${relPath}) = ${hash}`);
|
|
116
|
+
}
|
|
117
|
+
await writeFile(join(packageDir, 'checksums.txt'), checksumEntries.join('\n') + '\n');
|
|
118
|
+
|
|
119
|
+
// Write package manifest (last — references everything)
|
|
120
|
+
const packageManifest = {
|
|
121
|
+
training_package_id: packageId,
|
|
122
|
+
created_at: new Date().toISOString(),
|
|
123
|
+
created_by: 'sdlab-training-package-v1',
|
|
124
|
+
training_manifest_id: manifestId,
|
|
125
|
+
training_profile_id: manifest.training_profile_id,
|
|
126
|
+
adapter_target: adapterTarget,
|
|
127
|
+
source_export_id: manifest.source_export_id,
|
|
128
|
+
source_snapshot_id: manifest.source_snapshot_id,
|
|
129
|
+
source_split_id: manifest.source_split_id,
|
|
130
|
+
config_fingerprint: manifest.config_fingerprint,
|
|
131
|
+
counts: {
|
|
132
|
+
total_records: totalRecords,
|
|
133
|
+
train: partitions.train.length,
|
|
134
|
+
val: partitions.val.length,
|
|
135
|
+
test: partitions.test.length,
|
|
136
|
+
images: totalImages,
|
|
137
|
+
checksums: checksumEntries.length,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
await writeFile(join(packageDir, 'manifest.json'), JSON.stringify(packageManifest, null, 2) + '\n');
|
|
141
|
+
|
|
142
|
+
// Write README
|
|
143
|
+
const readme = generatePackageReadme(packageManifest, profile, manifest, adapterTarget);
|
|
144
|
+
await writeFile(join(packageDir, 'README.md'), readme);
|
|
145
|
+
|
|
146
|
+
return { packageId, records: totalRecords, images: totalImages };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function generatePackageReadme(pkg, profile, manifest, adapter) {
|
|
150
|
+
return `# Training Package: ${pkg.training_package_id}
|
|
151
|
+
|
|
152
|
+
**Profile:** ${profile.label} (${profile.profile_id})
|
|
153
|
+
**Adapter:** ${adapter}
|
|
154
|
+
**Target:** ${profile.target_family} / ${profile.asset_type}
|
|
155
|
+
**Created:** ${pkg.created_at}
|
|
156
|
+
|
|
157
|
+
## Source chain
|
|
158
|
+
|
|
159
|
+
- Training manifest: \`${manifest.training_manifest_id}\`
|
|
160
|
+
- Export: \`${manifest.source_export_id}\`
|
|
161
|
+
- Snapshot: \`${manifest.source_snapshot_id}\`
|
|
162
|
+
- Split: \`${manifest.source_split_id}\`
|
|
163
|
+
- Config fingerprint: \`${manifest.config_fingerprint.slice(0, 16)}...\`
|
|
164
|
+
|
|
165
|
+
## Counts
|
|
166
|
+
|
|
167
|
+
| Partition | Records |
|
|
168
|
+
|-----------|---------|
|
|
169
|
+
| Train | ${pkg.counts.train} |
|
|
170
|
+
| Val | ${pkg.counts.val} |
|
|
171
|
+
| Test | ${pkg.counts.test} |
|
|
172
|
+
| **Total** | **${pkg.counts.total_records}** |
|
|
173
|
+
|
|
174
|
+
Images: ${pkg.counts.images}
|
|
175
|
+
|
|
176
|
+
## Checksums
|
|
177
|
+
|
|
178
|
+
All files in this package are checksummed in \`checksums.txt\` (BSD format).
|
|
179
|
+
|
|
180
|
+
## Provenance
|
|
181
|
+
|
|
182
|
+
This package was produced by [Style Dataset Lab](https://github.com/mcp-tool-shop-org/style-dataset-lab).
|
|
183
|
+
Every included record has been human-reviewed, canon-bound, and verified against project constitution rules.
|
|
184
|
+
The dataset split ensures no subject family appears in multiple partitions.
|
|
185
|
+
`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function collectFiles(dir) {
|
|
189
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
190
|
+
const files = [];
|
|
191
|
+
for (const entry of entries) {
|
|
192
|
+
const full = join(dir, entry.name);
|
|
193
|
+
if (entry.isDirectory()) {
|
|
194
|
+
files.push(...await collectFiles(full));
|
|
195
|
+
} else {
|
|
196
|
+
files.push(full);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return files;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* List all training packages.
|
|
204
|
+
*/
|
|
205
|
+
export async function listTrainingPackages(projectRoot) {
|
|
206
|
+
const dir = join(projectRoot, 'training', 'packages');
|
|
207
|
+
if (!existsSync(dir)) return [];
|
|
208
|
+
|
|
209
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
210
|
+
const packages = [];
|
|
211
|
+
|
|
212
|
+
for (const entry of entries) {
|
|
213
|
+
if (!entry.isDirectory() || !entry.name.startsWith('tp-')) continue;
|
|
214
|
+
const manifestPath = join(dir, entry.name, 'manifest.json');
|
|
215
|
+
if (!existsSync(manifestPath)) continue;
|
|
216
|
+
try {
|
|
217
|
+
const data = JSON.parse(await readFile(manifestPath, 'utf-8'));
|
|
218
|
+
packages.push({
|
|
219
|
+
id: data.training_package_id,
|
|
220
|
+
created_at: data.created_at,
|
|
221
|
+
profile: data.training_profile_id,
|
|
222
|
+
adapter: data.adapter_target,
|
|
223
|
+
records: data.counts.total_records,
|
|
224
|
+
images: data.counts.images,
|
|
225
|
+
});
|
|
226
|
+
} catch { /* skip */ }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return packages.sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Load a training package manifest.
|
|
234
|
+
*/
|
|
235
|
+
export async function loadTrainingPackage(projectRoot, packageId) {
|
|
236
|
+
const path = join(projectRoot, 'training', 'packages', packageId, 'manifest.json');
|
|
237
|
+
if (!existsSync(path)) {
|
|
238
|
+
throw new Error(`Training package "${packageId}" not found at ${path}`);
|
|
239
|
+
}
|
|
240
|
+
return JSON.parse(await readFile(path, 'utf-8'));
|
|
241
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Training profile management.
|
|
3
|
+
*
|
|
4
|
+
* A training profile defines how a project wants to turn an export package
|
|
5
|
+
* into a trainable model asset. Profiles never select data directly — they
|
|
6
|
+
* define eligibility rules and packaging expectations.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFile, readdir, writeFile, mkdir } from 'node:fs/promises';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
|
|
13
|
+
const REQUIRED_FIELDS = ['profile_id', 'label', 'target_family', 'asset_type', 'eligible_lanes', 'adapter_targets'];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Validate a training profile object.
|
|
17
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
18
|
+
*/
|
|
19
|
+
export function validateProfile(profile) {
|
|
20
|
+
const errors = [];
|
|
21
|
+
for (const field of REQUIRED_FIELDS) {
|
|
22
|
+
if (profile[field] === undefined || profile[field] === null) {
|
|
23
|
+
errors.push(`missing required field: ${field}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (profile.eligible_lanes && !Array.isArray(profile.eligible_lanes)) {
|
|
27
|
+
errors.push('eligible_lanes must be an array');
|
|
28
|
+
}
|
|
29
|
+
if (profile.adapter_targets && !Array.isArray(profile.adapter_targets)) {
|
|
30
|
+
errors.push('adapter_targets must be an array');
|
|
31
|
+
}
|
|
32
|
+
if (profile.adapter_targets?.length === 0) {
|
|
33
|
+
errors.push('adapter_targets must have at least one target');
|
|
34
|
+
}
|
|
35
|
+
return { valid: errors.length === 0, errors };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Load a training profile from a project.
|
|
40
|
+
*/
|
|
41
|
+
export async function loadTrainingProfile(projectRoot, profileId) {
|
|
42
|
+
const path = join(projectRoot, 'training', 'profiles', `${profileId}.json`);
|
|
43
|
+
if (!existsSync(path)) {
|
|
44
|
+
throw new Error(`Training profile "${profileId}" not found at ${path}`);
|
|
45
|
+
}
|
|
46
|
+
const data = JSON.parse(await readFile(path, 'utf-8'));
|
|
47
|
+
const { valid, errors } = validateProfile(data);
|
|
48
|
+
if (!valid) {
|
|
49
|
+
throw new Error(`Training profile "${profileId}" is invalid:\n ${errors.join('\n ')}`);
|
|
50
|
+
}
|
|
51
|
+
return data;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* List all training profiles in a project.
|
|
56
|
+
*/
|
|
57
|
+
export async function listTrainingProfiles(projectRoot) {
|
|
58
|
+
const dir = join(projectRoot, 'training', 'profiles');
|
|
59
|
+
if (!existsSync(dir)) return [];
|
|
60
|
+
|
|
61
|
+
const files = (await readdir(dir)).filter(f => f.endsWith('.json')).sort();
|
|
62
|
+
const profiles = [];
|
|
63
|
+
|
|
64
|
+
for (const file of files) {
|
|
65
|
+
try {
|
|
66
|
+
const data = JSON.parse(await readFile(join(dir, file), 'utf-8'));
|
|
67
|
+
profiles.push({
|
|
68
|
+
profile_id: data.profile_id,
|
|
69
|
+
label: data.label,
|
|
70
|
+
target_family: data.target_family,
|
|
71
|
+
asset_type: data.asset_type,
|
|
72
|
+
lanes: data.eligible_lanes?.length || 0,
|
|
73
|
+
adapters: data.adapter_targets || [],
|
|
74
|
+
});
|
|
75
|
+
} catch {
|
|
76
|
+
// Skip malformed
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return profiles;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Save a training profile to a project.
|
|
85
|
+
*/
|
|
86
|
+
export async function saveTrainingProfile(projectRoot, profile) {
|
|
87
|
+
const { valid, errors } = validateProfile(profile);
|
|
88
|
+
if (!valid) {
|
|
89
|
+
throw new Error(`Invalid training profile:\n ${errors.join('\n ')}`);
|
|
90
|
+
}
|
|
91
|
+
const dir = join(projectRoot, 'training', 'profiles');
|
|
92
|
+
await mkdir(dir, { recursive: true });
|
|
93
|
+
await writeFile(
|
|
94
|
+
join(dir, `${profile.profile_id}.json`),
|
|
95
|
+
JSON.stringify(profile, null, 2) + '\n'
|
|
96
|
+
);
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcptoolshop/style-dataset-lab",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "Visual canon and dataset production pipeline — snapshots, splits, versioned exports, eval packs, and canon-bound curation for any image production domain",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"sdlab": "./bin/sdlab.js"
|
|
@@ -28,16 +28,18 @@
|
|
|
28
28
|
],
|
|
29
29
|
"keywords": [
|
|
30
30
|
"dataset",
|
|
31
|
-
"
|
|
31
|
+
"computer-vision",
|
|
32
32
|
"training-data",
|
|
33
|
+
"image-dataset",
|
|
34
|
+
"evaluation",
|
|
35
|
+
"curation",
|
|
36
|
+
"canon",
|
|
37
|
+
"visual-development",
|
|
33
38
|
"concept-art",
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"comfyui"
|
|
38
|
-
"vlm",
|
|
39
|
-
"fine-tuning",
|
|
40
|
-
"multimodal"
|
|
39
|
+
"style-guide",
|
|
40
|
+
"lora",
|
|
41
|
+
"multimodal",
|
|
42
|
+
"comfyui"
|
|
41
43
|
],
|
|
42
44
|
"homepage": "https://mcp-tool-shop-org.github.io/style-dataset-lab/",
|
|
43
45
|
"repository": {
|
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
|
@@ -33,6 +33,14 @@ const REQUIRED_DIRS = [
|
|
|
33
33
|
'outputs/approved',
|
|
34
34
|
'outputs/rejected',
|
|
35
35
|
'exports',
|
|
36
|
+
'snapshots',
|
|
37
|
+
'splits',
|
|
38
|
+
'eval-packs',
|
|
39
|
+
'training/profiles',
|
|
40
|
+
'training/manifests',
|
|
41
|
+
'training/packages',
|
|
42
|
+
'training/eval-runs',
|
|
43
|
+
'training/implementations',
|
|
36
44
|
];
|
|
37
45
|
|
|
38
46
|
function pass(msg) { console.log(` \x1b[32m✓\x1b[0m ${msg}`); }
|
|
@@ -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
|
+
}
|