@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.
@@ -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
+ }