@mcptoolshop/style-dataset-lab 3.3.0 → 3.5.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 (119) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.es.md +117 -55
  3. package/README.fr.md +125 -65
  4. package/README.hi.md +117 -68
  5. package/README.it.md +119 -58
  6. package/README.ja.md +124 -74
  7. package/README.md +73 -7
  8. package/README.pt-BR.md +128 -63
  9. package/README.zh.md +116 -65
  10. package/bin/sdlab.js +100 -4
  11. package/lib/adapters/ai-toolkit.js +224 -35
  12. package/lib/adapters/comfyui-runner.js +79 -26
  13. package/lib/adapters/comfyui-workflows.js +94 -11
  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/asset-ingest.js +575 -0
  18. package/lib/asset-masks.js +323 -0
  19. package/lib/asset-source.js +1176 -0
  20. package/lib/batch-modes.js +29 -8
  21. package/lib/batch-runs.js +29 -1
  22. package/lib/batch-sheet-render.js +13 -3
  23. package/lib/brief-compiler.js +19 -0
  24. package/lib/candidate-sheet.js +327 -0
  25. package/lib/canon-author.js +350 -0
  26. package/lib/canon-build/build.js +146 -93
  27. package/lib/canon-review-gate.js +205 -0
  28. package/lib/captions.js +57 -0
  29. package/lib/card.js +74 -2
  30. package/lib/colab-launcher-guards.js +285 -0
  31. package/lib/comfyui.js +116 -18
  32. package/lib/config.js +116 -12
  33. package/lib/critique-engine.js +36 -2
  34. package/lib/eligibility.js +121 -9
  35. package/lib/eval-pack.js +14 -14
  36. package/lib/eval-runs.js +33 -0
  37. package/lib/export.js +122 -34
  38. package/lib/freeze-gate.js +16 -3
  39. package/lib/freeze-stamp.js +28 -3
  40. package/lib/generated-provenance.js +194 -6
  41. package/lib/implementation-packs.js +95 -17
  42. package/lib/ingest.js +396 -0
  43. package/lib/judgment-provenance.js +220 -0
  44. package/lib/lane1-render.js +668 -0
  45. package/lib/measure.js +422 -0
  46. package/lib/memory-store.js +663 -0
  47. package/lib/model-licences.js +305 -0
  48. package/lib/npy-meta.js +130 -0
  49. package/lib/paths.js +196 -7
  50. package/lib/png-meta.js +512 -0
  51. package/lib/records.js +54 -8
  52. package/lib/reingest-selected.js +113 -17
  53. package/lib/reingest.js +27 -3
  54. package/lib/run-manifest.js +159 -25
  55. package/lib/run-summary.js +12 -6
  56. package/lib/runtime-runs.js +45 -4
  57. package/lib/snapshot.js +114 -15
  58. package/lib/split.js +364 -48
  59. package/lib/train-runs.js +214 -0
  60. package/lib/training-manifests.js +114 -21
  61. package/lib/training-packages.js +147 -15
  62. package/lib/verifier.js +3 -0
  63. package/lib/verify-chain.js +367 -0
  64. package/lib/workflow-profiles.js +24 -3
  65. package/package.json +4 -3
  66. package/runtime/comfyui/txt2img-qwen.json +38 -0
  67. package/runtime/comfyui/txt2img-standard.json +21 -0
  68. package/scripts/_resume.js +62 -0
  69. package/scripts/_shared-args.js +44 -0
  70. package/scripts/asset-ingest.js +160 -0
  71. package/scripts/brief-compile.js +2 -1
  72. package/scripts/brief-show.js +2 -1
  73. package/scripts/canon-bind.js +2 -2
  74. package/scripts/canon-build.js +27 -20
  75. package/scripts/canon-draft.js +82 -0
  76. package/scripts/canon-drift.js +16 -7
  77. package/scripts/canon-freeze-status.js +14 -6
  78. package/scripts/canon-freeze.js +31 -11
  79. package/scripts/canon-unfreeze.js +17 -9
  80. package/scripts/card.js +2 -2
  81. package/scripts/colab/REAL_SERVICE_ASSUMPTIONS.md +14 -0
  82. package/scripts/colab/ai-toolkit-override-py315.txt +45 -0
  83. package/scripts/colab/colab_window.sh +587 -0
  84. package/scripts/colab/lane1_render.py +970 -0
  85. package/scripts/colab/launch_guards.py +564 -0
  86. package/scripts/colab/launch_lane1_stub.sh +309 -0
  87. package/scripts/colab/launch_lane3_stub.sh +366 -0
  88. package/scripts/colab/remote_runner.py +59 -0
  89. package/scripts/compare.js +2 -2
  90. package/scripts/critique.js +16 -1
  91. package/scripts/curate.js +2 -2
  92. package/scripts/doctor.js +182 -55
  93. package/scripts/eligibility.js +42 -2
  94. package/scripts/eval-pack.js +2 -2
  95. package/scripts/eval-run.js +2 -2
  96. package/scripts/export.js +2 -2
  97. package/scripts/generate-controlnet.js +45 -25
  98. package/scripts/generate-identity.js +53 -23
  99. package/scripts/generate-ipadapter.js +44 -23
  100. package/scripts/generate.js +86 -17
  101. package/scripts/implementation-pack.js +2 -2
  102. package/scripts/ingest.js +145 -0
  103. package/scripts/init.js +73 -13
  104. package/scripts/measure.js +170 -0
  105. package/scripts/measure_image.py +458 -0
  106. package/scripts/migrate-records.js +142 -0
  107. package/scripts/painterly-test.js +4 -3
  108. package/scripts/painterly.js +7 -3
  109. package/scripts/qwen_generate.py +138 -20
  110. package/scripts/reingest.js +2 -2
  111. package/scripts/sheet.js +142 -0
  112. package/scripts/snapshot.js +2 -2
  113. package/scripts/split.js +2 -2
  114. package/scripts/training-manifest.js +2 -2
  115. package/scripts/training-package.js +2 -2
  116. package/scripts/training-profile.js +2 -2
  117. package/scripts/verify.js +67 -0
  118. package/scripts/workflow-list.js +2 -1
  119. package/scripts/workflow-show.js +2 -1
