@mcptoolshop/style-dataset-lab 3.4.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 (48) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.es.md +111 -59
  3. package/README.fr.md +117 -67
  4. package/README.hi.md +109 -70
  5. package/README.it.md +111 -60
  6. package/README.ja.md +116 -76
  7. package/README.md +58 -2
  8. package/README.pt-BR.md +119 -64
  9. package/README.zh.md +108 -67
  10. package/bin/sdlab.js +50 -1
  11. package/lib/adapters/ai-toolkit.js +187 -30
  12. package/lib/adapters/comfyui-workflows.js +84 -9
  13. package/lib/asset-ingest.js +575 -0
  14. package/lib/asset-masks.js +323 -0
  15. package/lib/asset-source.js +1176 -0
  16. package/lib/canon-author.js +350 -0
  17. package/lib/canon-review-gate.js +205 -0
  18. package/lib/colab-launcher-guards.js +285 -0
  19. package/lib/export.js +1 -1
  20. package/lib/freeze-gate.js +3 -0
  21. package/lib/generated-provenance.js +178 -5
  22. package/lib/lane1-render.js +668 -0
  23. package/lib/memory-store.js +663 -0
  24. package/lib/model-licences.js +305 -0
  25. package/lib/npy-meta.js +130 -0
  26. package/lib/png-meta.js +512 -0
  27. package/lib/records.js +54 -8
  28. package/lib/reingest-selected.js +15 -1
  29. package/lib/reingest.js +27 -3
  30. package/lib/run-manifest.js +59 -11
  31. package/lib/train-runs.js +214 -0
  32. package/lib/verifier.js +3 -0
  33. package/lib/verify-chain.js +367 -0
  34. package/package.json +2 -2
  35. package/scripts/asset-ingest.js +160 -0
  36. package/scripts/canon-draft.js +82 -0
  37. package/scripts/canon-freeze.js +4 -0
  38. package/scripts/colab/REAL_SERVICE_ASSUMPTIONS.md +14 -0
  39. package/scripts/colab/ai-toolkit-override-py315.txt +45 -0
  40. package/scripts/colab/colab_window.sh +587 -0
  41. package/scripts/colab/lane1_render.py +970 -0
  42. package/scripts/colab/launch_guards.py +564 -0
  43. package/scripts/colab/launch_lane1_stub.sh +309 -0
  44. package/scripts/colab/launch_lane3_stub.sh +366 -0
  45. package/scripts/colab/remote_runner.py +59 -0
  46. package/scripts/doctor.js +1 -0
  47. package/scripts/generate.js +38 -12
  48. package/scripts/verify.js +67 -0
