@mcptoolshop/style-dataset-lab 3.2.0 → 3.4.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 +58 -1
- package/README.es.md +40 -19
- package/README.fr.md +74 -53
- package/README.hi.md +67 -46
- package/README.it.md +42 -21
- package/README.ja.md +44 -23
- package/README.md +25 -4
- package/README.pt-BR.md +42 -21
- package/README.zh.md +66 -45
- package/bin/sdlab.js +51 -4
- package/lib/adapters/ai-toolkit.js +37 -5
- package/lib/adapters/comfyui-runner.js +114 -27
- package/lib/adapters/comfyui-workflows.js +10 -2
- package/lib/adapters/diffusers-lora.js +37 -5
- package/lib/adapters/generic-image-caption.js +43 -5
- package/lib/args.js +16 -1
- package/lib/batch-modes.js +29 -8
- package/lib/batch-runs.js +29 -1
- package/lib/batch-sheet-render.js +13 -3
- package/lib/brief-compiler.js +19 -0
- package/lib/candidate-sheet.js +327 -0
- package/lib/canon-build/build.js +146 -93
- package/lib/captions.js +57 -0
- package/lib/card.js +74 -2
- package/lib/comfyui.js +116 -18
- package/lib/config.js +116 -12
- package/lib/critique-engine.js +82 -3
- package/lib/critique-render.js +45 -0
- package/lib/eligibility.js +121 -9
- package/lib/eval-pack.js +14 -14
- package/lib/eval-runs.js +33 -0
- package/lib/export.js +122 -34
- package/lib/freeze-gate.js +69 -16
- package/lib/freeze-stamp.js +28 -3
- package/lib/generated-provenance.js +16 -1
- package/lib/implementation-packs.js +95 -17
- package/lib/ingest.js +396 -0
- package/lib/judgment-provenance.js +220 -0
- package/lib/measure.js +422 -0
- package/lib/paths.js +196 -7
- package/lib/reingest-selected.js +98 -16
- package/lib/run-manifest.js +394 -0
- package/lib/run-summary.js +12 -6
- package/lib/runtime-runs.js +45 -4
- package/lib/snapshot.js +121 -16
- package/lib/split.js +364 -48
- package/lib/training-manifests.js +114 -21
- package/lib/training-packages.js +147 -15
- package/lib/verifier.js +67 -0
- package/lib/workflow-profiles.js +24 -3
- package/package.json +3 -2
- package/runtime/comfyui/txt2img-qwen.json +38 -0
- package/runtime/comfyui/txt2img-standard.json +21 -0
- package/scripts/_resume.js +62 -0
- package/scripts/_shared-args.js +44 -0
- package/scripts/brief-compile.js +2 -1
- package/scripts/brief-show.js +2 -1
- package/scripts/canon-bind.js +2 -2
- package/scripts/canon-build.js +27 -20
- package/scripts/canon-drift.js +16 -7
- package/scripts/canon-freeze-status.js +14 -6
- package/scripts/canon-freeze.js +27 -11
- package/scripts/canon-unfreeze.js +17 -9
- package/scripts/card.js +2 -2
- package/scripts/compare.js +2 -2
- package/scripts/critique.js +48 -8
- package/scripts/curate.js +23 -2
- package/scripts/doctor.js +181 -55
- package/scripts/eligibility.js +42 -2
- package/scripts/eval-pack.js +2 -2
- package/scripts/eval-run.js +2 -2
- package/scripts/export.js +2 -2
- package/scripts/generate-controlnet.js +45 -25
- package/scripts/generate-identity.js +53 -23
- package/scripts/generate-ipadapter.js +44 -23
- package/scripts/generate.js +72 -5
- package/scripts/implementation-pack.js +2 -2
- package/scripts/ingest.js +145 -0
- package/scripts/init.js +73 -13
- package/scripts/measure.js +170 -0
- package/scripts/measure_image.py +458 -0
- package/scripts/migrate-records.js +142 -0
- package/scripts/painterly-test.js +4 -3
- package/scripts/painterly.js +7 -3
- package/scripts/qwen_generate.py +334 -13
- package/scripts/reingest.js +2 -2
- package/scripts/run-generate.js +3 -0
- package/scripts/sheet.js +142 -0
- package/scripts/snapshot.js +2 -2
- package/scripts/split.js +2 -2
- package/scripts/training-manifest.js +2 -2
- package/scripts/training-package.js +2 -2
- package/scripts/training-profile.js +2 -2
- package/scripts/workflow-list.js +2 -1
- package/scripts/workflow-show.js +2 -1
package/lib/snapshot.js
CHANGED
|
@@ -19,8 +19,14 @@ import { warn } from './log.js';
|
|
|
19
19
|
* snapshot/split/export/eval-pack/eval-run/training-manifest/training-package/
|
|
20
20
|
* impl-pack stamps this version; readers log a WARNING on version mismatch
|
|
21
21
|
* (best-effort compatibility).
|
|
22
|
+
*
|
|
23
|
+
* 2.3.0 (v3.3.0): PIN_PER_STEP — the ComfyUI run manifest now also stamps this
|
|
24
|
+
* version and carries a `pinning` block (comfy_workflow_sha + model/LoRA content
|
|
25
|
+
* hashes + seed_policy; see lib/run-manifest.js). The block is additive and
|
|
26
|
+
* optional, so a legacy manifest with no schema_version / no pinning still loads
|
|
27
|
+
* (checkManifestVersion only warns; it never throws).
|
|
22
28
|
*/
|
|
23
|
-
export const SCHEMA_VERSION = '2.
|
|
29
|
+
export const SCHEMA_VERSION = '2.3.0';
|
|
24
30
|
|
|
25
31
|
/**
|
|
26
32
|
* Warn if a loaded manifest's schema_version does not match the reader.
|
|
@@ -35,6 +41,96 @@ export function checkManifestVersion(manifest, kind) {
|
|
|
35
41
|
}
|
|
36
42
|
}
|
|
37
43
|
|
|
44
|
+
/**
|
|
45
|
+
* SDL-M10: atomically claim a fresh, unused SUBDIRECTORY of `parentDir`.
|
|
46
|
+
*
|
|
47
|
+
* The pattern this replaces — `existsSync(dir)` then throw-if-true,
|
|
48
|
+
* followed later by `mkdir(dir, {recursive:true})` — is check-then-act: a
|
|
49
|
+
* second concurrent process can create the same directory in the window
|
|
50
|
+
* between the check and the mkdir, and recursive mkdir never throws on an
|
|
51
|
+
* already-existing directory, so the race is silently invisible (two
|
|
52
|
+
* builders interleave writes into one snapshot/split/export/etc. dir —
|
|
53
|
+
* Law 1 exposure: "snapshots frozen").
|
|
54
|
+
*
|
|
55
|
+
* Mirrors lib/selections.js's claimSelectionId: a non-recursive `mkdir`
|
|
56
|
+
* is atomic at the filesystem level (POSIX and Windows both guarantee
|
|
57
|
+
* this) — it either creates the directory and returns, or fails with
|
|
58
|
+
* EEXIST if something else claimed that exact name first. On EEXIST we
|
|
59
|
+
* regenerate a fresh id and retry; for the timestamp+random ids used
|
|
60
|
+
* throughout this module, two consecutive real collisions is already
|
|
61
|
+
* vanishingly unlikely.
|
|
62
|
+
*
|
|
63
|
+
* Creating `parentDir` itself (e.g. "snapshots/") is NOT part of the race
|
|
64
|
+
* being closed here — two processes racing to create the same shared
|
|
65
|
+
* parent with {recursive:true} is harmless (both succeed; no caller's
|
|
66
|
+
* data lives directly in the parent, only under their own distinctly
|
|
67
|
+
* named leaf directory).
|
|
68
|
+
*
|
|
69
|
+
* @param {string} parentDir — parent directory, created recursively if missing
|
|
70
|
+
* @param {() => string} generateId — produces a candidate leaf id; called
|
|
71
|
+
* again on every retry so concurrent claimants converge on different ids
|
|
72
|
+
* @param {{code: string, message: string, hint: string, maxAttempts?: number}} onExhausted
|
|
73
|
+
* — error identity thrown if every attempt collides (kept caller-specific
|
|
74
|
+
* so existing codes like SNAPSHOT_ID_COLLISION stay externally stable)
|
|
75
|
+
* @returns {Promise<{id: string, dir: string}>}
|
|
76
|
+
*/
|
|
77
|
+
export async function claimIdDir(parentDir, generateId, onExhausted) {
|
|
78
|
+
const maxAttempts = onExhausted.maxAttempts ?? 25;
|
|
79
|
+
if (!existsSync(parentDir)) {
|
|
80
|
+
await mkdir(parentDir, { recursive: true });
|
|
81
|
+
}
|
|
82
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
83
|
+
const id = generateId();
|
|
84
|
+
const dir = join(parentDir, id);
|
|
85
|
+
try {
|
|
86
|
+
await mkdir(dir); // non-recursive — throws EEXIST if already claimed
|
|
87
|
+
return { id, dir };
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (err.code === 'EEXIST') continue;
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
throw inputError(onExhausted.code, onExhausted.message, onExhausted.hint);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* SDL-M10: atomically claim a fresh, unused FILE `<parentDir>/<id>.json`.
|
|
98
|
+
*
|
|
99
|
+
* Same TOCTOU problem as claimIdDir, but for the one artifact in this
|
|
100
|
+
* domain stored as a flat file rather than a directory (training
|
|
101
|
+
* manifests: `training/manifests/<id>.json`). The atomic primitive here
|
|
102
|
+
* is `writeFile(..., {flag: 'wx'})` — exclusive create, which fails with
|
|
103
|
+
* EEXIST rather than silently overwriting — so claim-and-write happen in
|
|
104
|
+
* one step instead of racing a separate existsSync check against it.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} parentDir — parent directory, created recursively if missing
|
|
107
|
+
* @param {() => string} generateId — produces a candidate id; called again
|
|
108
|
+
* on every retry
|
|
109
|
+
* @param {(id: string) => string} buildContent — renders the file content
|
|
110
|
+
* for a given candidate id (called fresh per attempt since the content
|
|
111
|
+
* embeds the id)
|
|
112
|
+
* @param {{code: string, message: string, hint: string, maxAttempts?: number}} onExhausted
|
|
113
|
+
* @returns {Promise<{id: string, filePath: string}>}
|
|
114
|
+
*/
|
|
115
|
+
export async function claimIdFile(parentDir, generateId, buildContent, onExhausted) {
|
|
116
|
+
const maxAttempts = onExhausted.maxAttempts ?? 25;
|
|
117
|
+
if (!existsSync(parentDir)) {
|
|
118
|
+
await mkdir(parentDir, { recursive: true });
|
|
119
|
+
}
|
|
120
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
121
|
+
const id = generateId();
|
|
122
|
+
const filePath = join(parentDir, `${id}.json`);
|
|
123
|
+
try {
|
|
124
|
+
await writeFile(filePath, buildContent(id), { flag: 'wx' }); // exclusive create — atomic
|
|
125
|
+
return { id, filePath };
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (err.code === 'EEXIST') continue;
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
throw inputError(onExhausted.code, onExhausted.message, onExhausted.hint);
|
|
132
|
+
}
|
|
133
|
+
|
|
38
134
|
/**
|
|
39
135
|
* Derive which rule names were evaluated against a record for a given profile.
|
|
40
136
|
* Mirrors the order of checks in evaluateEligibility() so the trace is stable.
|
|
@@ -95,18 +191,6 @@ export function computeConfigFingerprint(projectRoot) {
|
|
|
95
191
|
export async function createSnapshot(projectRoot, profile, options = {}) {
|
|
96
192
|
const config = loadProjectConfig(projectRoot);
|
|
97
193
|
const recordsDir = join(projectRoot, 'records');
|
|
98
|
-
const snapshotId = generateSnapshotId();
|
|
99
|
-
const snapshotDir = join(projectRoot, 'snapshots', snapshotId);
|
|
100
|
-
|
|
101
|
-
// D-005: refuse to proceed if the generated ID has collided with an existing
|
|
102
|
-
// snapshot dir — silent overwrite would break Law 1 (snapshots are frozen).
|
|
103
|
-
if (!options.dryRun && existsSync(snapshotDir)) {
|
|
104
|
-
throw inputError(
|
|
105
|
-
'SNAPSHOT_ID_COLLISION',
|
|
106
|
-
`Snapshot directory already exists: ${snapshotDir}`,
|
|
107
|
-
'Retry to generate a fresh snapshot ID.',
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
194
|
|
|
111
195
|
const configFingerprint = computeConfigFingerprint(projectRoot);
|
|
112
196
|
const profileId = profile.profile_id || profile.id || 'unknown';
|
|
@@ -141,7 +225,7 @@ export async function createSnapshot(projectRoot, profile, options = {}) {
|
|
|
141
225
|
continue;
|
|
142
226
|
}
|
|
143
227
|
|
|
144
|
-
const { eligible, reasons } = evaluateEligibility(
|
|
228
|
+
const { eligible, reasons, reason_codes: reasonCodes } = evaluateEligibility(
|
|
145
229
|
record, profile, config.lanes, config.terminology
|
|
146
230
|
);
|
|
147
231
|
|
|
@@ -175,7 +259,13 @@ export async function createSnapshot(projectRoot, profile, options = {}) {
|
|
|
175
259
|
}
|
|
176
260
|
}
|
|
177
261
|
} else {
|
|
178
|
-
|
|
262
|
+
// F5: forward the stable reason_codes alongside the human-readable
|
|
263
|
+
// reasons — purely additive on excluded.jsonl's on-disk shape (one
|
|
264
|
+
// new key), so old readers/old files that only expect
|
|
265
|
+
// {record_id, reasons} are unaffected. Lets categorizeExclusions
|
|
266
|
+
// (lib/eligibility.js) categorize this snapshot's exclusions by
|
|
267
|
+
// code instead of prose-matching `reasons`.
|
|
268
|
+
excluded.push({ record_id: record.id, reasons, reason_codes: reasonCodes });
|
|
179
269
|
}
|
|
180
270
|
}
|
|
181
271
|
|
|
@@ -191,9 +281,24 @@ export async function createSnapshot(projectRoot, profile, options = {}) {
|
|
|
191
281
|
|
|
192
282
|
// Write snapshot files (skip if dry run)
|
|
193
283
|
if (options.dryRun) {
|
|
284
|
+
// Dry run never touches disk, so there is nothing to atomically claim —
|
|
285
|
+
// a fresh preview id is enough to characterize the shape of the result.
|
|
286
|
+
const snapshotId = generateSnapshotId();
|
|
194
287
|
return { snapshotId, included: included.length, excluded: excluded.length, unreadable: unreadable.length };
|
|
195
288
|
}
|
|
196
|
-
|
|
289
|
+
|
|
290
|
+
// SDL-M10: atomically claim the snapshot id/dir — closes the
|
|
291
|
+
// check-then-act race the old existsSync+recursive-mkdir pattern had
|
|
292
|
+
// (D-005's intent, now actually race-proof; see claimIdDir above).
|
|
293
|
+
const { id: snapshotId, dir: snapshotDir } = await claimIdDir(
|
|
294
|
+
join(projectRoot, 'snapshots'),
|
|
295
|
+
generateSnapshotId,
|
|
296
|
+
{
|
|
297
|
+
code: 'SNAPSHOT_ID_COLLISION',
|
|
298
|
+
message: 'Could not claim a unique snapshot ID — every candidate collided with an existing snapshot directory.',
|
|
299
|
+
hint: 'Retry; if this persists, another sdlab process may be creating snapshots in a tight loop.',
|
|
300
|
+
},
|
|
301
|
+
);
|
|
197
302
|
|
|
198
303
|
const manifest = {
|
|
199
304
|
// DB-001 / DB-006: stamp manifest schema version (reader checks on load)
|