@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.
- package/CHANGELOG.md +27 -0
- package/README.es.md +111 -59
- package/README.fr.md +117 -67
- package/README.hi.md +109 -70
- package/README.it.md +111 -60
- package/README.ja.md +116 -76
- package/README.md +58 -2
- package/README.pt-BR.md +119 -64
- package/README.zh.md +108 -67
- package/bin/sdlab.js +50 -1
- package/lib/adapters/ai-toolkit.js +187 -30
- package/lib/adapters/comfyui-workflows.js +84 -9
- package/lib/asset-ingest.js +575 -0
- package/lib/asset-masks.js +323 -0
- package/lib/asset-source.js +1176 -0
- package/lib/canon-author.js +350 -0
- package/lib/canon-review-gate.js +205 -0
- package/lib/colab-launcher-guards.js +285 -0
- package/lib/export.js +1 -1
- package/lib/freeze-gate.js +3 -0
- package/lib/generated-provenance.js +178 -5
- package/lib/lane1-render.js +668 -0
- package/lib/memory-store.js +663 -0
- package/lib/model-licences.js +305 -0
- package/lib/npy-meta.js +130 -0
- package/lib/png-meta.js +512 -0
- package/lib/records.js +54 -8
- package/lib/reingest-selected.js +15 -1
- package/lib/reingest.js +27 -3
- package/lib/run-manifest.js +59 -11
- package/lib/train-runs.js +214 -0
- package/lib/verifier.js +3 -0
- package/lib/verify-chain.js +367 -0
- package/package.json +2 -2
- package/scripts/asset-ingest.js +160 -0
- package/scripts/canon-draft.js +82 -0
- package/scripts/canon-freeze.js +4 -0
- package/scripts/colab/REAL_SERVICE_ASSUMPTIONS.md +14 -0
- package/scripts/colab/ai-toolkit-override-py315.txt +45 -0
- package/scripts/colab/colab_window.sh +587 -0
- package/scripts/colab/lane1_render.py +970 -0
- package/scripts/colab/launch_guards.py +564 -0
- package/scripts/colab/launch_lane1_stub.sh +309 -0
- package/scripts/colab/launch_lane3_stub.sh +366 -0
- package/scripts/colab/remote_runner.py +59 -0
- package/scripts/doctor.js +1 -0
- package/scripts/generate.js +38 -12
- package/scripts/verify.js +67 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C2 Colab launcher guards (DESIGN-v3 §9.2 item 6 — lessons v4k, v4p, v4q).
|
|
3
|
+
*
|
|
4
|
+
* Implements the four required launcher guards:
|
|
5
|
+
* 1. Independent wall watcher (`evaluateWallGuard`, v4k): honours `wallS` and
|
|
6
|
+
* `graceS` in ALL modes including `stub` mode.
|
|
7
|
+
* 2. Independent cap watcher (`evaluateCapGuard`, v4k): honours `cap` and
|
|
8
|
+
* `margin` in ALL modes including `stub` mode (`spend = max(drop, rate * elapsed_h)`).
|
|
9
|
+
* 3. `vitals.log` parser & `DEAD HARNESS` / `STALL` watcher (`parseVitalsLog`,
|
|
10
|
+
* `evaluateVitalsAndStallGuard`, v4p): trips `DEAD_HARNESS_TRIPPED` when 2
|
|
11
|
+
* consecutive `phase: run` samples have 0 active worker processes, and trips
|
|
12
|
+
* `STALL_GUARD_TRIPPED` when neither rows nor `vitals.log` advance in `stallS`.
|
|
13
|
+
* 4. Host-RAM / cgroup limit pre-load projection check & between-item fetch
|
|
14
|
+
* (`checkHostRamProjection`, `runBetweenItemFetch`, v4q): refuses any step
|
|
15
|
+
* whose projected anonymous host RAM (`projectedMb * 1.05 > limitMb`) would
|
|
16
|
+
* trigger a kernel SIGKILL (`rc=-9`), and copies completed artifacts listed
|
|
17
|
+
* in `current.json` before `current.json` is overwritten for the next item.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { inputError as rawInputError } from './errors.js';
|
|
23
|
+
|
|
24
|
+
function inputError(code, msg) {
|
|
25
|
+
return rawInputError(code, `${code}: ${msg}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const WORKER_PROC_RE = /\b(comfy|main\.py|ai-toolkit|run\.py|llama-server|ollama|lane1_render\.py)\b/i;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 1. Independent wall-clock watcher (v4k).
|
|
32
|
+
* Never disabled in `stub` mode: trips when `elapsedS >= wallS - graceS`.
|
|
33
|
+
*
|
|
34
|
+
* @param {Object} opts
|
|
35
|
+
* @param {string} [opts.mode='live']
|
|
36
|
+
* @param {number} opts.elapsedS
|
|
37
|
+
* @param {number} [opts.wallS=3000]
|
|
38
|
+
* @param {number} [opts.graceS=60]
|
|
39
|
+
* @returns {{ mode: string, tripped: boolean, code: string|null, remainingS: number, thresholdS: number }}
|
|
40
|
+
*/
|
|
41
|
+
export function evaluateWallGuard({
|
|
42
|
+
mode = 'live',
|
|
43
|
+
elapsedS,
|
|
44
|
+
wallS = 3000,
|
|
45
|
+
graceS = 60,
|
|
46
|
+
} = {}) {
|
|
47
|
+
const thresholdS = Math.max(0, Number(wallS) - Number(graceS));
|
|
48
|
+
const remainingS = thresholdS - Number(elapsedS);
|
|
49
|
+
const tripped = Number(elapsedS) >= thresholdS;
|
|
50
|
+
return {
|
|
51
|
+
mode,
|
|
52
|
+
tripped,
|
|
53
|
+
code: tripped ? 'WALL_GUARD_TRIPPED' : null,
|
|
54
|
+
remainingS,
|
|
55
|
+
thresholdS,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 2. Independent CCU spend cap watcher (v4k).
|
|
61
|
+
* Never disabled in `stub` mode: computes `spend = max(startBalance - currentBalance, rateCcuPerHour * elapsedH)`
|
|
62
|
+
* and trips when `spend >= cap - margin`.
|
|
63
|
+
*
|
|
64
|
+
* @param {Object} opts
|
|
65
|
+
* @param {string} [opts.mode='live']
|
|
66
|
+
* @param {number} [opts.cap=0.05]
|
|
67
|
+
* @param {number} [opts.margin=0.01]
|
|
68
|
+
* @param {number|null} [opts.startBalance=null]
|
|
69
|
+
* @param {number|null} [opts.currentBalance=null]
|
|
70
|
+
* @param {number} [opts.rateCcuPerHour=0]
|
|
71
|
+
* @param {number} [opts.elapsedS=0]
|
|
72
|
+
* @returns {{ mode: string, tripped: boolean, code: string|null, spendCcu: number, ceilingCcu: number }}
|
|
73
|
+
*/
|
|
74
|
+
export function evaluateCapGuard({
|
|
75
|
+
mode = 'live',
|
|
76
|
+
cap = 0.05,
|
|
77
|
+
margin = 0.01,
|
|
78
|
+
startBalance = null,
|
|
79
|
+
currentBalance = null,
|
|
80
|
+
rateCcuPerHour = 0,
|
|
81
|
+
elapsedS = 0,
|
|
82
|
+
} = {}) {
|
|
83
|
+
const balanceDrop =
|
|
84
|
+
startBalance !== null && currentBalance !== null
|
|
85
|
+
? Math.max(0, Number(startBalance) - Number(currentBalance))
|
|
86
|
+
: 0;
|
|
87
|
+
const rateEst = Math.max(0, (Number(rateCcuPerHour) * Number(elapsedS)) / 3600);
|
|
88
|
+
const spendCcu = Math.max(balanceDrop, rateEst);
|
|
89
|
+
const ceilingCcu = Math.max(0, Number(cap) - Number(margin));
|
|
90
|
+
const tripped = spendCcu >= ceilingCcu;
|
|
91
|
+
return {
|
|
92
|
+
mode,
|
|
93
|
+
tripped,
|
|
94
|
+
code: tripped ? 'CAP_GUARD_TRIPPED' : null,
|
|
95
|
+
spendCcu: Number(spendCcu.toFixed(6)),
|
|
96
|
+
ceilingCcu: Number(ceilingCcu.toFixed(6)),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Parse `vitals.log` blocks (`=== VITALS <iso> ===`) emitted by the VM vitals sampler (v4p).
|
|
102
|
+
*
|
|
103
|
+
* @param {string} vitalsText
|
|
104
|
+
* @returns {Array<{ timestamp: string, phase: string|null, workerProcCount: number, rawBlock: string }>}
|
|
105
|
+
*/
|
|
106
|
+
export function parseVitalsLog(vitalsText = '') {
|
|
107
|
+
const blocks = String(vitalsText)
|
|
108
|
+
.split(/^===\s*VITALS\s+/m)
|
|
109
|
+
.map((b) => b.trim())
|
|
110
|
+
.filter(Boolean);
|
|
111
|
+
|
|
112
|
+
const samples = [];
|
|
113
|
+
for (const block of blocks) {
|
|
114
|
+
const lines = block.split(/\r?\n/);
|
|
115
|
+
const header = lines[0] || '';
|
|
116
|
+
const tsMatch = /^([^\s=]+)/.exec(header);
|
|
117
|
+
const timestamp = tsMatch ? tsMatch[1] : '';
|
|
118
|
+
|
|
119
|
+
let phase = null;
|
|
120
|
+
let workerProcCount = 0;
|
|
121
|
+
let inProc = false;
|
|
122
|
+
|
|
123
|
+
for (let i = 1; i < lines.length; i++) {
|
|
124
|
+
const line = lines[i];
|
|
125
|
+
if (line.startsWith('PROC:')) {
|
|
126
|
+
inProc = true;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (line.startsWith('HARNESS:')) {
|
|
130
|
+
inProc = false;
|
|
131
|
+
const payload = line.slice('HARNESS:'.length).trim();
|
|
132
|
+
try {
|
|
133
|
+
const parsed = JSON.parse(payload);
|
|
134
|
+
phase = parsed.phase || null;
|
|
135
|
+
} catch {
|
|
136
|
+
const m = /"phase"\s*:\s*"([^"]+)"/.exec(payload) || /phase:\s*([a-z0-9_-]+)/i.exec(payload);
|
|
137
|
+
phase = m ? m[1] : null;
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (inProc && WORKER_PROC_RE.test(line)) {
|
|
142
|
+
workerProcCount++;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
samples.push({
|
|
147
|
+
timestamp,
|
|
148
|
+
phase,
|
|
149
|
+
workerProcCount,
|
|
150
|
+
rawBlock: block,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return samples;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 3. Evaluate the `DEAD HARNESS` and `STALL` watcher over `vitals.log` + idle timer (v4p).
|
|
158
|
+
*
|
|
159
|
+
* Trips `DEAD_HARNESS_TRIPPED` when the last 2 vitals samples both show
|
|
160
|
+
* `phase === 'run'` (or `phase.startsWith('run')`) and `workerProcCount === 0`.
|
|
161
|
+
* Otherwise trips `STALL_GUARD_TRIPPED` when `idleSeconds >= stallS`.
|
|
162
|
+
*
|
|
163
|
+
* @param {Object} opts
|
|
164
|
+
* @param {string} opts.vitalsText
|
|
165
|
+
* @param {number} [opts.idleSeconds=0]
|
|
166
|
+
* @param {number} [opts.stallS=300]
|
|
167
|
+
* @returns {{ tripped: boolean, code: string|null, reason: string|null, samplesCount: number }}
|
|
168
|
+
*/
|
|
169
|
+
export function evaluateVitalsAndStallGuard({
|
|
170
|
+
vitalsText = '',
|
|
171
|
+
idleSeconds = 0,
|
|
172
|
+
stallS = 300,
|
|
173
|
+
} = {}) {
|
|
174
|
+
const samples = parseVitalsLog(vitalsText);
|
|
175
|
+
if (samples.length >= 2) {
|
|
176
|
+
const lastTwo = samples.slice(-2);
|
|
177
|
+
const bothDeadInRun = lastTwo.every(
|
|
178
|
+
(s) => typeof s.phase === 'string' && s.phase.startsWith('run') && s.workerProcCount === 0
|
|
179
|
+
);
|
|
180
|
+
if (bothDeadInRun) {
|
|
181
|
+
return {
|
|
182
|
+
tripped: true,
|
|
183
|
+
code: 'DEAD_HARNESS_TRIPPED',
|
|
184
|
+
reason: '2 consecutive vitals samples in phase=run with 0 active worker processes',
|
|
185
|
+
samplesCount: samples.length,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (Number(idleSeconds) >= Number(stallS)) {
|
|
191
|
+
return {
|
|
192
|
+
tripped: true,
|
|
193
|
+
code: 'STALL_GUARD_TRIPPED',
|
|
194
|
+
reason: `No output row or vitals progress for ${idleSeconds}s (stallS=${stallS}s)`,
|
|
195
|
+
samplesCount: samples.length,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
tripped: false,
|
|
201
|
+
code: null,
|
|
202
|
+
reason: null,
|
|
203
|
+
samplesCount: samples.length,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 4a. Host-RAM / cgroup limit pre-load projection check (v4q).
|
|
209
|
+
* Refuses any step whose projected anonymous host RAM (`(currentAnonMb + projectedDeltaMb) * safetyFactor`)
|
|
210
|
+
* exceeds `min(memTotalMb, cgroupMaxMb)`.
|
|
211
|
+
*
|
|
212
|
+
* @param {Object} opts
|
|
213
|
+
* @param {number} opts.currentAnonMb
|
|
214
|
+
* @param {number} opts.projectedDeltaMb
|
|
215
|
+
* @param {number} opts.memTotalMb
|
|
216
|
+
* @param {number|null} [opts.cgroupMaxMb=null]
|
|
217
|
+
* @param {number} [opts.safetyFactor=1.05]
|
|
218
|
+
* @returns {{ ok: true, projectedMb: number, limitMb: number }}
|
|
219
|
+
*/
|
|
220
|
+
export function checkHostRamProjection({
|
|
221
|
+
currentAnonMb,
|
|
222
|
+
projectedDeltaMb,
|
|
223
|
+
memTotalMb,
|
|
224
|
+
cgroupMaxMb = null,
|
|
225
|
+
safetyFactor = 1.05,
|
|
226
|
+
}) {
|
|
227
|
+
const limits = [Number(memTotalMb)];
|
|
228
|
+
if (cgroupMaxMb !== null && cgroupMaxMb !== undefined && Number.isFinite(Number(cgroupMaxMb))) {
|
|
229
|
+
limits.push(Number(cgroupMaxMb));
|
|
230
|
+
}
|
|
231
|
+
const limitMb = Math.min(...limits);
|
|
232
|
+
const rawProjected = Number(currentAnonMb) + Number(projectedDeltaMb);
|
|
233
|
+
const projectedWithMargin = rawProjected * Number(safetyFactor);
|
|
234
|
+
|
|
235
|
+
if (projectedWithMargin > limitMb) {
|
|
236
|
+
throw inputError(
|
|
237
|
+
'HOST_RAM_PROJECTION_EXCEEDED',
|
|
238
|
+
`Projected anonymous host RAM ${projectedWithMargin.toFixed(1)} MB (${rawProjected} MB * ${safetyFactor}) exceeds host/cgroup ceiling ${limitMb} MB (v4q guard against kernel SIGKILL rc=-9).`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
ok: true,
|
|
244
|
+
projectedMb: Number(projectedWithMargin.toFixed(1)),
|
|
245
|
+
limitMb,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* 4b. Between-item fetch (v4q): copies every completed file listed in `current.json`
|
|
251
|
+
* from `remoteDir` to `localDir` BEFORE `current.json` is overwritten for `nextItem`.
|
|
252
|
+
*
|
|
253
|
+
* @param {Object} opts
|
|
254
|
+
* @param {string} opts.remoteDir
|
|
255
|
+
* @param {string} opts.localDir
|
|
256
|
+
* @param {Object|null} [opts.nextItem=null]
|
|
257
|
+
* @returns {{ copiedFiles: string[], previousItem: Object|null }}
|
|
258
|
+
*/
|
|
259
|
+
export function runBetweenItemFetch({ remoteDir, localDir, nextItem = null }) {
|
|
260
|
+
mkdirSync(localDir, { recursive: true });
|
|
261
|
+
const currentJsonPath = join(remoteDir, 'current.json');
|
|
262
|
+
const copiedFiles = [];
|
|
263
|
+
let previousItem = null;
|
|
264
|
+
|
|
265
|
+
if (existsSync(currentJsonPath)) {
|
|
266
|
+
previousItem = JSON.parse(readFileSync(currentJsonPath, 'utf-8'));
|
|
267
|
+
const files = Array.isArray(previousItem?.files) ? previousItem.files : [];
|
|
268
|
+
for (const relFile of files) {
|
|
269
|
+
const src = join(remoteDir, relFile);
|
|
270
|
+
if (existsSync(src)) {
|
|
271
|
+
const dst = join(localDir, relFile);
|
|
272
|
+
mkdirSync(join(dst, '..'), { recursive: true });
|
|
273
|
+
copyFileSync(src, dst);
|
|
274
|
+
copiedFiles.push(relFile);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
copyFileSync(currentJsonPath, join(localDir, 'current.json'));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (nextItem) {
|
|
281
|
+
writeFileSync(currentJsonPath, JSON.stringify(nextItem, null, 2) + '\n', 'utf-8');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { copiedFiles, previousItem };
|
|
285
|
+
}
|
package/lib/export.js
CHANGED
|
@@ -40,7 +40,7 @@ async function sha256File(filePath) {
|
|
|
40
40
|
* SDL-H6: validate that a record-relative asset_path does not escape
|
|
41
41
|
* projectRoot. Rejects absolute paths outright (ASSET_PATH_INVALID) and
|
|
42
42
|
* rejects any relative path that resolves outside projectRoot
|
|
43
|
-
* (ASSET_PATH_TRAVERSAL — e.g. "../../../../
|
|
43
|
+
* (ASSET_PATH_TRAVERSAL — e.g. "../../../../etc/passwd").
|
|
44
44
|
* Returns the resolved absolute path on success.
|
|
45
45
|
*
|
|
46
46
|
* This was previously inlined here only (D-007) and never ported to the
|
package/lib/freeze-gate.js
CHANGED
|
@@ -27,6 +27,9 @@ import { readFreezeStatus } from './freeze-stamp.js';
|
|
|
27
27
|
import { appendEvent, readEventsFor } from './freeze-events.js';
|
|
28
28
|
import { inputError } from './errors.js';
|
|
29
29
|
import { warn } from './log.js';
|
|
30
|
+
import { assertDirectorRatifiedForFreeze } from './canon-review-gate.js';
|
|
31
|
+
|
|
32
|
+
export { assertDirectorRatifiedForFreeze };
|
|
30
33
|
|
|
31
34
|
/**
|
|
32
35
|
* Best-effort freeze date for an entry: the `at` of its most recent freeze /
|
|
@@ -3,13 +3,117 @@
|
|
|
3
3
|
* attached to records created from selected run/batch outputs.
|
|
4
4
|
*
|
|
5
5
|
* Normalizes source context (run vs batch) into a single shape
|
|
6
|
-
* that traces back through brief → workflow → seed → config
|
|
6
|
+
* that traces back through brief → workflow → seed → config,
|
|
7
|
+
* plus G4 (model licences), G5 (owner + CC-BY-4.0), and G6 (canon link).
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import { readFileSync, existsSync } from 'node:fs';
|
|
10
11
|
import { join } from 'node:path';
|
|
11
12
|
import { createHash } from 'node:crypto';
|
|
12
13
|
import { warn } from './log.js';
|
|
14
|
+
import { STUDIO_OWNER, STUDIO_LICENCE } from './records.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve `{ canon_sha, entry_id, entry_sha256 }` from explicit metadata or
|
|
18
|
+
* the project's `canon-build/<canon_sha>/manifest.json` (`per_entity_hashes`).
|
|
19
|
+
*/
|
|
20
|
+
export function resolveCanonLink(
|
|
21
|
+
projectRoot,
|
|
22
|
+
{
|
|
23
|
+
canonSha = null,
|
|
24
|
+
entryId = null,
|
|
25
|
+
entrySha256 = null,
|
|
26
|
+
subjectId = null,
|
|
27
|
+
runManifest = null,
|
|
28
|
+
batchManifest = null,
|
|
29
|
+
item = null,
|
|
30
|
+
brief = null,
|
|
31
|
+
} = {},
|
|
32
|
+
) {
|
|
33
|
+
let resolvedCanonSha =
|
|
34
|
+
canonSha ||
|
|
35
|
+
item?.canon_sha ||
|
|
36
|
+
runManifest?.canon_sha ||
|
|
37
|
+
batchManifest?.canon_sha ||
|
|
38
|
+
brief?.canon_sha ||
|
|
39
|
+
null;
|
|
40
|
+
|
|
41
|
+
if (!resolvedCanonSha && projectRoot) {
|
|
42
|
+
const currentPath = join(projectRoot, 'canon-build', 'CURRENT');
|
|
43
|
+
if (existsSync(currentPath)) {
|
|
44
|
+
try {
|
|
45
|
+
const raw = readFileSync(currentPath, 'utf-8').trim();
|
|
46
|
+
if (raw) resolvedCanonSha = raw;
|
|
47
|
+
} catch {
|
|
48
|
+
// best-effort read of CURRENT pointer
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let resolvedEntryId =
|
|
54
|
+
entryId ||
|
|
55
|
+
item?.entry_id ||
|
|
56
|
+
runManifest?.entry_id ||
|
|
57
|
+
batchManifest?.entry_id ||
|
|
58
|
+
brief?.entry_id ||
|
|
59
|
+
subjectId ||
|
|
60
|
+
item?.subject_id ||
|
|
61
|
+
runManifest?.subject_id ||
|
|
62
|
+
batchManifest?.subject_id ||
|
|
63
|
+
brief?.subject_id ||
|
|
64
|
+
null;
|
|
65
|
+
|
|
66
|
+
let resolvedEntrySha =
|
|
67
|
+
entrySha256 ||
|
|
68
|
+
item?.entry_sha256 ||
|
|
69
|
+
runManifest?.entry_sha256 ||
|
|
70
|
+
batchManifest?.entry_sha256 ||
|
|
71
|
+
brief?.entry_sha256 ||
|
|
72
|
+
null;
|
|
73
|
+
|
|
74
|
+
if (resolvedCanonSha && projectRoot) {
|
|
75
|
+
const dirName = String(resolvedCanonSha).replace(/:/g, '-');
|
|
76
|
+
const manifestPath = existsSync(join(projectRoot, 'canon-build', String(resolvedCanonSha), 'manifest.json'))
|
|
77
|
+
? join(projectRoot, 'canon-build', String(resolvedCanonSha), 'manifest.json')
|
|
78
|
+
: join(projectRoot, 'canon-build', dirName, 'manifest.json');
|
|
79
|
+
if (existsSync(manifestPath)) {
|
|
80
|
+
try {
|
|
81
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
82
|
+
const hashes = manifest.per_entity_hashes || {};
|
|
83
|
+
if (resolvedEntryId) {
|
|
84
|
+
if (hashes[resolvedEntryId]) {
|
|
85
|
+
resolvedEntrySha = resolvedEntrySha || hashes[resolvedEntryId];
|
|
86
|
+
} else {
|
|
87
|
+
const suffix = `/${resolvedEntryId}`;
|
|
88
|
+
const matchKey = Object.keys(hashes).find((k) => k.endsWith(suffix));
|
|
89
|
+
if (matchKey) {
|
|
90
|
+
resolvedEntryId = matchKey;
|
|
91
|
+
resolvedEntrySha = resolvedEntrySha || hashes[matchKey];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
const keys = Object.keys(hashes);
|
|
96
|
+
if (keys.length === 1) {
|
|
97
|
+
resolvedEntryId = keys[0];
|
|
98
|
+
resolvedEntrySha = resolvedEntrySha || hashes[keys[0]];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// best-effort manifest lookup
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!resolvedCanonSha && !resolvedEntryId && !resolvedEntrySha) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
canon_sha: resolvedCanonSha,
|
|
113
|
+
entry_id: resolvedEntryId,
|
|
114
|
+
entry_sha256: resolvedEntrySha,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
13
117
|
|
|
14
118
|
/**
|
|
15
119
|
* Build a generation provenance payload for a selected output.
|
|
@@ -21,6 +125,9 @@ import { warn } from './log.js';
|
|
|
21
125
|
* @param {Object} [opts.batchManifest] — batch manifest if source is batch
|
|
22
126
|
* @param {Object} opts.item — the selection item (slot_or_output, filename, seed)
|
|
23
127
|
* @param {string} opts.projectRoot
|
|
128
|
+
* @param {string} [opts.canonSha]
|
|
129
|
+
* @param {string} [opts.entryId]
|
|
130
|
+
* @param {string} [opts.entrySha256]
|
|
24
131
|
* @returns {Object} generation_provenance block
|
|
25
132
|
*/
|
|
26
133
|
export function buildGeneratedProvenance({
|
|
@@ -30,8 +137,13 @@ export function buildGeneratedProvenance({
|
|
|
30
137
|
batchManifest,
|
|
31
138
|
item,
|
|
32
139
|
projectRoot,
|
|
140
|
+
canonSha = null,
|
|
141
|
+
entryId = null,
|
|
142
|
+
entrySha256 = null,
|
|
33
143
|
}) {
|
|
34
144
|
const prov = {
|
|
145
|
+
owner: STUDIO_OWNER,
|
|
146
|
+
licence: STUDIO_LICENCE,
|
|
35
147
|
source_type: sourceType,
|
|
36
148
|
source_id: sourceId,
|
|
37
149
|
run_id: runManifest.run_id,
|
|
@@ -45,12 +157,55 @@ export function buildGeneratedProvenance({
|
|
|
45
157
|
prov.batch_id = batchManifest.batch_id;
|
|
46
158
|
}
|
|
47
159
|
|
|
160
|
+
const matchedOutput = runManifest.outputs
|
|
161
|
+
? runManifest.outputs.find((o) => o.filename === item.filename)
|
|
162
|
+
: null;
|
|
163
|
+
|
|
48
164
|
// Seed from the item or from the run manifest outputs
|
|
49
165
|
if (item.seed !== undefined) {
|
|
50
166
|
prov.seed = item.seed;
|
|
51
|
-
} else if (
|
|
52
|
-
|
|
53
|
-
|
|
167
|
+
} else if (matchedOutput?.seed !== undefined) {
|
|
168
|
+
prov.seed = matchedOutput.seed;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Prompt and prompt_sha256 when available on the item or output
|
|
172
|
+
const promptText = item.prompt ?? matchedOutput?.prompt ?? null;
|
|
173
|
+
if (typeof promptText === 'string' && promptText.length > 0) {
|
|
174
|
+
prov.prompt = promptText;
|
|
175
|
+
prov.prompt_sha256 = 'sha256:' + createHash('sha256').update(promptText).digest('hex');
|
|
176
|
+
} else if (item.prompt_sha256 || matchedOutput?.prompt_sha256) {
|
|
177
|
+
prov.prompt_sha256 = item.prompt_sha256 || matchedOutput.prompt_sha256;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Comfy workflow sha and model licences from run manifest pinning (G4, G9)
|
|
181
|
+
const wfSha = runManifest.pinning?.comfy_workflow_sha ?? runManifest.comfy_workflow_sha ?? null;
|
|
182
|
+
if (wfSha) {
|
|
183
|
+
prov.comfy_workflow_sha = wfSha;
|
|
184
|
+
}
|
|
185
|
+
if (runManifest.pinning?.models || runManifest.pinning?.loras) {
|
|
186
|
+
prov.model_licences = {
|
|
187
|
+
cc_by_4_eligible: runManifest.pinning.cc_by_4_eligible ?? null,
|
|
188
|
+
models: Object.fromEntries(
|
|
189
|
+
Object.entries(runManifest.pinning.models || {}).map(([role, m]) => [
|
|
190
|
+
role,
|
|
191
|
+
{
|
|
192
|
+
name: m.name,
|
|
193
|
+
sha256: m.sha256 ?? null,
|
|
194
|
+
licence_id: m.licence_id ?? m.licence?.spdx_id ?? null,
|
|
195
|
+
licence_url: m.licence_url ?? m.licence?.url ?? null,
|
|
196
|
+
date_read: m.date_read ?? m.licence?.verified_at ?? null,
|
|
197
|
+
},
|
|
198
|
+
]),
|
|
199
|
+
),
|
|
200
|
+
loras: (runManifest.pinning.loras || []).map((l) => ({
|
|
201
|
+
name: l.name,
|
|
202
|
+
weight: l.weight ?? 1.0,
|
|
203
|
+
sha256: l.sha256 ?? null,
|
|
204
|
+
licence_id: l.licence_id ?? l.licence?.spdx_id ?? null,
|
|
205
|
+
licence_url: l.licence_url ?? l.licence?.url ?? null,
|
|
206
|
+
date_read: l.date_read ?? l.licence?.verified_at ?? null,
|
|
207
|
+
})),
|
|
208
|
+
};
|
|
54
209
|
}
|
|
55
210
|
|
|
56
211
|
// Adapter target from run manifest
|
|
@@ -58,15 +213,17 @@ export function buildGeneratedProvenance({
|
|
|
58
213
|
prov.adapter_target = runManifest.adapter_target;
|
|
59
214
|
}
|
|
60
215
|
|
|
216
|
+
let parsedBrief = null;
|
|
61
217
|
// Subject from run brief or batch manifest
|
|
62
218
|
if (batchManifest?.subject_id) {
|
|
63
219
|
prov.subject_id = batchManifest.subject_id;
|
|
64
|
-
} else {
|
|
220
|
+
} else if (projectRoot && runManifest.brief_id) {
|
|
65
221
|
// Try to read subject from brief
|
|
66
222
|
const briefPath = join(projectRoot, 'briefs', `${runManifest.brief_id}.json`);
|
|
67
223
|
if (existsSync(briefPath)) {
|
|
68
224
|
try {
|
|
69
225
|
const brief = JSON.parse(readFileSync(briefPath, 'utf-8'));
|
|
226
|
+
parsedBrief = brief;
|
|
70
227
|
if (brief.subject_id) prov.subject_id = brief.subject_id;
|
|
71
228
|
if (brief.parent_brief_id) prov.parent_brief_id = brief.parent_brief_id;
|
|
72
229
|
} catch (err) {
|
|
@@ -87,6 +244,22 @@ export function buildGeneratedProvenance({
|
|
|
87
244
|
}
|
|
88
245
|
}
|
|
89
246
|
|
|
247
|
+
const canonLink = resolveCanonLink(projectRoot, {
|
|
248
|
+
canonSha,
|
|
249
|
+
entryId,
|
|
250
|
+
entrySha256,
|
|
251
|
+
subjectId: prov.subject_id,
|
|
252
|
+
runManifest,
|
|
253
|
+
batchManifest,
|
|
254
|
+
item,
|
|
255
|
+
brief: parsedBrief,
|
|
256
|
+
});
|
|
257
|
+
if (canonLink) {
|
|
258
|
+
prov.canon_sha = canonLink.canon_sha;
|
|
259
|
+
prov.entry_id = canonLink.entry_id;
|
|
260
|
+
prov.entry_sha256 = canonLink.entry_sha256;
|
|
261
|
+
}
|
|
262
|
+
|
|
90
263
|
// Config fingerprint — hash the run manifest for reproducibility tracing
|
|
91
264
|
prov.config_fingerprint = createHash('sha256')
|
|
92
265
|
.update(JSON.stringify({
|