@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
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Model and LoRA licence catalog and verification helpers (G4, §5.2–§5.3).
3
+ *
4
+ * Cleared SPDX licences for CC-BY-4.0 dataset/LoRA release:
5
+ * Apache-2.0, MIT, BSD-2-Clause, BSD-3-Clause, CC0-1.0, CC-BY-4.0
6
+ *
7
+ * Excluded / held models (FLUX.1-dev, FLUX.2-dev, FLUX.2-klein-9B, SD 3.5,
8
+ * Qwen-Image 2.1, community SDXL merges, and the held Portlight `plsvessel`
9
+ * LoRA trained on 336 Nano Banana-derived pairs) are explicitly flagged
10
+ * `cc_by_4_eligible: false` and `excluded: true`.
11
+ */
12
+
13
+ import { inputError } from './errors.js';
14
+
15
+ export const CLEARED_SPDX_IDS = Object.freeze([
16
+ 'Apache-2.0',
17
+ 'MIT',
18
+ 'BSD-2-Clause',
19
+ 'BSD-3-Clause',
20
+ 'CC0-1.0',
21
+ 'CC-BY-4.0',
22
+ ]);
23
+
24
+ export const QUARANTINED_PATTERNS = Object.freeze([
25
+ /plsvessel/i,
26
+ /nano[_-\s]?banana/i,
27
+ /held[_-\s]?portlight/i,
28
+ ]);
29
+
30
+ const KNOWN_CATALOG = Object.freeze([
31
+ // Quarantined / held Portlight distillation artifacts (§5.3, §8.1)
32
+ {
33
+ pattern: /^(?:plsvessel|.*nano[_-\s]?banana.*)/i,
34
+ spdx_id: 'PROPRIETARY-HELD-DISTILL',
35
+ url: 'https://ai.google.dev/gemini-api/terms',
36
+ verified_at: '2026-10-25',
37
+ cc_by_4_eligible: false,
38
+ excluded: true,
39
+ quarantined: true,
40
+ status: 'held',
41
+ reason: 'Held Portlight LoRA trained on 336 Nano Banana-derived pairs; not eligible for CC-BY-4.0 release or training',
42
+ },
43
+ // Excluded Qwen-Image 2.1 proprietary API (§5.3)
44
+ {
45
+ pattern: /^qwen[_-]image[_-]2\.1/i,
46
+ spdx_id: 'PROPRIETARY-API',
47
+ url: 'https://www.alibabacloud.com/help/en/model-studio/',
48
+ verified_at: '2026-10-25',
49
+ cc_by_4_eligible: false,
50
+ excluded: true,
51
+ status: 'excluded',
52
+ reason: 'Proprietary API-only model; no open weights',
53
+ },
54
+ // Cleared Qwen-Image 20B and VAE (§5.2)
55
+ {
56
+ pattern: /^qwen[_-]image/i,
57
+ spdx_id: 'Apache-2.0',
58
+ url: 'https://github.com/QwenLM/Qwen-Image/blob/main/LICENSE',
59
+ verified_at: '2026-10-25',
60
+ cc_by_4_eligible: true,
61
+ excluded: false,
62
+ status: 'cleared',
63
+ },
64
+ // Cleared Qwen 2.5 VL 7B text encoder (§5.2)
65
+ {
66
+ pattern: /^qwen[_-]?2\.5[_-]vl[_-]7b/i,
67
+ spdx_id: 'Apache-2.0',
68
+ url: 'https://github.com/QwenLM/Qwen2.5-VL/blob/main/LICENSE',
69
+ verified_at: '2026-10-28',
70
+ cc_by_4_eligible: true,
71
+ excluded: false,
72
+ status: 'cleared',
73
+ },
74
+ // Cleared FLUX.2 [klein] 4B and FLUX.2 VAE (§5.2)
75
+ {
76
+ pattern: /^(?:flux[_-]?2[_-]klein[_-](?:base[_-])?4b|flux2[_-]vae|ae\.safetensors)/i,
77
+ spdx_id: 'Apache-2.0',
78
+ url: 'https://huggingface.co/black-forest-labs/FLUX.2-klein-4B/blob/main/LICENSE.md',
79
+ verified_at: '2026-10-25',
80
+ cc_by_4_eligible: true,
81
+ excluded: false,
82
+ status: 'cleared',
83
+ },
84
+ // Cleared Muse Glimmer 30B / Qwen3-VL-30B-A3B-Instruct / Qwen2.5-VL-32B (§5.2)
85
+ {
86
+ pattern: /^qwen(?:3[_-]vl[_-]30b[_-]a3b|2\.5[_-]vl[_-]32b|2\.5[:_-]14b|3[:_-]14b)/i,
87
+ spdx_id: 'Apache-2.0',
88
+ url: 'https://huggingface.co/Qwen/Qwen3-VL-30B-A3B-Instruct/blob/main/LICENSE',
89
+ verified_at: '2026-10-25',
90
+ cc_by_4_eligible: true,
91
+ excluded: false,
92
+ status: 'cleared',
93
+ },
94
+ // Excluded FLUX.1 [dev] (§5.3)
95
+ {
96
+ pattern: /^flux[_-]?1[_-]dev/i,
97
+ spdx_id: 'FLUX-1-DEV-NC',
98
+ url: 'https://huggingface.co/black-forest-labs/FLUX.1-dev/blob/main/LICENSE.md',
99
+ verified_at: '2026-10-25',
100
+ cc_by_4_eligible: false,
101
+ excluded: true,
102
+ status: 'excluded',
103
+ reason: 'FLUX.1 [dev] Non-Commercial License restricts model and derivatives',
104
+ },
105
+ // Excluded FLUX.2 [dev] and FLUX.2 [klein] 9B (§5.3)
106
+ {
107
+ pattern: /^flux[_-]?2[_-](?:dev|klein[_-](?:base[_-])?9b)/i,
108
+ spdx_id: 'FLUX-2-NC',
109
+ url: 'https://huggingface.co/black-forest-labs/FLUX.2-dev/blob/main/LICENSE.md',
110
+ verified_at: '2026-10-25',
111
+ cc_by_4_eligible: false,
112
+ excluded: true,
113
+ status: 'excluded',
114
+ reason: 'FLUX.2 Non-Commercial License',
115
+ },
116
+ // Excluded Stable Diffusion 3.5 family (§5.3)
117
+ {
118
+ pattern: /^(?:sd3\.?5|stable[_-]diffusion[_-]3\.?5)/i,
119
+ spdx_id: 'STABILITY-COMMUNITY',
120
+ url: 'https://huggingface.co/stabilityai/stable-diffusion-3.5-large/blob/main/LICENSE.md',
121
+ verified_at: '2026-10-25',
122
+ cc_by_4_eligible: false,
123
+ excluded: true,
124
+ status: 'excluded',
125
+ reason: 'Stability AI Community License revenue cap and derivative terms',
126
+ },
127
+ // Excluded community SDXL merges (§5.3)
128
+ {
129
+ pattern: /^(?:dreamshaper|juggernaut|pony|illustrious|animagine)/i,
130
+ spdx_id: 'UNCLEARED-COMMUNITY-MERGE',
131
+ url: null,
132
+ verified_at: '2026-10-25',
133
+ cc_by_4_eligible: false,
134
+ excluded: true,
135
+ status: 'excluded',
136
+ reason: 'Community SDXL merge without audited upstream weight lineage',
137
+ },
138
+ ]);
139
+
140
+ /**
141
+ * Check whether a string references a quarantined/held artifact (`plsvessel`, `nano-banana`).
142
+ */
143
+ export function isQuarantinedName(value) {
144
+ if (!value || typeof value !== 'string') return false;
145
+ return QUARANTINED_PATTERNS.some((re) => re.test(value));
146
+ }
147
+
148
+ /**
149
+ * Normalize a licence descriptor into the canonical `{ spdx_id, licence_id, url, licence_url, verified_at, date_read, cc_by_4_eligible, excluded, status }` shape.
150
+ */
151
+ function normalizeLicenceObject(raw) {
152
+ const spdxId = raw.spdx_id || raw.licence_id || null;
153
+ const url = raw.url || raw.licence_url || null;
154
+ const verifiedAt = raw.verified_at || raw.date_read || null;
155
+ const excluded = Boolean(raw.excluded);
156
+ const quarantined = Boolean(raw.quarantined);
157
+ const eligible =
158
+ !excluded &&
159
+ !quarantined &&
160
+ raw.cc_by_4_eligible !== false &&
161
+ Boolean(spdxId && CLEARED_SPDX_IDS.includes(spdxId)) &&
162
+ typeof url === 'string' &&
163
+ url.trim().length > 0 &&
164
+ typeof verifiedAt === 'string' &&
165
+ verifiedAt.trim().length > 0;
166
+ return {
167
+ spdx_id: spdxId,
168
+ licence_id: spdxId,
169
+ url,
170
+ licence_url: url,
171
+ verified_at: verifiedAt,
172
+ date_read: verifiedAt,
173
+ cc_by_4_eligible: eligible,
174
+ excluded,
175
+ ...(quarantined ? { quarantined: true } : {}),
176
+ status: raw.status || (eligible ? 'cleared' : excluded ? 'excluded' : 'unknown'),
177
+ ...(raw.reason ? { reason: raw.reason } : {}),
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Resolve licence metadata for a model or LoRA filename.
183
+ *
184
+ * @param {string} name - Model or LoRA filename
185
+ * @param {Object|null} [explicitLicence] - Explicit override `{ licence_id, licence_url, date_read }`
186
+ * @param {Object|null} [modelLicences] - Map of `filename -> licence` overrides
187
+ */
188
+ export function resolveModelLicence(name, explicitLicence = null, modelLicences = null) {
189
+ if (!name || typeof name !== 'string') {
190
+ return normalizeLicenceObject({ status: 'unknown', cc_by_4_eligible: false });
191
+ }
192
+
193
+ // Always check catalog first for quarantined/excluded models so an override cannot mask a held model.
194
+ const baseName = name.split(/[\\/]/).pop();
195
+ const catalogMatch = KNOWN_CATALOG.find((entry) => entry.pattern.test(baseName));
196
+ if (catalogMatch && catalogMatch.excluded) {
197
+ return normalizeLicenceObject(catalogMatch);
198
+ }
199
+
200
+ const override = explicitLicence || (modelLicences && (modelLicences[name] || modelLicences[baseName]));
201
+ if (override && typeof override === 'object') {
202
+ return normalizeLicenceObject(override);
203
+ }
204
+
205
+ if (catalogMatch) {
206
+ return normalizeLicenceObject(catalogMatch);
207
+ }
208
+
209
+ return normalizeLicenceObject({ status: 'unknown', cc_by_4_eligible: false });
210
+ }
211
+
212
+ /**
213
+ * Check whether a model/LoRA entry carries a non-empty sha256 digest.
214
+ */
215
+ export function hasModelSha256(entry) {
216
+ if (!entry || typeof entry !== 'object') return false;
217
+ return typeof entry.sha256 === 'string' && entry.sha256.trim().length > 0;
218
+ }
219
+
220
+ /**
221
+ * Normalize a model identifier to its canonical family key (DESIGN-v3 §12.5).
222
+ */
223
+ export function normalizeModelFamilyKey(modelId) {
224
+ if (!modelId || typeof modelId !== 'string') return '';
225
+ const raw = modelId.trim().toLowerCase();
226
+ if (!raw) return '';
227
+ if (raw.startsWith('human:') || raw === 'human') return 'human';
228
+ if (raw.startsWith('rule-based:')) return 'rule-based';
229
+ const stripped = raw
230
+ .replace(/^model:/, '')
231
+ .replace(/@sha256:[0-9a-f]{64}$/, '')
232
+ .replace(/\.(safetensors|ckpt|pt|pth|bin|gguf)$/, '');
233
+ if (stripped.includes('glimmer') || stripped.includes('muse-glimmer') || stripped.includes('muse_glimmer')) {
234
+ return 'family:muse-glimmer-30b';
235
+ }
236
+ if (stripped.includes('qwen')) {
237
+ return 'family:qwen';
238
+ }
239
+ if (stripped.includes('gemma')) {
240
+ return 'family:gemma';
241
+ }
242
+ if (stripped.includes('internvl')) {
243
+ return 'family:internvl';
244
+ }
245
+ if (stripped.includes('siglip') || stripped.includes('ai-eyes')) {
246
+ return 'family:siglip2';
247
+ }
248
+ if (stripped.includes('flux') || stripped === 'ae') {
249
+ return 'family:flux';
250
+ }
251
+ if (/^(?:sd3\.?5|stable[_-]diffusion[_-]3\.?5)/.test(stripped)) {
252
+ return 'family:sd3';
253
+ }
254
+ if (
255
+ stripped.startsWith('sdxl') ||
256
+ stripped.includes('stable-diffusion-xl') ||
257
+ stripped.includes('stable_diffusion_xl') ||
258
+ /^(?:dreamshaper|juggernaut|pony|illustrious|animagine)/.test(stripped)
259
+ ) {
260
+ return 'family:sdxl';
261
+ }
262
+ return stripped;
263
+ }
264
+
265
+ /**
266
+ * Check whether a model/LoRA pinning entry has a valid cleared SPDX licence,
267
+ * non-empty URL, non-empty date_read, and (by default) a non-empty sha256.
268
+ */
269
+ export function isClearedLicenceEntry(entry, { requireSha256 = true } = {}) {
270
+ if (!entry || typeof entry !== 'object') return false;
271
+ if (entry.name && isQuarantinedName(entry.name)) return false;
272
+ if (requireSha256 && !hasModelSha256(entry)) return false;
273
+ const lic = entry.licence || {};
274
+ const spdxId = entry.licence_id || lic.spdx_id || lic.licence_id;
275
+ const url = entry.licence_url || lic.url || lic.licence_url;
276
+ const dateRead = entry.date_read || lic.verified_at || lic.date_read;
277
+ if (lic.excluded === true || lic.cc_by_4_eligible === false) return false;
278
+ if (!spdxId || !CLEARED_SPDX_IDS.includes(spdxId)) return false;
279
+ if (typeof url !== 'string' || !url.trim()) return false;
280
+ if (typeof dateRead !== 'string' || !dateRead.trim()) return false;
281
+ return true;
282
+ }
283
+
284
+ /**
285
+ * Enforce that every model and LoRA in a pinning block has a cleared licence
286
+ * unless `allowUnlicensedResearch` is explicitly set.
287
+ */
288
+ export function assertClearedPinningLicences(models, loras, { allowUnlicensedResearch = false, requireSha256 = false } = {}) {
289
+ if (allowUnlicensedResearch) return;
290
+ const entries = [
291
+ ...Object.entries(models || {}).map(([role, entry]) => ({ role, ...entry })),
292
+ ...(loras || []).map((entry, idx) => ({ role: `lora[${idx}]`, ...entry })),
293
+ ];
294
+ const bad = entries.filter((e) => !isClearedLicenceEntry(e, { requireSha256 }));
295
+ if (bad.length > 0) {
296
+ const summary = bad
297
+ .map((b) => `${b.role}:${b.name || '<unnamed>'} (${b.licence_id || 'unlicensed'})`)
298
+ .join(', ');
299
+ throw inputError(
300
+ 'MODEL_LICENCE_REJECTED',
301
+ `Model/LoRA licence check failed for: ${summary}`,
302
+ 'Only cleared SPDX weights (Apache-2.0, MIT, BSD-*, CC0-1.0, CC-BY-4.0) with a verified licence_url are eligible for CC-BY-4.0 release.',
303
+ );
304
+ }
305
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * NPY header reader — structural validation for .npy channel files in the
3
+ * asset-lane ingest contract (docs/asset-lane-kickoff.md).
4
+ *
5
+ * The asset contract lets a manifest declare a channel stored as a NumPy
6
+ * array (facet's standing sidecars, e.g. a styled/unstyled mask or a future
7
+ * per-texel view-owner map). sdlab never interprets the array's MEANING —
8
+ * it proves the file's declared dtype and shape match its actual header,
9
+ * the same way lib/png-meta.js proves a PNG's declared encoding matches
10
+ * its IHDR. Header only; the payload is hashed, not parsed.
11
+ *
12
+ * Format reference: NumPy NEP 1 (the .npy format, versions 1.0/2.0/3.0):
13
+ * magic "\x93NUMPY", one version byte pair, a little-endian header length
14
+ * (2 bytes for v1, 4 bytes for v2+), then a Python dict literal with keys
15
+ * 'descr', 'fortran_order', 'shape'.
16
+ *
17
+ * Schema 1.1.0 adds `readNpyValues` — still no MEANING, but a categorical
18
+ * npy channel declares its classes, and "declared ⊆ actual" is provable only
19
+ * by looking at the values. Kept to single-byte dtypes: those are what a
20
+ * per-pixel class/owner map is, and a reader that cannot honestly decode a
21
+ * dtype says so rather than skipping the proof silently.
22
+ */
23
+
24
+ import { inputError } from './errors.js';
25
+
26
+ const MAGIC = Buffer.from('\x93NUMPY', 'latin1');
27
+
28
+ function npyError(message, hint) {
29
+ return inputError('NPY_INVALID', message, hint);
30
+ }
31
+
32
+ /**
33
+ * Parse a .npy file's header.
34
+ *
35
+ * @param {Buffer} buf — at least the first ~64KB of the file (the whole
36
+ * file is fine; only the header is read)
37
+ * @param {string} [label] — filename for error messages
38
+ * @returns {{dtype:string, fortranOrder:boolean, shape:number[],
39
+ * version:string, headerLength:number, dataOffset:number}}
40
+ */
41
+ export function parseNpyHeader(buf, label = 'npy') {
42
+ if (buf.length < 10 || !buf.subarray(0, 6).equals(MAGIC)) {
43
+ throw npyError(`${label}: not a .npy file (bad magic).`);
44
+ }
45
+ const major = buf[6];
46
+ const minor = buf[7];
47
+ let headerLength;
48
+ let headerStart;
49
+ if (major === 1) {
50
+ headerLength = buf.readUInt16LE(8);
51
+ headerStart = 10;
52
+ } else if (major === 2 || major === 3) {
53
+ if (buf.length < 12) throw npyError(`${label}: truncated v${major} header.`);
54
+ headerLength = buf.readUInt32LE(8);
55
+ headerStart = 12;
56
+ } else {
57
+ throw npyError(`${label}: unsupported .npy format version ${major}.${minor}.`);
58
+ }
59
+ if (buf.length < headerStart + headerLength) {
60
+ throw npyError(`${label}: truncated header (declared ${headerLength} bytes).`);
61
+ }
62
+
63
+ const header = buf.toString('latin1', headerStart, headerStart + headerLength);
64
+
65
+ const descrMatch = header.match(/'descr'\s*:\s*'([^']+)'/);
66
+ const fortranMatch = header.match(/'fortran_order'\s*:\s*(True|False)/);
67
+ const shapeMatch = header.match(/'shape'\s*:\s*\(([^)]*)\)/);
68
+ if (!descrMatch || !fortranMatch || !shapeMatch) {
69
+ throw npyError(`${label}: header dict is missing descr/fortran_order/shape.`);
70
+ }
71
+
72
+ const shape = shapeMatch[1]
73
+ .split(',')
74
+ .map((s) => s.trim())
75
+ .filter((s) => s.length > 0)
76
+ .map((s) => {
77
+ const n = Number(s);
78
+ if (!Number.isInteger(n) || n < 0) {
79
+ throw npyError(`${label}: non-integer dimension "${s}" in shape.`);
80
+ }
81
+ return n;
82
+ });
83
+
84
+ return {
85
+ dtype: descrMatch[1],
86
+ fortranOrder: fortranMatch[1] === 'True',
87
+ shape,
88
+ version: `${major}.${minor}`,
89
+ headerLength,
90
+ dataOffset: headerStart + headerLength,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Single-byte dtypes whose values this contract can read. A categorical npy
96
+ * channel must declare one of these — see readNpyValues.
97
+ */
98
+ export const NPY_PROVABLE_DTYPES = new Set(['|i1', '|u1', '|b1']);
99
+
100
+ /**
101
+ * Read an npy payload as a typed array. Single-byte dtypes only, so element
102
+ * order is byte order and `fortran_order` does not change the VALUE SET a
103
+ * categorical proof examines (it changes which index means which pixel —
104
+ * that is the consumer's business, and numpy reads the flag itself).
105
+ *
106
+ * Also catches a truncation the header-only parse cannot: a file whose
107
+ * payload is shorter than its declared shape.
108
+ *
109
+ * @param {Buffer} buf — the whole file
110
+ * @param {ReturnType<typeof parseNpyHeader>} header
111
+ * @param {string} [label]
112
+ * @returns {Int8Array|Uint8Array}
113
+ */
114
+ export function readNpyValues(buf, header, label = 'npy') {
115
+ if (!NPY_PROVABLE_DTYPES.has(header.dtype)) {
116
+ throw npyError(
117
+ `${label}: dtype ${header.dtype} is not one this contract reads values from.`,
118
+ `Value proofs cover ${[...NPY_PROVABLE_DTYPES].join(', ')} — the single-byte dtypes a per-pixel class or owner map uses. Export the channel in one of those, or drop "categorical" and let it ride as an opaque declared array.`
119
+ );
120
+ }
121
+ const count = header.shape.reduce((a, b) => a * b, 1);
122
+ const available = buf.length - header.dataOffset;
123
+ if (available < count) {
124
+ throw npyError(
125
+ `${label}: truncated payload — shape ${JSON.stringify(header.shape)} needs ${count} byte(s), the file carries ${available} after the header.`
126
+ );
127
+ }
128
+ const Ctor = header.dtype === '|i1' ? Int8Array : Uint8Array;
129
+ return new Ctor(buf.buffer, buf.byteOffset + header.dataOffset, count);
130
+ }