package/lib/snapshot.js CHANGED
@@ -41,6 +41,96 @@ export function checkManifestVersion(manifest, kind) {
41
41
  }
42
42
  }
43
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
+
44
134
  /**
45
135
  * Derive which rule names were evaluated against a record for a given profile.
46
136
  * Mirrors the order of checks in evaluateEligibility() so the trace is stable.
@@ -101,18 +191,6 @@ export function computeConfigFingerprint(projectRoot) {
101
191
  export async function createSnapshot(projectRoot, profile, options = {}) {
102
192
  const config = loadProjectConfig(projectRoot);
103
193
  const recordsDir = join(projectRoot, 'records');
104
- const snapshotId = generateSnapshotId();
105
- const snapshotDir = join(projectRoot, 'snapshots', snapshotId);
106
-
107
- // D-005: refuse to proceed if the generated ID has collided with an existing
108
- // snapshot dir — silent overwrite would break Law 1 (snapshots are frozen).
109
- if (!options.dryRun && existsSync(snapshotDir)) {
110
- throw inputError(
111
- 'SNAPSHOT_ID_COLLISION',
112
- `Snapshot directory already exists: ${snapshotDir}`,
113
- 'Retry to generate a fresh snapshot ID.',
114
- );
115
- }
116
194
 
117
195
  const configFingerprint = computeConfigFingerprint(projectRoot);
118
196
  const profileId = profile.profile_id || profile.id || 'unknown';
@@ -147,7 +225,7 @@ export async function createSnapshot(projectRoot, profile, options = {}) {
147
225
  continue;
148
226
  }
149
227
 
150
- const { eligible, reasons } = evaluateEligibility(
228
+ const { eligible, reasons, reason_codes: reasonCodes } = evaluateEligibility(
151
229
  record, profile, config.lanes, config.terminology
152
230
  );
153
231
 
@@ -181,7 +259,13 @@ export async function createSnapshot(projectRoot, profile, options = {}) {
181
259
  }
182
260
  }
183
261
  } else {
184
- 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 });
185
269
  }
186
270
  }
187
271
 
@@ -197,9 +281,24 @@ export async function createSnapshot(projectRoot, profile, options = {}) {
197
281
 
198
282
  // Write snapshot files (skip if dry run)
199
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();
200
287
  return { snapshotId, included: included.length, excluded: excluded.length, unreadable: unreadable.length };
201
288
  }
202
- 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
+ );
203
302
 
204
303
  const manifest = {
205
304
  // DB-001 / DB-006: stamp manifest schema version (reader checks on load)