@@ -26,6 +26,19 @@ import { createHash } from 'node:crypto';
26
26
  import { openSync, readSync, closeSync, statSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
27
27
  import { join } from 'node:path';
28
28
  import { warn } from './log.js';
29
+ import {
30
+ CLEARED_SPDX_IDS,
31
+ resolveModelLicence,
32
+ isClearedLicenceEntry,
33
+ assertClearedPinningLicences,
34
+ } from './model-licences.js';
35
+
36
+ export {
37
+ CLEARED_SPDX_IDS,
38
+ resolveModelLicence,
39
+ isClearedLicenceEntry,
40
+ assertClearedPinningLicences,
41
+ };
29
42
 
30
43
  /**
31
44
  * Contract version for the `pinning` block itself. Bump when the block's shape
@@ -239,6 +252,17 @@ function sampleFingerprint(path, size) {
239
252
  }
240
253
  }
241
254
 
255
+ function withLicenceFields(base, name, { licence = null, modelLicences = null } = {}) {
256
+ const lic = resolveModelLicence(name, licence, modelLicences);
257
+ return {
258
+ ...base,
259
+ licence_id: lic.licence_id,
260
+ licence_url: lic.licence_url,
261
+ date_read: lic.date_read,
262
+ licence: lic,
263
+ };
264
+ }
265
+
242
266
  /**
243
267
  * Best-effort content identity for one model/LoRA file: {name, size_bytes, sha256}
244
268
  * (+ a short `hash_note` when sha256 can't be produced). size_bytes is always
@@ -255,10 +279,10 @@ function sampleFingerprint(path, size) {
255
279
  * @param {boolean} [opts.doHash=false] — compute sha256 (else record name+size only)
256
280
  * @param {{map: Map, cacheFile?: string, dirty: boolean}} [opts.cache]
257
281
  */
258
- export function hashModelFile(name, { kind, modelsDir = defaultModelsDir(), doHash = false, cache } = {}) {
259
- if (!name) return { name: name ?? null, size_bytes: null, sha256: null, hash_note: 'no filename' };
282
+ export function hashModelFile(name, { kind, modelsDir = defaultModelsDir(), doHash = false, cache, licence = null, modelLicences = null } = {}) {
283
+ if (!name) return withLicenceFields({ name: name ?? null, size_bytes: null, sha256: null, hash_note: 'no filename' }, name, { licence, modelLicences });
260
284
  const resolved = resolveModelPath(name, kind, modelsDir);
261
- if (!resolved) return { name, size_bytes: null, sha256: null, hash_note: `unresolved under ${modelsDir}` };
285
+ if (!resolved) return withLicenceFields({ name, size_bytes: null, sha256: null, hash_note: `unresolved under ${modelsDir}` }, name, { licence, modelLicences });
262
286
  let size_bytes = null;
263
287
  let mtimeMs = 0;
264
288
  try {
@@ -266,9 +290,9 @@ export function hashModelFile(name, { kind, modelsDir = defaultModelsDir(), doHa
266
290
  size_bytes = st.size;
267
291
  mtimeMs = st.mtimeMs;
268
292
  } catch {
269
- return { name, size_bytes: null, sha256: null, hash_note: 'stat failed' };
293
+ return withLicenceFields({ name, size_bytes: null, sha256: null, hash_note: 'stat failed' }, name, { licence, modelLicences });
270
294
  }
271
- if (!doHash) return { name, size_bytes, sha256: null, hash_note: HASH_SKIPPED_NOTE };
295
+ if (!doHash) return withLicenceFields({ name, size_bytes, sha256: null, hash_note: HASH_SKIPPED_NOTE }, name, { licence, modelLicences });
272
296
  let sample = '';
273
297
  try {
274
298
  sample = sampleFingerprint(resolved, size_bytes);
@@ -278,18 +302,18 @@ export function hashModelFile(name, { kind, modelsDir = defaultModelsDir(), doHa
278
302
  // real read error anyway.
279
303
  }
280
304
  const key = `${name}:${size_bytes}:${Math.round(mtimeMs)}:${sample}`;
281
- if (cache?.map?.has(key)) return { name, size_bytes, sha256: cache.map.get(key) };
305
+ if (cache?.map?.has(key)) return withLicenceFields({ name, size_bytes, sha256: cache.map.get(key) }, name, { licence, modelLicences });
282
306
  let sha256;
283
307
  try {
284
308
  sha256 = sha256File(resolved);
285
309
  } catch {
286
- return { name, size_bytes, sha256: null, hash_note: 'hash failed (read error)' };
310
+ return withLicenceFields({ name, size_bytes, sha256: null, hash_note: 'hash failed (read error)' }, name, { licence, modelLicences });
287
311
  }
288
312
  if (cache?.map) {
289
313
  cache.map.set(key, sha256);
290
314
  cache.dirty = true;
291
315
  }
292
- return { name, size_bytes, sha256 };
316
+ return withLicenceFields({ name, size_bytes, sha256 }, name, { licence, modelLicences });
293
317
  }
294
318
 
295
319
  /**
@@ -357,16 +381,34 @@ export function flushHashCache(cache) {
357
381
  * @param {boolean} [args.hashModels=false] — opt-in sha256 of model/LoRA files
358
382
  * @param {string} [args.modelsDir]
359
383
  * @param {{map: Map, cacheFile?: string, dirty: boolean}} [args.cache]
384
+ * @param {Object} [args.modelLicences]
385
+ * @param {boolean} [args.enforceLicence=false]
386
+ * @param {boolean} [args.allowUnlicensedResearch=false]
360
387
  * @returns {Object} pinning block
361
388
  */
362
- export function buildPinning({ graph, models = {}, loras = [], seedPolicy = null, hashModels = false, modelsDir = defaultModelsDir(), cache } = {}) {
363
- const opts = { modelsDir, doHash: hashModels, cache };
389
+ export function buildPinning({
390
+ graph,
391
+ models = {},
392
+ loras = [],
393
+ seedPolicy = null,
394
+ hashModels = false,
395
+ modelsDir = defaultModelsDir(),
396
+ cache,
397
+ modelLicences = null,
398
+ enforceLicence = false,
399
+ allowUnlicensedResearch = false,
400
+ } = {}) {
401
+ const opts = { modelsDir, doHash: hashModels, cache, modelLicences };
364
402
  const modelHashes = buildModelHashes(models, opts);
365
403
  const loraHashes = (loras || []).map((l) => ({
366
- ...hashModelFile(l.name, { ...opts, kind: 'lora' }),
404
+ ...hashModelFile(l.name, { ...opts, kind: 'lora', licence: l.licence || null }),
367
405
  weight: l.weight ?? 1.0,
368
406
  }));
369
407
 
408
+ if (enforceLicence) {
409
+ assertClearedPinningLicences(modelHashes, loraHashes, { allowUnlicensedResearch });
410
+ }
411
+
370
412
  // B07: hash_note silently encoded a hashing FAILURE (unresolved file,
371
413
  // stat failed, read error, no filename) with nothing reading it —
372
414
  // buildPinning() never aggregated or logged, and this file imported no
@@ -384,10 +426,16 @@ export function buildPinning({ graph, models = {}, loras = [], seedPolicy = null
384
426
  warn(`buildPinning: ${failed.length}/${allEntries.length} model(s)/lora(s) failed to hash: ${detail}`);
385
427
  }
386
428
 
429
+ const ccBy4Eligible =
430
+ !allowUnlicensedResearch &&
431
+ allEntries.length > 0 &&
432
+ allEntries.every((entry) => isClearedLicenceEntry(entry));
433
+
387
434
  return {
388
435
  pinning_version: PINNING_VERSION,
389
436
  comfy_workflow_sha: comfyWorkflowSha(graph),
390
437
  seed_policy: seedPolicy,
438
+ cc_by_4_eligible: ccBy4Eligible,
391
439
  models: modelHashes,
392
440
  loras: loraHashes,
393
441
  };
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Trained-LoRA provenance record (`train-run.json`) contract (DESIGN-v3 §7, G13).
3
+ *
4
+ * Links every trained LoRA checkpoint back to:
5
+ * - dataset_package_id + dataset_manifest_sha256
6
+ * - config_sha256 (ai-toolkit-config.yaml)
7
+ * - override_file + override_sha256 + upstream_issue (#1087)
8
+ * - base_model pin with cleared Apache-2.0 SPDX licence
9
+ * - window_id
10
+ * - steps, checkpoint_sha256s, sample_grid_sha256s
11
+ * - eval_pack_sha (attached once the eval pack runs)
12
+ */
13
+
14
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
15
+ import { dirname } from 'node:path';
16
+ import {
17
+ AI_TOOLKIT_UPSTREAM_ISSUE,
18
+ assertNotHeldTrainingData,
19
+ getAiToolkitOverrideMetadata,
20
+ } from './adapters/ai-toolkit.js';
21
+ import { inputError as rawInputError } from './errors.js';
22
+
23
+ function inputError(code, msg) {
24
+ return rawInputError(code, `${code}: ${msg}`);
25
+ }
26
+
27
+ export const CLEARED_TRAIN_BASE_LICENCES = new Set([
28
+ 'Apache-2.0',
29
+ 'MIT',
30
+ 'BSD-2-Clause',
31
+ 'BSD-3-Clause',
32
+ 'CC0-1.0',
33
+ 'CC-BY-4.0',
34
+ ]);
35
+
36
+ function normalizeSha256(val) {
37
+ if (typeof val !== 'string') return null;
38
+ const trimmed = val.trim().toLowerCase();
39
+ const hex = trimmed.startsWith('sha256:') ? trimmed.slice(7) : trimmed;
40
+ if (!/^[0-9a-f]{64}$/.test(hex)) return null;
41
+ return `sha256:${hex}`;
42
+ }
43
+
44
+ /**
45
+ * Build and validate a `train-run.json` record (§7, G13).
46
+ *
47
+ * @param {Object} opts
48
+ * @returns {Object}
49
+ */
50
+ export function buildTrainRunRecord(opts = {}) {
51
+ const {
52
+ run_id,
53
+ window_id,
54
+ dataset_package_id,
55
+ dataset_manifest_sha256,
56
+ config_sha256,
57
+ override_file,
58
+ override_sha256,
59
+ upstream_issue = AI_TOOLKIT_UPSTREAM_ISSUE,
60
+ base_model,
61
+ steps = [],
62
+ checkpoint_sha256s = {},
63
+ sample_grid_sha256s = {},
64
+ eval_pack_sha = null,
65
+ created_at = new Date().toISOString(),
66
+ } = opts;
67
+
68
+ if (!run_id || typeof run_id !== 'string') {
69
+ throw inputError('TRAIN_RUN_INVALID', 'train-run.json requires a non-empty "run_id".');
70
+ }
71
+ if (!window_id || typeof window_id !== 'string') {
72
+ throw inputError('TRAIN_RUN_INVALID', 'train-run.json requires a non-empty "window_id".');
73
+ }
74
+ if (!dataset_package_id || typeof dataset_package_id !== 'string') {
75
+ throw inputError('TRAIN_RUN_INVALID', 'train-run.json requires a non-empty "dataset_package_id".');
76
+ }
77
+
78
+ assertNotHeldTrainingData({
79
+ profile: { profile_id: dataset_package_id },
80
+ manifest: { source_export_id: run_id },
81
+ });
82
+
83
+ const normDatasetSha = normalizeSha256(dataset_manifest_sha256);
84
+ const normConfigSha = normalizeSha256(config_sha256);
85
+ const defaultOverride = getAiToolkitOverrideMetadata();
86
+ const normOverrideSha = normalizeSha256(override_sha256 || defaultOverride.override_sha256);
87
+
88
+ if (!normDatasetSha || !normConfigSha || !normOverrideSha) {
89
+ throw inputError(
90
+ 'TRAIN_RUN_SHA_INVALID',
91
+ 'train-run.json requires valid 64-hex dataset_manifest_sha256, config_sha256, and override_sha256.'
92
+ );
93
+ }
94
+
95
+ if (!base_model || typeof base_model !== 'object' || !base_model.name) {
96
+ throw inputError('TRAIN_RUN_BASE_MODEL_INVALID', 'train-run.json requires a base_model object with name.');
97
+ }
98
+ const normBaseSha = normalizeSha256(base_model.sha256);
99
+ if (!normBaseSha) {
100
+ throw inputError('TRAIN_RUN_SHA_INVALID', 'base_model.sha256 must be a valid 64-hex sha256 digest.');
101
+ }
102
+ if (
103
+ !base_model.licence_id ||
104
+ !CLEARED_TRAIN_BASE_LICENCES.has(base_model.licence_id) ||
105
+ !base_model.licence_url ||
106
+ !base_model.date_read
107
+ ) {
108
+ throw inputError(
109
+ 'TRAIN_RUN_LICENCE_INVALID',
110
+ `base_model "${base_model.name}" must carry a cleared commercial SPDX licence_id (${[...CLEARED_TRAIN_BASE_LICENCES].join(', ')}), licence_url, and date_read (got "${base_model.licence_id}").`
111
+ );
112
+ }
113
+
114
+ if (!Array.isArray(steps) || steps.length === 0) {
115
+ throw inputError('TRAIN_RUN_STEPS_EMPTY', 'train-run.json requires at least one checkpoint step.');
116
+ }
117
+
118
+ const normalizedCheckpoints = {};
119
+ const normalizedSamples = {};
120
+ for (const step of steps) {
121
+ const key = String(step);
122
+ const ckptSha = normalizeSha256(checkpoint_sha256s[key] ?? checkpoint_sha256s[step]);
123
+ if (!ckptSha) {
124
+ throw inputError(
125
+ 'TRAIN_RUN_SHA_INVALID',
126
+ `Missing or invalid checkpoint_sha256 for step ${key}.`
127
+ );
128
+ }
129
+ normalizedCheckpoints[key] = ckptSha;
130
+
131
+ const gridRaw = sample_grid_sha256s[key] ?? sample_grid_sha256s[step];
132
+ if (gridRaw !== undefined && gridRaw !== null) {
133
+ const gridSha = normalizeSha256(gridRaw);
134
+ if (!gridSha) {
135
+ throw inputError(
136
+ 'TRAIN_RUN_SHA_INVALID',
137
+ `Invalid sample_grid_sha256 for step ${key}.`
138
+ );
139
+ }
140
+ normalizedSamples[key] = gridSha;
141
+ }
142
+ }
143
+
144
+ const normEvalSha = eval_pack_sha ? normalizeSha256(eval_pack_sha) : null;
145
+ if (eval_pack_sha && !normEvalSha) {
146
+ throw inputError('TRAIN_RUN_SHA_INVALID', 'eval_pack_sha must be null or a valid 64-hex sha256 digest.');
147
+ }
148
+
149
+ return {
150
+ run_id,
151
+ window_id,
152
+ lane: 'train',
153
+ dataset_package_id,
154
+ dataset_manifest_sha256: normDatasetSha,
155
+ config_sha256: normConfigSha,
156
+ override_file: override_file || defaultOverride.override_file,
157
+ override_sha256: normOverrideSha,
158
+ upstream_issue,
159
+ base_model: {
160
+ name: base_model.name,
161
+ repo: base_model.repo || base_model.name,
162
+ revision: base_model.revision || 'main',
163
+ sha256: normBaseSha,
164
+ licence_id: base_model.licence_id,
165
+ licence_url: base_model.licence_url,
166
+ date_read: base_model.date_read,
167
+ },
168
+ steps: steps.map(Number),
169
+ checkpoint_sha256s: normalizedCheckpoints,
170
+ sample_grid_sha256s: normalizedSamples,
171
+ eval_pack_sha: normEvalSha,
172
+ created_at,
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Write `train-run.json` to disk.
178
+ *
179
+ * @param {string} trainRunPath
180
+ * @param {Object} record
181
+ * @returns {Object}
182
+ */
183
+ export function writeTrainRunJson(trainRunPath, record) {
184
+ const validated = buildTrainRunRecord(record);
185
+ mkdirSync(dirname(trainRunPath), { recursive: true });
186
+ writeFileSync(trainRunPath, JSON.stringify(validated, null, 2) + '\n', 'utf-8');
187
+ return validated;
188
+ }
189
+
190
+ /**
191
+ * Read and validate `train-run.json` from disk.
192
+ *
193
+ * @param {string} trainRunPath
194
+ * @returns {Object}
195
+ */
196
+ export function readTrainRunJson(trainRunPath) {
197
+ const parsed = JSON.parse(readFileSync(trainRunPath, 'utf-8'));
198
+ return buildTrainRunRecord(parsed);
199
+ }
200
+
201
+ /**
202
+ * Attach `eval_pack_sha` to an existing `train-run.json` once the eval pack runs (G13).
203
+ *
204
+ * @param {string} trainRunPath
205
+ * @param {string} evalPackSha
206
+ * @returns {Object}
207
+ */
208
+ export function attachEvalPackSha(trainRunPath, evalPackSha) {
209
+ const current = readTrainRunJson(trainRunPath);
210
+ return writeTrainRunJson(trainRunPath, {
211
+ ...current,
212
+ eval_pack_sha: evalPackSha,
213
+ });
214
+ }
package/lib/verifier.js CHANGED
@@ -13,6 +13,9 @@
13
13
  */
14
14
 
15
15
  import { warn } from './log.js';
16
+ import { assertNoSelfVerification } from './canon-review-gate.js';
17
+
18
+ export { assertNoSelfVerification };
16
19
 
17
20
  /** The rule-based critique engine's judge id. Bump vN when the rule set changes. */
18
21
  export const RULE_BASED_JUDGE = 'rule-based:sdlab-critique-v1';