@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.
Files changed (95) hide show
  1. package/CHANGELOG.md +58 -1
  2. package/README.es.md +40 -19
  3. package/README.fr.md +74 -53
  4. package/README.hi.md +67 -46
  5. package/README.it.md +42 -21
  6. package/README.ja.md +44 -23
  7. package/README.md +25 -4
  8. package/README.pt-BR.md +42 -21
  9. package/README.zh.md +66 -45
  10. package/bin/sdlab.js +51 -4
  11. package/lib/adapters/ai-toolkit.js +37 -5
  12. package/lib/adapters/comfyui-runner.js +114 -27
  13. package/lib/adapters/comfyui-workflows.js +10 -2
  14. package/lib/adapters/diffusers-lora.js +37 -5
  15. package/lib/adapters/generic-image-caption.js +43 -5
  16. package/lib/args.js +16 -1
  17. package/lib/batch-modes.js +29 -8
  18. package/lib/batch-runs.js +29 -1
  19. package/lib/batch-sheet-render.js +13 -3
  20. package/lib/brief-compiler.js +19 -0
  21. package/lib/candidate-sheet.js +327 -0
  22. package/lib/canon-build/build.js +146 -93
  23. package/lib/captions.js +57 -0
  24. package/lib/card.js +74 -2
  25. package/lib/comfyui.js +116 -18
  26. package/lib/config.js +116 -12
  27. package/lib/critique-engine.js +82 -3
  28. package/lib/critique-render.js +45 -0
  29. package/lib/eligibility.js +121 -9
  30. package/lib/eval-pack.js +14 -14
  31. package/lib/eval-runs.js +33 -0
  32. package/lib/export.js +122 -34
  33. package/lib/freeze-gate.js +69 -16
  34. package/lib/freeze-stamp.js +28 -3
  35. package/lib/generated-provenance.js +16 -1
  36. package/lib/implementation-packs.js +95 -17
  37. package/lib/ingest.js +396 -0
  38. package/lib/judgment-provenance.js +220 -0
  39. package/lib/measure.js +422 -0
  40. package/lib/paths.js +196 -7
  41. package/lib/reingest-selected.js +98 -16
  42. package/lib/run-manifest.js +394 -0
  43. package/lib/run-summary.js +12 -6
  44. package/lib/runtime-runs.js +45 -4
  45. package/lib/snapshot.js +121 -16
  46. package/lib/split.js +364 -48
  47. package/lib/training-manifests.js +114 -21
  48. package/lib/training-packages.js +147 -15
  49. package/lib/verifier.js +67 -0
  50. package/lib/workflow-profiles.js +24 -3
  51. package/package.json +3 -2
  52. package/runtime/comfyui/txt2img-qwen.json +38 -0
  53. package/runtime/comfyui/txt2img-standard.json +21 -0
  54. package/scripts/_resume.js +62 -0
  55. package/scripts/_shared-args.js +44 -0
  56. package/scripts/brief-compile.js +2 -1
  57. package/scripts/brief-show.js +2 -1
  58. package/scripts/canon-bind.js +2 -2
  59. package/scripts/canon-build.js +27 -20
  60. package/scripts/canon-drift.js +16 -7
  61. package/scripts/canon-freeze-status.js +14 -6
  62. package/scripts/canon-freeze.js +27 -11
  63. package/scripts/canon-unfreeze.js +17 -9
  64. package/scripts/card.js +2 -2
  65. package/scripts/compare.js +2 -2
  66. package/scripts/critique.js +48 -8
  67. package/scripts/curate.js +23 -2
  68. package/scripts/doctor.js +181 -55
  69. package/scripts/eligibility.js +42 -2
  70. package/scripts/eval-pack.js +2 -2
  71. package/scripts/eval-run.js +2 -2
  72. package/scripts/export.js +2 -2
  73. package/scripts/generate-controlnet.js +45 -25
  74. package/scripts/generate-identity.js +53 -23
  75. package/scripts/generate-ipadapter.js +44 -23
  76. package/scripts/generate.js +72 -5
  77. package/scripts/implementation-pack.js +2 -2
  78. package/scripts/ingest.js +145 -0
  79. package/scripts/init.js +73 -13
  80. package/scripts/measure.js +170 -0
  81. package/scripts/measure_image.py +458 -0
  82. package/scripts/migrate-records.js +142 -0
  83. package/scripts/painterly-test.js +4 -3
  84. package/scripts/painterly.js +7 -3
  85. package/scripts/qwen_generate.py +334 -13
  86. package/scripts/reingest.js +2 -2
  87. package/scripts/run-generate.js +3 -0
  88. package/scripts/sheet.js +142 -0
  89. package/scripts/snapshot.js +2 -2
  90. package/scripts/split.js +2 -2
  91. package/scripts/training-manifest.js +2 -2
  92. package/scripts/training-package.js +2 -2
  93. package/scripts/training-profile.js +2 -2
  94. package/scripts/workflow-list.js +2 -1
  95. 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.2.0';
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
- excluded.push({ record_id: record.id, reasons });
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
- await mkdir(snapshotDir, { recursive: true });
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)