@mcptoolshop/style-dataset-lab 2.3.0 → 3.0.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 +178 -0
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +71 -0
- package/README.md +82 -9
- package/SECURITY.md +6 -5
- package/bin/sdlab.js +359 -11
- package/lib/adapters/comfyui-runner.js +170 -0
- package/lib/adapters/comfyui-workflows.js +163 -0
- package/lib/adapters/diffusers-lora.js +7 -2
- package/lib/adapters/generic-image-caption.js +6 -1
- package/lib/args.js +200 -13
- package/lib/batch-compiler.js +122 -0
- package/lib/batch-modes.js +111 -0
- package/lib/batch-runs.js +237 -0
- package/lib/batch-sheet-render.js +209 -0
- package/lib/brief-compiler.js +625 -0
- package/lib/brief-render.js +205 -0
- package/lib/card.js +11 -4
- package/lib/comfyui.js +100 -6
- package/lib/config.js +68 -3
- package/lib/critique-engine.js +413 -0
- package/lib/critique-render.js +171 -0
- package/lib/deprecation.js +1 -1
- package/lib/eval-pack.js +94 -4
- package/lib/eval-runs.js +42 -3
- package/lib/export.js +144 -34
- package/lib/generated-provenance.js +87 -0
- package/lib/implementation-packs.js +15 -1
- package/lib/log.js +41 -4
- package/lib/paths.js +72 -3
- package/lib/refine-briefs.js +336 -0
- package/lib/reingest-selected.js +184 -0
- package/lib/reingest.js +15 -1
- package/lib/run-summary.js +132 -0
- package/lib/runtime-runs.js +218 -0
- package/lib/selections.js +375 -0
- package/lib/snapshot.js +100 -7
- package/lib/split.js +175 -17
- package/lib/training-manifests.js +17 -3
- package/lib/training-packages.js +28 -14
- package/lib/workflow-profiles.js +161 -0
- package/package.json +13 -4
- package/scripts/batch-generate.js +176 -0
- package/scripts/batch-sheet.js +46 -0
- package/scripts/batch-show.js +86 -0
- package/scripts/brief-compile.js +134 -0
- package/scripts/brief-show.js +53 -0
- package/scripts/canon-bind.js +7 -9
- package/scripts/card.js +22 -25
- package/scripts/compare.js +38 -33
- package/scripts/critique-show.js +50 -0
- package/scripts/critique.js +70 -0
- package/scripts/curate.js +91 -46
- package/scripts/doctor.js +172 -6
- package/scripts/eligibility.js +17 -14
- package/scripts/eval-pack.js +13 -11
- package/scripts/eval-run.js +30 -29
- package/scripts/export.js +27 -31
- package/scripts/generate-controlnet.js +31 -17
- package/scripts/generate-identity.js +37 -20
- package/scripts/generate-ipadapter.js +22 -16
- package/scripts/generate.js +68 -13
- package/scripts/implementation-pack.js +19 -14
- package/scripts/init.js +137 -17
- package/scripts/painterly.js +8 -10
- package/scripts/refine.js +119 -0
- package/scripts/reingest-selected.js +78 -0
- package/scripts/reingest.js +37 -18
- package/scripts/run-generate.js +76 -0
- package/scripts/run-list.js +58 -0
- package/scripts/run-show.js +63 -0
- package/scripts/select.js +119 -0
- package/scripts/selection-show.js +88 -0
- package/scripts/snapshot.js +22 -24
- package/scripts/split.js +26 -31
- package/scripts/training-manifest.js +29 -24
- package/scripts/training-package.js +24 -20
- package/scripts/training-profile.js +13 -8
- package/scripts/workflow-list.js +61 -0
- package/scripts/workflow-show.js +97 -0
- package/templates/domains/architecture/inputs/prompts/example-wave.json +32 -0
- package/templates/domains/architecture/terminology.json +10 -8
- package/templates/domains/architecture/workflows/profiles/environment-moodboard.json +62 -0
- package/templates/domains/architecture/workflows/profiles/interior-variant-pack.json +60 -0
- package/templates/domains/character-design/inputs/prompts/example-wave.json +32 -0
- package/templates/domains/character-design/project.json +4 -2
- package/templates/domains/character-design/workflows/profiles/character-portrait-set.json +61 -0
- package/templates/domains/character-design/workflows/profiles/expression-sheet.json +59 -0
- package/templates/domains/creature-design/inputs/prompts/example-wave.json +32 -0
- package/templates/domains/creature-design/project.json +4 -2
- package/templates/domains/creature-design/terminology.json +1 -0
- package/templates/domains/creature-design/workflows/profiles/creature-portrait-set.json +61 -0
- package/templates/domains/creature-design/workflows/profiles/silhouette-exploration.json +58 -0
- package/templates/domains/game-art/inputs/prompts/example-wave.json +37 -0
- package/templates/domains/game-art/project.json +4 -2
- package/templates/domains/game-art/workflows/profiles/character-portrait-set.json +62 -0
- package/templates/domains/game-art/workflows/profiles/environment-moodboard.json +61 -0
- package/templates/domains/vehicle-mech/inputs/prompts/example-wave.json +37 -0
- package/templates/domains/vehicle-mech/project.json +4 -2
- package/templates/domains/vehicle-mech/workflows/profiles/variant-pack.json +60 -0
- package/templates/domains/vehicle-mech/workflows/profiles/vehicle-silhouette-pack.json +59 -0
- package/scripts/bulk-curate-wave2-5.js +0 -138
- package/scripts/bulk-curate-waves11-18.js +0 -413
- package/scripts/curate-wave25.js +0 -209
- package/scripts/migrate-records.js +0 -141
- package/templates/inputs/prompts/example-wave.json +0 -27
package/lib/reingest.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* canon-bound like everything else.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { readFile, writeFile, readdir, stat } from 'node:fs/promises';
|
|
10
|
+
import { readFile, writeFile, readdir, stat, mkdir, copyFile } from 'node:fs/promises';
|
|
11
11
|
import { join, basename, extname } from 'node:path';
|
|
12
12
|
import { existsSync } from 'node:fs';
|
|
13
13
|
import { loadTrainingManifest } from './training-manifests.js';
|
|
@@ -33,6 +33,12 @@ const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp']);
|
|
|
33
33
|
export async function reingestGenerated(projectRoot, sourcePath, manifestId, options = {}) {
|
|
34
34
|
const manifest = await loadTrainingManifest(projectRoot, manifestId);
|
|
35
35
|
const recordsDir = join(projectRoot, 'records');
|
|
36
|
+
// D-020: ensure outputs/candidates/ exists so the record's asset_path
|
|
37
|
+
// contract ("outputs/candidates/<file>") actually points at the image.
|
|
38
|
+
const candidatesDir = join(projectRoot, 'outputs', 'candidates');
|
|
39
|
+
if (!options.dryRun) {
|
|
40
|
+
await mkdir(candidatesDir, { recursive: true });
|
|
41
|
+
}
|
|
36
42
|
|
|
37
43
|
// Find image files in source directory
|
|
38
44
|
const entries = await readdir(sourcePath);
|
|
@@ -80,6 +86,14 @@ export async function reingestGenerated(projectRoot, sourcePath, manifestId, opt
|
|
|
80
86
|
record.schema_version = '2.1.0';
|
|
81
87
|
|
|
82
88
|
if (!options.dryRun) {
|
|
89
|
+
// D-020: copy the image into outputs/candidates/ so the record's
|
|
90
|
+
// project-relative asset_path contract is satisfied. Without this,
|
|
91
|
+
// downstream readers (export, adapters) would silently skip the
|
|
92
|
+
// record because the file does not exist at the declared path.
|
|
93
|
+
const destPath = join(candidatesDir, file);
|
|
94
|
+
if (!existsSync(destPath)) {
|
|
95
|
+
await copyFile(filePath, destPath);
|
|
96
|
+
}
|
|
83
97
|
await writeFile(recordPath, JSON.stringify(record, null, 2) + '\n');
|
|
84
98
|
}
|
|
85
99
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run summary — render manifest to human-readable summary files.
|
|
3
|
+
*
|
|
4
|
+
* Writes summary.md + summary.json to the run directory.
|
|
5
|
+
* Contact sheet generation is separate (requires image processing).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { writeFile } from 'node:fs/promises';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build a summary object from a manifest.
|
|
13
|
+
*/
|
|
14
|
+
export function buildRunSummary(manifest) {
|
|
15
|
+
const ok = manifest.outputs.filter(o => o.status === 'ok');
|
|
16
|
+
const failed = manifest.outputs.filter(o => o.status === 'error');
|
|
17
|
+
const totalElapsed = ok.reduce((sum, o) => sum + (o.elapsed_ms || 0), 0);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
run_id: manifest.run_id,
|
|
21
|
+
brief_id: manifest.brief_id,
|
|
22
|
+
project_id: manifest.project_id,
|
|
23
|
+
adapter_target: manifest.adapter_target,
|
|
24
|
+
output_mode: manifest.output_mode,
|
|
25
|
+
created_at: manifest.created_at,
|
|
26
|
+
dry_run: manifest.dry_run,
|
|
27
|
+
success_count: manifest.success_count,
|
|
28
|
+
error_count: manifest.error_count,
|
|
29
|
+
total_outputs: manifest.output_count,
|
|
30
|
+
total_elapsed_ms: totalElapsed,
|
|
31
|
+
avg_elapsed_ms: ok.length > 0 ? Math.round(totalElapsed / ok.length) : 0,
|
|
32
|
+
seed_plan: manifest.seed_plan,
|
|
33
|
+
checkpoint: manifest.checkpoint,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Render a manifest to Markdown summary.
|
|
39
|
+
*/
|
|
40
|
+
export function renderRunMarkdown(manifest) {
|
|
41
|
+
const lines = [];
|
|
42
|
+
lines.push(`# Run: ${manifest.run_id}`);
|
|
43
|
+
lines.push('');
|
|
44
|
+
lines.push(`**Brief:** ${manifest.brief_id}`);
|
|
45
|
+
lines.push(`**Project:** ${manifest.project_id}`);
|
|
46
|
+
lines.push(`**Created:** ${manifest.created_at}`);
|
|
47
|
+
lines.push(`**Adapter:** ${manifest.adapter_target}`);
|
|
48
|
+
lines.push(`**Mode:** ${manifest.output_mode}`);
|
|
49
|
+
if (manifest.dry_run) lines.push('**⚠ DRY RUN — no images generated**');
|
|
50
|
+
lines.push('');
|
|
51
|
+
|
|
52
|
+
// Results
|
|
53
|
+
lines.push('## Results');
|
|
54
|
+
lines.push('');
|
|
55
|
+
lines.push(`| # | Seed | Status | File | Time |`);
|
|
56
|
+
lines.push(`|---|------|--------|------|------|`);
|
|
57
|
+
for (const out of manifest.outputs) {
|
|
58
|
+
const file = out.filename || '—';
|
|
59
|
+
const time = out.elapsed_ms ? `${(out.elapsed_ms / 1000).toFixed(1)}s` : '—';
|
|
60
|
+
const status = out.status === 'ok' ? '✓' : out.status === 'dry_run' ? '○' : '✗';
|
|
61
|
+
lines.push(`| ${out.index + 1} | ${out.seed} | ${status} | ${file} | ${time} |`);
|
|
62
|
+
}
|
|
63
|
+
lines.push('');
|
|
64
|
+
|
|
65
|
+
// Errors
|
|
66
|
+
const errors = manifest.errors || [];
|
|
67
|
+
if (errors.length > 0) {
|
|
68
|
+
lines.push('## Errors');
|
|
69
|
+
lines.push('');
|
|
70
|
+
for (const err of errors) {
|
|
71
|
+
lines.push(`- **Image ${err.index + 1}** (seed ${err.seed}): ${err.error}`);
|
|
72
|
+
}
|
|
73
|
+
lines.push('');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Runtime
|
|
77
|
+
lines.push('## Runtime');
|
|
78
|
+
lines.push('');
|
|
79
|
+
lines.push(`| Parameter | Value |`);
|
|
80
|
+
lines.push(`|-----------|-------|`);
|
|
81
|
+
lines.push(`| Checkpoint | ${manifest.checkpoint || '—'} |`);
|
|
82
|
+
lines.push(`| LoRAs | ${(manifest.loras || []).map(l => l.name).join(', ') || '—'} |`);
|
|
83
|
+
lines.push(`| ComfyUI | ${manifest.comfy_url || '—'} |`);
|
|
84
|
+
|
|
85
|
+
const rp = manifest.runtime_plan || {};
|
|
86
|
+
lines.push(`| Size | ${rp.width || '?'}×${rp.height || '?'} |`);
|
|
87
|
+
lines.push(`| Steps | ${rp.steps || '?'} |`);
|
|
88
|
+
lines.push(`| CFG | ${rp.cfg || '?'} |`);
|
|
89
|
+
lines.push(`| Sampler | ${rp.sampler || '?'} |`);
|
|
90
|
+
lines.push(`| Seed mode | ${rp.seed_mode || '?'} |`);
|
|
91
|
+
lines.push(`| Base seed | ${manifest.seed_plan?.base_seed || '?'} |`);
|
|
92
|
+
lines.push('');
|
|
93
|
+
|
|
94
|
+
return lines.join('\n');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Render a manifest to terminal text.
|
|
99
|
+
*/
|
|
100
|
+
export function renderRunText(manifest) {
|
|
101
|
+
const ok = manifest.outputs.filter(o => o.status === 'ok' || o.status === 'dry_run');
|
|
102
|
+
const failed = manifest.outputs.filter(o => o.status === 'error');
|
|
103
|
+
const lines = [];
|
|
104
|
+
|
|
105
|
+
lines.push(`Run: ${manifest.run_id}`);
|
|
106
|
+
lines.push(`Brief: ${manifest.brief_id}`);
|
|
107
|
+
lines.push(`Project: ${manifest.project_id}`);
|
|
108
|
+
lines.push(`Mode: ${manifest.output_mode} (${manifest.adapter_target})`);
|
|
109
|
+
if (manifest.dry_run) lines.push('⚠ DRY RUN');
|
|
110
|
+
lines.push(`Results: ${ok.length}/${manifest.output_count} succeeded`);
|
|
111
|
+
if (failed.length > 0) lines.push(`Errors: ${failed.length}`);
|
|
112
|
+
lines.push('');
|
|
113
|
+
|
|
114
|
+
for (const out of manifest.outputs) {
|
|
115
|
+
const status = out.status === 'ok' ? '✓' : out.status === 'dry_run' ? '○' : '✗';
|
|
116
|
+
const time = out.elapsed_ms ? ` (${(out.elapsed_ms / 1000).toFixed(1)}s)` : '';
|
|
117
|
+
const file = out.filename ? ` → ${out.filename}` : '';
|
|
118
|
+
const err = out.error ? ` — ${out.error}` : '';
|
|
119
|
+
lines.push(` ${status} [${out.index + 1}] seed ${out.seed}${file}${time}${err}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return lines.join('\n');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Save summary files into the run directory.
|
|
127
|
+
*/
|
|
128
|
+
export async function saveRunSummary(runDir, manifest) {
|
|
129
|
+
const summary = buildRunSummary(manifest);
|
|
130
|
+
await writeFile(join(runDir, 'summary.json'), JSON.stringify(summary, null, 2) + '\n');
|
|
131
|
+
await writeFile(join(runDir, 'summary.md'), renderRunMarkdown(manifest) + '\n');
|
|
132
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime run management — create, load, list run artifacts.
|
|
3
|
+
*
|
|
4
|
+
* A run is a frozen execution artifact: brief + seeds + outputs + manifest.
|
|
5
|
+
* The run folder IS the review surface — humans open it directly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFile, writeFile, mkdir, readdir, copyFile, rename, unlink } from 'node:fs/promises';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { randomInt } from 'node:crypto';
|
|
12
|
+
import { getRunsDir, getBriefsDir } from './paths.js';
|
|
13
|
+
import { inputError } from './errors.js';
|
|
14
|
+
import { verbose } from './log.js';
|
|
15
|
+
|
|
16
|
+
// ─── Run ID generation ───────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate a run ID: run_YYYY-MM-DD_NNN
|
|
20
|
+
*/
|
|
21
|
+
// PB-008: tighten regex so stray directories like 'run_<date>_001_wip'
|
|
22
|
+
// don't inflate the sequence via parseInt's lenient parsing.
|
|
23
|
+
const RUN_SEQ_RE = /^(\d{3})$/;
|
|
24
|
+
|
|
25
|
+
export function generateRunId(runsDir) {
|
|
26
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
27
|
+
const prefix = `run_${today}_`;
|
|
28
|
+
|
|
29
|
+
let maxSeq = 0;
|
|
30
|
+
if (existsSync(runsDir)) {
|
|
31
|
+
const existing = readdirSync(runsDir, { withFileTypes: true })
|
|
32
|
+
.filter(e => e.isDirectory() && e.name.startsWith(prefix));
|
|
33
|
+
for (const e of existing) {
|
|
34
|
+
const seqStr = e.name.slice(prefix.length);
|
|
35
|
+
const match = RUN_SEQ_RE.exec(seqStr);
|
|
36
|
+
if (!match) continue;
|
|
37
|
+
const seq = parseInt(match[1], 10);
|
|
38
|
+
if (seq > maxSeq) maxSeq = seq;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const seq = String(maxSeq + 1).padStart(3, '0');
|
|
43
|
+
return `${prefix}${seq}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Seed planning ───────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build a deterministic seed plan.
|
|
50
|
+
* @param {Object} opts
|
|
51
|
+
* @param {'fixed'|'increment'|'random'} opts.seedMode
|
|
52
|
+
* @param {number} opts.outputCount
|
|
53
|
+
* @param {number} [opts.baseSeed] — explicit base seed (default: random)
|
|
54
|
+
* @returns {{ base_seed: number, seeds: number[] }}
|
|
55
|
+
*/
|
|
56
|
+
export function buildSeedPlan({ seedMode, outputCount, baseSeed }) {
|
|
57
|
+
const base = baseSeed ?? randomInt(2_147_483_647);
|
|
58
|
+
const seeds = [];
|
|
59
|
+
|
|
60
|
+
switch (seedMode) {
|
|
61
|
+
case 'fixed':
|
|
62
|
+
for (let i = 0; i < outputCount; i++) seeds.push(base);
|
|
63
|
+
break;
|
|
64
|
+
case 'increment':
|
|
65
|
+
for (let i = 0; i < outputCount; i++) seeds.push(base + i);
|
|
66
|
+
break;
|
|
67
|
+
case 'random':
|
|
68
|
+
seeds.push(base);
|
|
69
|
+
for (let i = 1; i < outputCount; i++) {
|
|
70
|
+
seeds.push(randomInt(2_147_483_647));
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
default:
|
|
74
|
+
// Default to increment
|
|
75
|
+
for (let i = 0; i < outputCount; i++) seeds.push(base + i);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { base_seed: base, seeds };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─── Run directory setup ─────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Prepare a run directory and copy brief artifacts into it.
|
|
85
|
+
* @returns {{ runId: string, runDir: string }}
|
|
86
|
+
*/
|
|
87
|
+
export async function prepareRunDir(projectRoot, brief) {
|
|
88
|
+
const runsDir = getRunsDir(projectRoot);
|
|
89
|
+
const runId = generateRunId(runsDir);
|
|
90
|
+
const runDir = join(runsDir, runId);
|
|
91
|
+
|
|
92
|
+
await mkdir(join(runDir, 'outputs'), { recursive: true });
|
|
93
|
+
|
|
94
|
+
// Copy brief artifacts
|
|
95
|
+
const briefsDir = getBriefsDir(projectRoot);
|
|
96
|
+
const briefJsonSrc = join(briefsDir, `${brief.brief_id}.json`);
|
|
97
|
+
const briefMdSrc = join(briefsDir, `${brief.brief_id}.md`);
|
|
98
|
+
|
|
99
|
+
await writeFile(join(runDir, 'brief.json'), JSON.stringify(brief, null, 2) + '\n');
|
|
100
|
+
|
|
101
|
+
if (existsSync(briefMdSrc)) {
|
|
102
|
+
await copyFile(briefMdSrc, join(runDir, 'brief.md'));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Write standalone prompt/negative files for easy inspection
|
|
106
|
+
await writeFile(join(runDir, 'prompt.txt'), brief.prompt || '');
|
|
107
|
+
await writeFile(join(runDir, 'negative-prompt.txt'), brief.negative_prompt || '');
|
|
108
|
+
|
|
109
|
+
return { runId, runDir };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── Manifest ────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Atomic write: write to a temp file in the same directory, then rename.
|
|
116
|
+
* Rename is atomic on the same filesystem, so a crash mid-write can never
|
|
117
|
+
* leave a half-written manifest in place of a good one.
|
|
118
|
+
*
|
|
119
|
+
* PB-002 / PB-005: callers may invoke this after each slot/candidate to
|
|
120
|
+
* produce incremental checkpoints.
|
|
121
|
+
*/
|
|
122
|
+
async function atomicWriteJson(targetPath, obj) {
|
|
123
|
+
const tmpPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
|
|
124
|
+
const body = JSON.stringify(obj, null, 2) + '\n';
|
|
125
|
+
try {
|
|
126
|
+
await writeFile(tmpPath, body);
|
|
127
|
+
await rename(tmpPath, targetPath);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
// Best-effort cleanup of the temp file on failure.
|
|
130
|
+
try { await unlink(tmpPath); } catch { /* ignore */ }
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Build and save a run manifest.
|
|
137
|
+
*
|
|
138
|
+
* PB-002: uses atomic write (temp + rename) so a crash mid-write cannot
|
|
139
|
+
* corrupt an existing manifest. Safe to call repeatedly for incremental
|
|
140
|
+
* checkpointing after each output completes.
|
|
141
|
+
*/
|
|
142
|
+
export async function saveRunManifest(runDir, manifest) {
|
|
143
|
+
const target = join(runDir, 'manifest.json');
|
|
144
|
+
await atomicWriteJson(target, manifest);
|
|
145
|
+
verbose(`saveRunManifest: wrote ${target}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Incremental checkpoint — same shape as saveRunManifest but stamps
|
|
150
|
+
* `incremental: true` and `last_checkpoint_at` so a human reading a
|
|
151
|
+
* partial manifest knows the run is in progress.
|
|
152
|
+
*
|
|
153
|
+
* PB-002: call after each completed output in long-running loops so a
|
|
154
|
+
* crash/interruption leaves a partial record instead of an orphan dir.
|
|
155
|
+
*/
|
|
156
|
+
export async function checkpointRunManifest(runDir, manifest) {
|
|
157
|
+
const snapshot = {
|
|
158
|
+
...manifest,
|
|
159
|
+
incremental: true,
|
|
160
|
+
last_checkpoint_at: new Date().toISOString(),
|
|
161
|
+
};
|
|
162
|
+
await atomicWriteJson(join(runDir, 'manifest.json'), snapshot);
|
|
163
|
+
verbose(`checkpointRunManifest: ${manifest.run_id} (${manifest.outputs?.length ?? 0} outputs)`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─── Load / List ─────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Load a run manifest by ID.
|
|
170
|
+
*/
|
|
171
|
+
export async function loadRun(projectRoot, runId) {
|
|
172
|
+
const runDir = join(getRunsDir(projectRoot), runId);
|
|
173
|
+
const manifestPath = join(runDir, 'manifest.json');
|
|
174
|
+
|
|
175
|
+
if (!existsSync(manifestPath)) {
|
|
176
|
+
throw inputError(
|
|
177
|
+
'RUN_NOT_FOUND',
|
|
178
|
+
`Run "${runId}" not found`,
|
|
179
|
+
`Check the run ID or run: sdlab run generate --brief <id>`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return JSON.parse(await readFile(manifestPath, 'utf-8'));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* List all runs in a project.
|
|
188
|
+
*/
|
|
189
|
+
export function listRuns(projectRoot) {
|
|
190
|
+
const runsDir = getRunsDir(projectRoot);
|
|
191
|
+
if (!existsSync(runsDir)) return [];
|
|
192
|
+
|
|
193
|
+
const entries = readdirSync(runsDir, { withFileTypes: true })
|
|
194
|
+
.filter(e => e.isDirectory() && e.name.startsWith('run_'))
|
|
195
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
196
|
+
|
|
197
|
+
const runs = [];
|
|
198
|
+
for (const entry of entries) {
|
|
199
|
+
const manifestPath = join(runsDir, entry.name, 'manifest.json');
|
|
200
|
+
if (!existsSync(manifestPath)) continue;
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
204
|
+
runs.push({
|
|
205
|
+
run_id: manifest.run_id,
|
|
206
|
+
brief_id: manifest.brief_id,
|
|
207
|
+
workflow_template_id: manifest.workflow_template_id,
|
|
208
|
+
output_count: manifest.output_count,
|
|
209
|
+
created_at: manifest.created_at,
|
|
210
|
+
adapter_target: manifest.adapter_target,
|
|
211
|
+
});
|
|
212
|
+
} catch {
|
|
213
|
+
// Skip malformed
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return runs;
|
|
218
|
+
}
|