@mcptoolshop/style-dataset-lab 2.1.0 → 2.2.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.
- package/CHANGELOG.md +24 -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 +80 -144
- package/README.pt-BR.md +137 -108
- package/README.zh.md +137 -108
- package/bin/sdlab.js +26 -0
- package/lib/adapters/diffusers-lora.js +110 -0
- package/lib/adapters/generic-image-caption.js +94 -0
- package/lib/eval-runs.js +285 -0
- package/lib/implementation-packs.js +316 -0
- package/lib/reingest.js +131 -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 +1 -1
- package/scripts/doctor.js +5 -0
- package/scripts/eval-run.js +121 -0
- package/scripts/implementation-pack.js +78 -0
- package/scripts/init.js +5 -0
- package/scripts/reingest.js +86 -0
- package/scripts/training-manifest.js +119 -0
- package/scripts/training-package.js +84 -0
- package/scripts/training-profile.js +63 -0
package/lib/eval-runs.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Eval run management.
|
|
3
|
+
*
|
|
4
|
+
* An eval run scores generated outputs against Phase 2 eval packs.
|
|
5
|
+
* Every eval run links back to a training manifest and eval pack —
|
|
6
|
+
* no orphan evals.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFile, writeFile, mkdir, readdir } from 'node:fs/promises';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { randomBytes } from 'node:crypto';
|
|
13
|
+
import { loadTrainingManifest } from './training-manifests.js';
|
|
14
|
+
import { loadEvalPack } from './eval-pack.js';
|
|
15
|
+
import { loadProjectConfig, detectLane, detectGroup } from './config.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate an eval run ID: er-YYYYMMDD-HHMMSS-XXXX
|
|
19
|
+
*/
|
|
20
|
+
function generateEvalRunId() {
|
|
21
|
+
const now = new Date();
|
|
22
|
+
const date = now.toISOString().replace(/[-:T]/g, '').slice(0, 14);
|
|
23
|
+
const rand = randomBytes(2).toString('hex');
|
|
24
|
+
return `er-${date.slice(0, 8)}-${date.slice(8, 14)}-${rand}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create an eval run — links a training manifest to an eval pack.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} projectRoot
|
|
31
|
+
* @param {string} manifestId — training manifest ID
|
|
32
|
+
* @param {string} evalPackId — eval pack ID
|
|
33
|
+
* @returns {Promise<{evalRunId: string}>}
|
|
34
|
+
*/
|
|
35
|
+
export async function createEvalRun(projectRoot, manifestId, evalPackId) {
|
|
36
|
+
// Verify both exist
|
|
37
|
+
const manifest = await loadTrainingManifest(projectRoot, manifestId);
|
|
38
|
+
const evalPack = await loadEvalPack(projectRoot, evalPackId);
|
|
39
|
+
|
|
40
|
+
const evalRunId = generateEvalRunId();
|
|
41
|
+
const evalRunDir = join(projectRoot, 'training', 'eval-runs', evalRunId);
|
|
42
|
+
await mkdir(evalRunDir, { recursive: true });
|
|
43
|
+
|
|
44
|
+
const runManifest = {
|
|
45
|
+
eval_run_id: evalRunId,
|
|
46
|
+
created_at: new Date().toISOString(),
|
|
47
|
+
created_by: 'sdlab-eval-run-v1',
|
|
48
|
+
training_manifest_id: manifestId,
|
|
49
|
+
eval_pack_id: evalPackId,
|
|
50
|
+
source_export_id: manifest.source_export_id,
|
|
51
|
+
training_profile_id: manifest.training_profile_id,
|
|
52
|
+
adapter_target: manifest.adapter_target,
|
|
53
|
+
status: 'created',
|
|
54
|
+
scorecard: null,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
await writeFile(join(evalRunDir, 'manifest.json'), JSON.stringify(runManifest, null, 2) + '\n');
|
|
58
|
+
|
|
59
|
+
return { evalRunId };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Score an eval run — compare generated outputs against eval pack tasks.
|
|
64
|
+
*
|
|
65
|
+
* Outputs are provided as a JSONL file where each line has:
|
|
66
|
+
* { record_id, generated_path, accept: true|false, notes? }
|
|
67
|
+
*
|
|
68
|
+
* @param {string} projectRoot
|
|
69
|
+
* @param {string} evalRunId
|
|
70
|
+
* @param {string} outputsPath — path to outputs index JSONL
|
|
71
|
+
* @returns {Promise<{scorecard: Object}>}
|
|
72
|
+
*/
|
|
73
|
+
export async function scoreEvalRun(projectRoot, evalRunId, outputsPath) {
|
|
74
|
+
const evalRunDir = join(projectRoot, 'training', 'eval-runs', evalRunId);
|
|
75
|
+
const runManifest = JSON.parse(await readFile(join(evalRunDir, 'manifest.json'), 'utf-8'));
|
|
76
|
+
const config = loadProjectConfig(projectRoot);
|
|
77
|
+
|
|
78
|
+
// Load outputs
|
|
79
|
+
const outputsRaw = await readFile(outputsPath, 'utf-8');
|
|
80
|
+
const outputs = outputsRaw.trim().split('\n').filter(Boolean).map(l => JSON.parse(l));
|
|
81
|
+
|
|
82
|
+
// Copy outputs index into eval run
|
|
83
|
+
await writeFile(join(evalRunDir, 'output-index.jsonl'), outputsRaw);
|
|
84
|
+
|
|
85
|
+
// Load eval pack tasks
|
|
86
|
+
const evalPackDir = join(projectRoot, 'eval-packs', runManifest.eval_pack_id);
|
|
87
|
+
const taskFiles = {
|
|
88
|
+
'lane-coverage': 'lane-coverage.jsonl',
|
|
89
|
+
'forbidden-drift': 'forbidden-drift.jsonl',
|
|
90
|
+
'anchor-gold': 'anchor-gold.jsonl',
|
|
91
|
+
'subject-continuity': 'subject-continuity.jsonl',
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const outputMap = new Map(outputs.map(o => [o.record_id, o]));
|
|
95
|
+
|
|
96
|
+
// Score each task type
|
|
97
|
+
const scores = {};
|
|
98
|
+
|
|
99
|
+
for (const [taskName, fileName] of Object.entries(taskFiles)) {
|
|
100
|
+
const taskPath = join(evalPackDir, fileName);
|
|
101
|
+
if (!existsSync(taskPath)) continue;
|
|
102
|
+
|
|
103
|
+
const taskEntries = (await readFile(taskPath, 'utf-8'))
|
|
104
|
+
.trim().split('\n').filter(Boolean).map(l => JSON.parse(l));
|
|
105
|
+
|
|
106
|
+
if (taskName === 'lane-coverage') {
|
|
107
|
+
scores.lane_coverage = scoreLaneCoverage(taskEntries, outputMap);
|
|
108
|
+
} else if (taskName === 'forbidden-drift') {
|
|
109
|
+
scores.forbidden_drift = scoreForbiddenDrift(taskEntries, outputMap);
|
|
110
|
+
} else if (taskName === 'anchor-gold') {
|
|
111
|
+
scores.anchor_gold = scoreAnchorGold(taskEntries, outputMap);
|
|
112
|
+
} else if (taskName === 'subject-continuity') {
|
|
113
|
+
scores.subject_continuity = scoreSubjectContinuity(taskEntries, outputMap);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Compute overall verdict
|
|
118
|
+
const allPassed = Object.values(scores).every(s => s.passed);
|
|
119
|
+
const overallScore = Object.values(scores).reduce((sum, s) => sum + (s.score || 0), 0) /
|
|
120
|
+
Math.max(Object.values(scores).length, 1);
|
|
121
|
+
|
|
122
|
+
const scorecard = {
|
|
123
|
+
eval_run_id: evalRunId,
|
|
124
|
+
scored_at: new Date().toISOString(),
|
|
125
|
+
overall_verdict: allPassed ? 'pass' : 'fail',
|
|
126
|
+
overall_score: +overallScore.toFixed(3),
|
|
127
|
+
output_count: outputs.length,
|
|
128
|
+
accepted_count: outputs.filter(o => o.accept).length,
|
|
129
|
+
rejected_count: outputs.filter(o => !o.accept).length,
|
|
130
|
+
tasks: scores,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Write scorecard
|
|
134
|
+
await writeFile(join(evalRunDir, 'scorecard.json'), JSON.stringify(scorecard, null, 2) + '\n');
|
|
135
|
+
|
|
136
|
+
// Write summary markdown
|
|
137
|
+
const summary = renderSummary(scorecard, runManifest);
|
|
138
|
+
await writeFile(join(evalRunDir, 'summary.md'), summary);
|
|
139
|
+
|
|
140
|
+
// Update manifest status
|
|
141
|
+
runManifest.status = 'scored';
|
|
142
|
+
runManifest.scorecard = scorecard;
|
|
143
|
+
await writeFile(join(evalRunDir, 'manifest.json'), JSON.stringify(runManifest, null, 2) + '\n');
|
|
144
|
+
|
|
145
|
+
return { scorecard };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function scoreLaneCoverage(tasks, outputMap) {
|
|
149
|
+
let covered = 0;
|
|
150
|
+
const lanes = new Set();
|
|
151
|
+
for (const task of tasks) {
|
|
152
|
+
lanes.add(task.lane);
|
|
153
|
+
const output = outputMap.get(task.record_id);
|
|
154
|
+
if (output?.accept) covered++;
|
|
155
|
+
}
|
|
156
|
+
const score = tasks.length > 0 ? covered / tasks.length : 0;
|
|
157
|
+
return {
|
|
158
|
+
task: 'lane_coverage',
|
|
159
|
+
total: tasks.length,
|
|
160
|
+
covered,
|
|
161
|
+
lanes_represented: lanes.size,
|
|
162
|
+
score: +score.toFixed(3),
|
|
163
|
+
passed: score >= 0.7,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function scoreForbiddenDrift(tasks, outputMap) {
|
|
168
|
+
// For forbidden drift, we check that rejected/borderline records are NOT accepted
|
|
169
|
+
let correctRejections = 0;
|
|
170
|
+
let falseAcceptances = 0;
|
|
171
|
+
for (const task of tasks) {
|
|
172
|
+
const output = outputMap.get(task.record_id);
|
|
173
|
+
if (!output || !output.accept) {
|
|
174
|
+
correctRejections++;
|
|
175
|
+
} else {
|
|
176
|
+
falseAcceptances++;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const score = tasks.length > 0 ? correctRejections / tasks.length : 1;
|
|
180
|
+
return {
|
|
181
|
+
task: 'forbidden_drift',
|
|
182
|
+
total: tasks.length,
|
|
183
|
+
correct_rejections: correctRejections,
|
|
184
|
+
false_acceptances: falseAcceptances,
|
|
185
|
+
score: +score.toFixed(3),
|
|
186
|
+
passed: falseAcceptances === 0,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function scoreAnchorGold(tasks, outputMap) {
|
|
191
|
+
let matched = 0;
|
|
192
|
+
for (const task of tasks) {
|
|
193
|
+
const output = outputMap.get(task.record_id);
|
|
194
|
+
if (output?.accept) matched++;
|
|
195
|
+
}
|
|
196
|
+
const score = tasks.length > 0 ? matched / tasks.length : 0;
|
|
197
|
+
return {
|
|
198
|
+
task: 'anchor_gold',
|
|
199
|
+
total: tasks.length,
|
|
200
|
+
matched,
|
|
201
|
+
score: +score.toFixed(3),
|
|
202
|
+
passed: score >= 0.8,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function scoreSubjectContinuity(tasks, outputMap) {
|
|
207
|
+
let subjectsWithCoverage = 0;
|
|
208
|
+
for (const task of tasks) {
|
|
209
|
+
const recordIds = task.record_ids || [];
|
|
210
|
+
const accepted = recordIds.filter(id => outputMap.get(id)?.accept).length;
|
|
211
|
+
if (accepted >= 2) subjectsWithCoverage++;
|
|
212
|
+
}
|
|
213
|
+
const score = tasks.length > 0 ? subjectsWithCoverage / tasks.length : 0;
|
|
214
|
+
return {
|
|
215
|
+
task: 'subject_continuity',
|
|
216
|
+
total_subjects: tasks.length,
|
|
217
|
+
subjects_with_coverage: subjectsWithCoverage,
|
|
218
|
+
score: +score.toFixed(3),
|
|
219
|
+
passed: score >= 0.6,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function renderSummary(scorecard, manifest) {
|
|
224
|
+
const lines = [];
|
|
225
|
+
lines.push(`# Eval Run: ${scorecard.eval_run_id}`);
|
|
226
|
+
lines.push('');
|
|
227
|
+
lines.push(`**Training manifest:** ${manifest.training_manifest_id}`);
|
|
228
|
+
lines.push(`**Eval pack:** ${manifest.eval_pack_id}`);
|
|
229
|
+
lines.push(`**Scored:** ${scorecard.scored_at}`);
|
|
230
|
+
lines.push(`**Verdict:** ${scorecard.overall_verdict.toUpperCase()}`);
|
|
231
|
+
lines.push(`**Overall score:** ${(scorecard.overall_score * 100).toFixed(1)}%`);
|
|
232
|
+
lines.push('');
|
|
233
|
+
lines.push(`**Outputs:** ${scorecard.output_count} total, ${scorecard.accepted_count} accepted, ${scorecard.rejected_count} rejected`);
|
|
234
|
+
lines.push('');
|
|
235
|
+
lines.push('## Task scores');
|
|
236
|
+
lines.push('');
|
|
237
|
+
lines.push('| Task | Score | Passed |');
|
|
238
|
+
lines.push('|------|-------|--------|');
|
|
239
|
+
for (const [name, task] of Object.entries(scorecard.tasks)) {
|
|
240
|
+
lines.push(`| ${name} | ${(task.score * 100).toFixed(1)}% | ${task.passed ? 'YES' : 'NO'} |`);
|
|
241
|
+
}
|
|
242
|
+
lines.push('');
|
|
243
|
+
return lines.join('\n');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Load an eval run manifest.
|
|
248
|
+
*/
|
|
249
|
+
export async function loadEvalRun(projectRoot, evalRunId) {
|
|
250
|
+
const path = join(projectRoot, 'training', 'eval-runs', evalRunId, 'manifest.json');
|
|
251
|
+
if (!existsSync(path)) {
|
|
252
|
+
throw new Error(`Eval run "${evalRunId}" not found at ${path}`);
|
|
253
|
+
}
|
|
254
|
+
return JSON.parse(await readFile(path, 'utf-8'));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* List all eval runs.
|
|
259
|
+
*/
|
|
260
|
+
export async function listEvalRuns(projectRoot) {
|
|
261
|
+
const dir = join(projectRoot, 'training', 'eval-runs');
|
|
262
|
+
if (!existsSync(dir)) return [];
|
|
263
|
+
|
|
264
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
265
|
+
const runs = [];
|
|
266
|
+
|
|
267
|
+
for (const entry of entries) {
|
|
268
|
+
if (!entry.isDirectory() || !entry.name.startsWith('er-')) continue;
|
|
269
|
+
const manifestPath = join(dir, entry.name, 'manifest.json');
|
|
270
|
+
if (!existsSync(manifestPath)) continue;
|
|
271
|
+
try {
|
|
272
|
+
const data = JSON.parse(await readFile(manifestPath, 'utf-8'));
|
|
273
|
+
runs.push({
|
|
274
|
+
id: data.eval_run_id,
|
|
275
|
+
created_at: data.created_at,
|
|
276
|
+
status: data.status,
|
|
277
|
+
manifest: data.training_manifest_id,
|
|
278
|
+
eval_pack: data.eval_pack_id,
|
|
279
|
+
verdict: data.scorecard?.overall_verdict || null,
|
|
280
|
+
});
|
|
281
|
+
} catch { /* skip */ }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return runs.sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
285
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Implementation example packs.
|
|
3
|
+
*
|
|
4
|
+
* Shows how to use the trained asset in practice: prompt examples,
|
|
5
|
+
* lane-targeted examples, subject continuity examples, known failure
|
|
6
|
+
* cases, and re-ingest guidance.
|
|
7
|
+
*
|
|
8
|
+
* Every implementation pack is tied to a training manifest.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFile, writeFile, mkdir, readdir } from 'node:fs/promises';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
14
|
+
import { randomBytes } from 'node:crypto';
|
|
15
|
+
import { loadTrainingManifest } from './training-manifests.js';
|
|
16
|
+
import { loadTrainingProfile } from './training-profiles.js';
|
|
17
|
+
import { loadProjectConfig, detectLane, detectGroup } from './config.js';
|
|
18
|
+
import { loadSplitPartition } from './split.js';
|
|
19
|
+
import { loadRecord } from './records.js';
|
|
20
|
+
|
|
21
|
+
function generateImplId() {
|
|
22
|
+
const now = new Date();
|
|
23
|
+
const date = now.toISOString().replace(/[-:T]/g, '').slice(0, 14);
|
|
24
|
+
const rand = randomBytes(2).toString('hex');
|
|
25
|
+
return `impl-${date.slice(0, 8)}-${date.slice(8, 14)}-${rand}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build an implementation pack from a training manifest.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} projectRoot
|
|
32
|
+
* @param {string} manifestId
|
|
33
|
+
* @returns {Promise<{implId: string, prompts: number, failures: number, subjects: number}>}
|
|
34
|
+
*/
|
|
35
|
+
export async function buildImplementationPack(projectRoot, manifestId) {
|
|
36
|
+
const manifest = await loadTrainingManifest(projectRoot, manifestId);
|
|
37
|
+
const profile = await loadTrainingProfile(projectRoot, manifest.training_profile_id);
|
|
38
|
+
const config = loadProjectConfig(projectRoot);
|
|
39
|
+
const recordsDir = join(projectRoot, 'records');
|
|
40
|
+
|
|
41
|
+
// Load test partition records (best for implementation examples)
|
|
42
|
+
const testEntries = await loadSplitPartition(projectRoot, manifest.source_split_id, 'test');
|
|
43
|
+
|
|
44
|
+
// Also load some train records for broader coverage
|
|
45
|
+
const trainEntries = await loadSplitPartition(projectRoot, manifest.source_split_id, 'train');
|
|
46
|
+
|
|
47
|
+
// Build prompt examples — per-lane best records
|
|
48
|
+
const promptExamples = [];
|
|
49
|
+
const laneExamples = new Map();
|
|
50
|
+
|
|
51
|
+
for (const entry of [...testEntries, ...trainEntries.slice(0, 50)]) {
|
|
52
|
+
const record = await loadRecord(recordsDir, entry.record_id);
|
|
53
|
+
if (!record) continue;
|
|
54
|
+
if (record.judgment?.status !== 'approved') continue;
|
|
55
|
+
|
|
56
|
+
const prompt = record.provenance?.prompt || '';
|
|
57
|
+
const lane = detectLane(record.id, prompt, config.lanes);
|
|
58
|
+
const group = detectGroup(record.id, prompt, config.terminology);
|
|
59
|
+
|
|
60
|
+
// Filter by profile lanes
|
|
61
|
+
if (profile.eligible_lanes.length > 0 && !profile.eligible_lanes.includes(lane)) continue;
|
|
62
|
+
|
|
63
|
+
if (!laneExamples.has(lane)) laneExamples.set(lane, []);
|
|
64
|
+
if (laneExamples.get(lane).length < 3) {
|
|
65
|
+
laneExamples.get(lane).push(record);
|
|
66
|
+
|
|
67
|
+
const passRatio = record.canon?.assertion_count > 0
|
|
68
|
+
? record.canon.pass_count / record.canon.assertion_count : 0;
|
|
69
|
+
|
|
70
|
+
promptExamples.push({
|
|
71
|
+
record_id: record.id,
|
|
72
|
+
lane,
|
|
73
|
+
group: group || 'unknown',
|
|
74
|
+
prompt: prompt.slice(0, 500),
|
|
75
|
+
pass_ratio: +passRatio.toFixed(3),
|
|
76
|
+
asset_path: record.asset_path,
|
|
77
|
+
usage_note: `Use as ${lane} reference for ${group || 'general'} style`,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Build known failure cases from rejected records
|
|
83
|
+
const knownFailures = [];
|
|
84
|
+
const allFiles = (await readdir(recordsDir)).filter(f => f.endsWith('.json')).sort();
|
|
85
|
+
let failCount = 0;
|
|
86
|
+
|
|
87
|
+
for (const file of allFiles) {
|
|
88
|
+
if (failCount >= 10) break;
|
|
89
|
+
const record = JSON.parse(await readFile(join(recordsDir, file), 'utf-8'));
|
|
90
|
+
if (record.judgment?.status !== 'rejected') continue;
|
|
91
|
+
|
|
92
|
+
const prompt = record.provenance?.prompt || '';
|
|
93
|
+
const lane = detectLane(record.id, prompt, config.lanes);
|
|
94
|
+
if (profile.eligible_lanes.length > 0 && !profile.eligible_lanes.includes(lane)) continue;
|
|
95
|
+
|
|
96
|
+
const failureModes = record.judgment?.failure_modes || [];
|
|
97
|
+
if (failureModes.length === 0 && !record.judgment?.explanation) continue;
|
|
98
|
+
|
|
99
|
+
knownFailures.push({
|
|
100
|
+
record_id: record.id,
|
|
101
|
+
lane,
|
|
102
|
+
failure_modes: failureModes,
|
|
103
|
+
explanation: record.judgment?.explanation || null,
|
|
104
|
+
lesson: `Avoid: ${failureModes.join(', ') || record.judgment?.explanation?.slice(0, 100)}`,
|
|
105
|
+
});
|
|
106
|
+
failCount++;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Build subject continuity examples
|
|
110
|
+
const subjectExamples = [];
|
|
111
|
+
const bySubject = new Map();
|
|
112
|
+
|
|
113
|
+
for (const entry of [...testEntries, ...trainEntries]) {
|
|
114
|
+
const record = await loadRecord(recordsDir, entry.record_id);
|
|
115
|
+
if (!record?.identity?.subject_name) continue;
|
|
116
|
+
const name = record.identity.subject_name;
|
|
117
|
+
if (!bySubject.has(name)) bySubject.set(name, []);
|
|
118
|
+
bySubject.get(name).push(record);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const [name, records] of [...bySubject].sort((a, b) => b[1].length - a[1].length).slice(0, 5)) {
|
|
122
|
+
subjectExamples.push({
|
|
123
|
+
subject_name: name,
|
|
124
|
+
record_count: records.length,
|
|
125
|
+
record_ids: records.map(r => r.id).sort(),
|
|
126
|
+
faction: records[0].identity?.faction || null,
|
|
127
|
+
note: `${records.length} views of ${name} — use to verify identity consistency`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Build expected behaviors
|
|
132
|
+
const expectedBehaviors = {
|
|
133
|
+
style_markers: buildStyleMarkers(profile, config),
|
|
134
|
+
forbidden_patterns: buildForbiddenPatterns(config),
|
|
135
|
+
quality_thresholds: config.rubric.thresholds || {},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Write implementation pack
|
|
139
|
+
const implId = generateImplId();
|
|
140
|
+
const implDir = join(projectRoot, 'training', 'implementations', implId);
|
|
141
|
+
await mkdir(implDir, { recursive: true });
|
|
142
|
+
|
|
143
|
+
const implManifest = {
|
|
144
|
+
implementation_pack_id: implId,
|
|
145
|
+
created_at: new Date().toISOString(),
|
|
146
|
+
created_by: 'sdlab-implementation-v1',
|
|
147
|
+
training_manifest_id: manifestId,
|
|
148
|
+
training_profile_id: manifest.training_profile_id,
|
|
149
|
+
counts: {
|
|
150
|
+
prompt_examples: promptExamples.length,
|
|
151
|
+
known_failures: knownFailures.length,
|
|
152
|
+
subject_examples: subjectExamples.length,
|
|
153
|
+
lanes_covered: laneExamples.size,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
await writeFile(join(implDir, 'manifest.json'), JSON.stringify(implManifest, null, 2) + '\n');
|
|
158
|
+
await writeFile(join(implDir, 'prompts.jsonl'), promptExamples.map(p => JSON.stringify(p)).join('\n') + '\n');
|
|
159
|
+
await writeFile(join(implDir, 'known-failures.json'), JSON.stringify(knownFailures, null, 2) + '\n');
|
|
160
|
+
await writeFile(join(implDir, 'expected-behaviors.json'), JSON.stringify(expectedBehaviors, null, 2) + '\n');
|
|
161
|
+
|
|
162
|
+
if (subjectExamples.length > 0) {
|
|
163
|
+
await writeFile(join(implDir, 'subject-continuity.json'), JSON.stringify(subjectExamples, null, 2) + '\n');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Write eval tasks JSONL — tasks that can be run against generated outputs
|
|
167
|
+
const evalTasks = promptExamples.map(p => ({
|
|
168
|
+
task: 'generate_and_compare',
|
|
169
|
+
prompt: p.prompt,
|
|
170
|
+
lane: p.lane,
|
|
171
|
+
reference_record: p.record_id,
|
|
172
|
+
expected: 'style-consistent output matching canon',
|
|
173
|
+
}));
|
|
174
|
+
await writeFile(join(implDir, 'eval-tasks.jsonl'), evalTasks.map(t => JSON.stringify(t)).join('\n') + '\n');
|
|
175
|
+
|
|
176
|
+
// Write reingest guide
|
|
177
|
+
const reingestGuide = generateReingestGuide(profile, manifest);
|
|
178
|
+
await writeFile(join(implDir, 'reingest-guide.md'), reingestGuide);
|
|
179
|
+
|
|
180
|
+
// Write README
|
|
181
|
+
const readme = generateImplReadme(implManifest, profile, manifest);
|
|
182
|
+
await writeFile(join(implDir, 'README.md'), readme);
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
implId,
|
|
186
|
+
prompts: promptExamples.length,
|
|
187
|
+
failures: knownFailures.length,
|
|
188
|
+
subjects: subjectExamples.length,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function buildStyleMarkers(profile, config) {
|
|
193
|
+
const markers = [];
|
|
194
|
+
const constitution = config.constitution;
|
|
195
|
+
if (constitution?.rules) {
|
|
196
|
+
for (const rule of constitution.rules.slice(0, 10)) {
|
|
197
|
+
markers.push({ rule_id: rule.id, summary: rule.summary || rule.id });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return markers;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function buildForbiddenPatterns(config) {
|
|
204
|
+
const forbidden = [];
|
|
205
|
+
const rubric = config.rubric;
|
|
206
|
+
if (rubric?.failure_to_rules) {
|
|
207
|
+
for (const [mode, rules] of Object.entries(rubric.failure_to_rules)) {
|
|
208
|
+
forbidden.push({ failure_mode: mode, linked_rules: rules });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return forbidden;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function generateReingestGuide(profile, manifest) {
|
|
215
|
+
return `# Re-ingest Guide
|
|
216
|
+
|
|
217
|
+
## When to re-ingest
|
|
218
|
+
|
|
219
|
+
After using the trained ${profile.asset_type} (${profile.label}) to generate new images,
|
|
220
|
+
accepted outputs should be re-ingested into the project as new records.
|
|
221
|
+
|
|
222
|
+
## How to re-ingest
|
|
223
|
+
|
|
224
|
+
\`\`\`bash
|
|
225
|
+
sdlab reingest generated --project <name> --source <outputs-dir>
|
|
226
|
+
\`\`\`
|
|
227
|
+
|
|
228
|
+
## Rules
|
|
229
|
+
|
|
230
|
+
1. Generated outputs enter as new records with \`provenance.source: "generated"\`
|
|
231
|
+
2. They must go through normal curation (\`sdlab curate\`)
|
|
232
|
+
3. They must be canon-bound (\`sdlab bind\`)
|
|
233
|
+
4. No bypass around review — generated work is judged like everything else
|
|
234
|
+
5. Accepted re-ingested records become eligible for future snapshots
|
|
235
|
+
|
|
236
|
+
## Provenance fields
|
|
237
|
+
|
|
238
|
+
Re-ingested records carry:
|
|
239
|
+
|
|
240
|
+
- \`provenance.source\`: \`"generated"\`
|
|
241
|
+
- \`provenance.training_manifest_id\`: \`"${manifest.training_manifest_id}"\`
|
|
242
|
+
- \`provenance.training_profile_id\`: \`"${manifest.training_profile_id}"\`
|
|
243
|
+
- \`provenance.adapter_target\`: \`"${manifest.adapter_target}"\`
|
|
244
|
+
- \`provenance.base_model\`: \`"${manifest.base_model || '(not specified)'}"\`
|
|
245
|
+
`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function generateImplReadme(impl, profile, manifest) {
|
|
249
|
+
return `# Implementation Pack: ${impl.implementation_pack_id}
|
|
250
|
+
|
|
251
|
+
**Profile:** ${profile.label} (${profile.profile_id})
|
|
252
|
+
**Training manifest:** ${manifest.training_manifest_id}
|
|
253
|
+
**Created:** ${impl.created_at}
|
|
254
|
+
|
|
255
|
+
## Contents
|
|
256
|
+
|
|
257
|
+
| File | Description |
|
|
258
|
+
|------|-------------|
|
|
259
|
+
| prompts.jsonl | ${impl.counts.prompt_examples} prompt examples across ${impl.counts.lanes_covered} lanes |
|
|
260
|
+
| known-failures.json | ${impl.counts.known_failures} documented failure cases to avoid |
|
|
261
|
+
| expected-behaviors.json | Style markers and forbidden patterns from constitution |
|
|
262
|
+
| subject-continuity.json | ${impl.counts.subject_examples} named-subject groups for identity testing |
|
|
263
|
+
| eval-tasks.jsonl | Generate-and-compare tasks for automated evaluation |
|
|
264
|
+
| reingest-guide.md | How to re-ingest accepted generated outputs |
|
|
265
|
+
|
|
266
|
+
## Usage
|
|
267
|
+
|
|
268
|
+
1. Load the trained ${profile.asset_type} with the prompt strategy from the profile
|
|
269
|
+
2. Use prompts.jsonl as starting points for each lane
|
|
270
|
+
3. Check generated outputs against expected-behaviors.json
|
|
271
|
+
4. Compare with known-failures.json to catch common drift
|
|
272
|
+
5. For named subjects, verify against subject-continuity.json
|
|
273
|
+
6. Re-ingest accepted outputs per reingest-guide.md
|
|
274
|
+
`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* List all implementation packs.
|
|
279
|
+
*/
|
|
280
|
+
export async function listImplementationPacks(projectRoot) {
|
|
281
|
+
const dir = join(projectRoot, 'training', 'implementations');
|
|
282
|
+
if (!existsSync(dir)) return [];
|
|
283
|
+
|
|
284
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
285
|
+
const packs = [];
|
|
286
|
+
|
|
287
|
+
for (const entry of entries) {
|
|
288
|
+
if (!entry.isDirectory() || !entry.name.startsWith('impl-')) continue;
|
|
289
|
+
const manifestPath = join(dir, entry.name, 'manifest.json');
|
|
290
|
+
if (!existsSync(manifestPath)) continue;
|
|
291
|
+
try {
|
|
292
|
+
const data = JSON.parse(await readFile(manifestPath, 'utf-8'));
|
|
293
|
+
packs.push({
|
|
294
|
+
id: data.implementation_pack_id,
|
|
295
|
+
created_at: data.created_at,
|
|
296
|
+
manifest: data.training_manifest_id,
|
|
297
|
+
profile: data.training_profile_id,
|
|
298
|
+
prompts: data.counts.prompt_examples,
|
|
299
|
+
failures: data.counts.known_failures,
|
|
300
|
+
});
|
|
301
|
+
} catch { /* skip */ }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return packs.sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Load an implementation pack manifest.
|
|
309
|
+
*/
|
|
310
|
+
export async function loadImplementationPack(projectRoot, implId) {
|
|
311
|
+
const path = join(projectRoot, 'training', 'implementations', implId, 'manifest.json');
|
|
312
|
+
if (!existsSync(path)) {
|
|
313
|
+
throw new Error(`Implementation pack "${implId}" not found at ${path}`);
|
|
314
|
+
}
|
|
315
|
+
return JSON.parse(await readFile(path, 'utf-8'));
|
|
316
|
+
}
|