@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.
- package/CHANGELOG.md +58 -1
- package/README.es.md +40 -19
- package/README.fr.md +74 -53
- package/README.hi.md +67 -46
- package/README.it.md +42 -21
- package/README.ja.md +44 -23
- package/README.md +25 -4
- package/README.pt-BR.md +42 -21
- package/README.zh.md +66 -45
- package/bin/sdlab.js +51 -4
- package/lib/adapters/ai-toolkit.js +37 -5
- package/lib/adapters/comfyui-runner.js +114 -27
- package/lib/adapters/comfyui-workflows.js +10 -2
- package/lib/adapters/diffusers-lora.js +37 -5
- package/lib/adapters/generic-image-caption.js +43 -5
- package/lib/args.js +16 -1
- package/lib/batch-modes.js +29 -8
- package/lib/batch-runs.js +29 -1
- package/lib/batch-sheet-render.js +13 -3
- package/lib/brief-compiler.js +19 -0
- package/lib/candidate-sheet.js +327 -0
- package/lib/canon-build/build.js +146 -93
- package/lib/captions.js +57 -0
- package/lib/card.js +74 -2
- package/lib/comfyui.js +116 -18
- package/lib/config.js +116 -12
- package/lib/critique-engine.js +82 -3
- package/lib/critique-render.js +45 -0
- package/lib/eligibility.js +121 -9
- package/lib/eval-pack.js +14 -14
- package/lib/eval-runs.js +33 -0
- package/lib/export.js +122 -34
- package/lib/freeze-gate.js +69 -16
- package/lib/freeze-stamp.js +28 -3
- package/lib/generated-provenance.js +16 -1
- package/lib/implementation-packs.js +95 -17
- package/lib/ingest.js +396 -0
- package/lib/judgment-provenance.js +220 -0
- package/lib/measure.js +422 -0
- package/lib/paths.js +196 -7
- package/lib/reingest-selected.js +98 -16
- package/lib/run-manifest.js +394 -0
- package/lib/run-summary.js +12 -6
- package/lib/runtime-runs.js +45 -4
- package/lib/snapshot.js +121 -16
- package/lib/split.js +364 -48
- package/lib/training-manifests.js +114 -21
- package/lib/training-packages.js +147 -15
- package/lib/verifier.js +67 -0
- package/lib/workflow-profiles.js +24 -3
- package/package.json +3 -2
- package/runtime/comfyui/txt2img-qwen.json +38 -0
- package/runtime/comfyui/txt2img-standard.json +21 -0
- package/scripts/_resume.js +62 -0
- package/scripts/_shared-args.js +44 -0
- package/scripts/brief-compile.js +2 -1
- package/scripts/brief-show.js +2 -1
- package/scripts/canon-bind.js +2 -2
- package/scripts/canon-build.js +27 -20
- package/scripts/canon-drift.js +16 -7
- package/scripts/canon-freeze-status.js +14 -6
- package/scripts/canon-freeze.js +27 -11
- package/scripts/canon-unfreeze.js +17 -9
- package/scripts/card.js +2 -2
- package/scripts/compare.js +2 -2
- package/scripts/critique.js +48 -8
- package/scripts/curate.js +23 -2
- package/scripts/doctor.js +181 -55
- package/scripts/eligibility.js +42 -2
- package/scripts/eval-pack.js +2 -2
- package/scripts/eval-run.js +2 -2
- package/scripts/export.js +2 -2
- package/scripts/generate-controlnet.js +45 -25
- package/scripts/generate-identity.js +53 -23
- package/scripts/generate-ipadapter.js +44 -23
- package/scripts/generate.js +72 -5
- package/scripts/implementation-pack.js +2 -2
- package/scripts/ingest.js +145 -0
- package/scripts/init.js +73 -13
- package/scripts/measure.js +170 -0
- package/scripts/measure_image.py +458 -0
- package/scripts/migrate-records.js +142 -0
- package/scripts/painterly-test.js +4 -3
- package/scripts/painterly.js +7 -3
- package/scripts/qwen_generate.py +334 -13
- package/scripts/reingest.js +2 -2
- package/scripts/run-generate.js +3 -0
- package/scripts/sheet.js +142 -0
- package/scripts/snapshot.js +2 -2
- package/scripts/split.js +2 -2
- package/scripts/training-manifest.js +2 -2
- package/scripts/training-package.js +2 -2
- package/scripts/training-profile.js +2 -2
- package/scripts/workflow-list.js +2 -1
- package/scripts/workflow-show.js +2 -1
package/scripts/doctor.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
import { getProjectName } from '../lib/args.js';
|
|
17
|
-
import { REPO_ROOT } from '../lib/paths.js';
|
|
17
|
+
import { REPO_ROOT, getProjectRoot, getRuntimeDir } from '../lib/paths.js';
|
|
18
18
|
import { runtimeError, handleCliError } from '../lib/errors.js';
|
|
19
19
|
|
|
20
20
|
const REQUIRED_CONFIG_FILES = [
|
|
@@ -52,7 +52,16 @@ const REQUIRED_DIRS = [
|
|
|
52
52
|
];
|
|
53
53
|
|
|
54
54
|
function pass(msg) { console.log(` \x1b[32m✓\x1b[0m ${msg}`); }
|
|
55
|
-
|
|
55
|
+
// M1: fail() carried no hint — every OTHER command in this CLI prints a
|
|
56
|
+
// "Hint:" line (lib/errors.js handleCliError) when one is available, but
|
|
57
|
+
// doctor's ~40 failure sites just dropped it on the floor. Doctor is the
|
|
58
|
+
// tool people run when confused; its output IS the error-recovery story.
|
|
59
|
+
// hint is optional so existing terse call sites (a handful of genuinely
|
|
60
|
+
// self-explanatory failures) keep working unchanged.
|
|
61
|
+
function fail(msg, hint) {
|
|
62
|
+
console.log(` \x1b[31m✗\x1b[0m ${msg}`);
|
|
63
|
+
if (hint) console.log(` \x1b[33mHint:\x1b[0m ${hint}`);
|
|
64
|
+
}
|
|
56
65
|
function warn(msg) { console.log(` \x1b[33m⚠\x1b[0m ${msg}`); }
|
|
57
66
|
|
|
58
67
|
function tryParseJson(filePath) {
|
|
@@ -65,7 +74,23 @@ function tryParseJson(filePath) {
|
|
|
65
74
|
|
|
66
75
|
export async function run(argv = process.argv.slice(2)) {
|
|
67
76
|
const projectName = getProjectName(argv);
|
|
68
|
-
|
|
77
|
+
// SDL-C3-completion: resolve through the hardened getProjectRoot() instead of
|
|
78
|
+
// a raw join(REPO_ROOT, 'projects', projectName) — the raw join let a
|
|
79
|
+
// --project value like "../../ai-eyes-mcp" escape the projects/ directory
|
|
80
|
+
// entirely (path traversal). getProjectRoot() rejects any name containing
|
|
81
|
+
// "/", "\" or ".." (INPUT_UNSAFE_PROJECT_NAME) and re-verifies containment,
|
|
82
|
+
// and throws INPUT_UNKNOWN_PROJECT (with the same "run sdlab init" hint this
|
|
83
|
+
// function used to print by hand) when the project does not exist.
|
|
84
|
+
//
|
|
85
|
+
// That second throw is also the SDL-C4 fix: this function used to check
|
|
86
|
+
// existence itself with `if (!existsSync(projectDir)) { fail(...); return; }`
|
|
87
|
+
// — a bare `return` that resolved the run() promise successfully, so
|
|
88
|
+
// handleCliError's process.exit() never ran and `sdlab project doctor
|
|
89
|
+
// --project <missing>` exited 0. getProjectRoot() throws instead of
|
|
90
|
+
// returning, so a missing project now rejects run() and reaches the CLI's
|
|
91
|
+
// normal nonzero-exit error path — the ANDON gate this command IS (it's
|
|
92
|
+
// exactly what `npm run verify` and the CI smoke-test step call).
|
|
93
|
+
const projectDir = getProjectRoot(projectName);
|
|
69
94
|
|
|
70
95
|
console.log(`\x1b[1msdlab project doctor\x1b[0m — ${projectName}`);
|
|
71
96
|
console.log(` Path: ${projectDir}\n`);
|
|
@@ -75,11 +100,8 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
75
100
|
let warnings = 0;
|
|
76
101
|
|
|
77
102
|
// ── 1. Project directory exists ──
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
console.log(`\n Run: sdlab init ${projectName} --domain <domain>`);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
103
|
+
// Guaranteed true here — getProjectRoot() above already throws otherwise.
|
|
104
|
+
// Recorded as a pass so the check numbering below stays stable.
|
|
83
105
|
pass('Project directory exists');
|
|
84
106
|
passes++;
|
|
85
107
|
|
|
@@ -89,13 +111,17 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
89
111
|
for (const file of REQUIRED_CONFIG_FILES) {
|
|
90
112
|
const filePath = join(projectDir, file);
|
|
91
113
|
if (!existsSync(filePath)) {
|
|
92
|
-
fail(
|
|
114
|
+
fail(
|
|
115
|
+
`Missing: ${file}`,
|
|
116
|
+
`Run "sdlab init <name> --domain <domain>" to scaffold it, or hand-author ${file} — see ` +
|
|
117
|
+
`templates/domains/*/${file} for the expected shape.`
|
|
118
|
+
);
|
|
93
119
|
failures++;
|
|
94
120
|
continue;
|
|
95
121
|
}
|
|
96
122
|
const { data, error } = tryParseJson(filePath);
|
|
97
123
|
if (error) {
|
|
98
|
-
fail(`${file} — invalid JSON: ${error}`);
|
|
124
|
+
fail(`${file} — invalid JSON: ${error}`, `Open ${filePath} and fix the syntax error above.`);
|
|
99
125
|
failures++;
|
|
100
126
|
continue;
|
|
101
127
|
}
|
|
@@ -121,7 +147,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
121
147
|
if (meta) {
|
|
122
148
|
console.log('');
|
|
123
149
|
if (meta.name) { pass(`project.json: name = "${meta.name}"`); passes++; }
|
|
124
|
-
else { fail('project.json: missing "name"'); failures++; }
|
|
150
|
+
else { fail('project.json: missing "name"', 'Add a top-level "name" field matching the project directory name.'); failures++; }
|
|
125
151
|
|
|
126
152
|
if (meta.domain) { pass(`project.json: domain = "${meta.domain}"`); passes++; }
|
|
127
153
|
else { warn('project.json: no "domain" set'); warnings++; }
|
|
@@ -133,21 +159,42 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
133
159
|
console.log('');
|
|
134
160
|
const rules = constitution.rules || (Array.isArray(constitution) ? constitution : []);
|
|
135
161
|
if (rules.length === 0) {
|
|
136
|
-
fail(
|
|
162
|
+
fail(
|
|
163
|
+
'constitution.json: no rules defined',
|
|
164
|
+
'Add at least one rule object to the "rules" array — see templates/domains/*/constitution.json for the shape.'
|
|
165
|
+
);
|
|
137
166
|
failures++;
|
|
138
167
|
} else {
|
|
139
168
|
pass(`constitution.json: ${rules.length} rules`);
|
|
140
169
|
passes++;
|
|
141
170
|
|
|
142
171
|
// Check required rule fields
|
|
143
|
-
|
|
172
|
+
// M1: localize WHICH index is missing an id, and WHICH TWO indices
|
|
173
|
+
// collide on a duplicate — a rule id is not the record's only
|
|
174
|
+
// identity, and "duplicate rule ID X" without saying where left the
|
|
175
|
+
// operator grepping the file by hand.
|
|
176
|
+
const ruleIdFirstIndex = new Map(); // id -> first index that defined it
|
|
144
177
|
let ruleErrors = 0;
|
|
145
|
-
for (const rule of rules) {
|
|
146
|
-
if (!rule.id) {
|
|
147
|
-
|
|
148
|
-
|
|
178
|
+
for (const [i, rule] of rules.entries()) {
|
|
179
|
+
if (!rule.id) {
|
|
180
|
+
fail(`constitution.json: rule missing "id"`, `Add an "id" field to rules[${i}] (e.g. "STY-002").`);
|
|
181
|
+
ruleErrors++;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (ruleIdFirstIndex.has(rule.id)) {
|
|
185
|
+
fail(
|
|
186
|
+
`constitution.json: duplicate rule ID "${rule.id}"`,
|
|
187
|
+
`rules[${ruleIdFirstIndex.get(rule.id)}] and rules[${i}] both use id "${rule.id}" — give one of them a unique id.`
|
|
188
|
+
);
|
|
189
|
+
ruleErrors++;
|
|
190
|
+
} else {
|
|
191
|
+
ruleIdFirstIndex.set(rule.id, i);
|
|
192
|
+
}
|
|
149
193
|
if (!rule.dims || rule.dims.length === 0) {
|
|
150
|
-
fail(
|
|
194
|
+
fail(
|
|
195
|
+
`constitution.json: rule ${rule.id} has no "dims" (scoring dimensions)`,
|
|
196
|
+
`Add a non-empty "dims" array to rules[${i}] (e.g. ["style_consistency"]) — see rubric.json's "dimensions" for valid values.`
|
|
197
|
+
);
|
|
151
198
|
ruleErrors++;
|
|
152
199
|
}
|
|
153
200
|
}
|
|
@@ -168,18 +215,35 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
168
215
|
pass(`lanes.json: ${laneList.length} lanes`);
|
|
169
216
|
passes++;
|
|
170
217
|
|
|
171
|
-
// Check lane IDs unique
|
|
172
|
-
const
|
|
218
|
+
// Check lane IDs unique — same index-localization as constitution rules above.
|
|
219
|
+
const laneIdFirstIndex = new Map();
|
|
173
220
|
let laneErrors = 0;
|
|
174
|
-
for (const lane of laneList) {
|
|
175
|
-
if (!lane.id) {
|
|
176
|
-
|
|
177
|
-
|
|
221
|
+
for (const [i, lane] of laneList.entries()) {
|
|
222
|
+
if (!lane.id) {
|
|
223
|
+
fail('lanes.json: lane missing "id"', `Add an "id" field to lanes[${i}].`);
|
|
224
|
+
laneErrors++;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (laneIdFirstIndex.has(lane.id)) {
|
|
228
|
+
fail(
|
|
229
|
+
`lanes.json: duplicate lane ID "${lane.id}"`,
|
|
230
|
+
`lanes[${laneIdFirstIndex.get(lane.id)}] and lanes[${i}] both use id "${lane.id}" — give one of them a unique id.`
|
|
231
|
+
);
|
|
232
|
+
laneErrors++;
|
|
233
|
+
} else {
|
|
234
|
+
laneIdFirstIndex.set(lane.id, i);
|
|
235
|
+
}
|
|
178
236
|
|
|
179
237
|
// Check patterns compile
|
|
180
238
|
for (const pattern of lane.id_patterns || []) {
|
|
181
239
|
try { new RegExp(pattern); }
|
|
182
|
-
catch {
|
|
240
|
+
catch {
|
|
241
|
+
fail(
|
|
242
|
+
`lanes.json: lane "${lane.id}" has invalid pattern "${pattern}"`,
|
|
243
|
+
`Fix the regular expression syntax for lane "${lane.id}"'s id_patterns entry "${pattern}".`
|
|
244
|
+
);
|
|
245
|
+
laneErrors++;
|
|
246
|
+
}
|
|
183
247
|
}
|
|
184
248
|
}
|
|
185
249
|
if (laneErrors === 0) { pass('lanes.json: all lane IDs unique, patterns compile'); passes++; }
|
|
@@ -198,7 +262,10 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
198
262
|
console.log('');
|
|
199
263
|
const dims = rubric.dimensions || [];
|
|
200
264
|
if (dims.length === 0) {
|
|
201
|
-
fail(
|
|
265
|
+
fail(
|
|
266
|
+
'rubric.json: no dimensions defined',
|
|
267
|
+
'Add at least one dimension string to "dimensions" (e.g. ["style_consistency", "composition"]).'
|
|
268
|
+
);
|
|
202
269
|
failures++;
|
|
203
270
|
} else {
|
|
204
271
|
pass(`rubric.json: ${dims.length} dimensions`);
|
|
@@ -206,7 +273,10 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
206
273
|
}
|
|
207
274
|
|
|
208
275
|
if (!rubric.thresholds) {
|
|
209
|
-
fail(
|
|
276
|
+
fail(
|
|
277
|
+
'rubric.json: missing "thresholds"',
|
|
278
|
+
'Add a "thresholds" object with approved/borderline/rejected bands — see templates/domains/*/rubric.json for the shape.'
|
|
279
|
+
);
|
|
210
280
|
failures++;
|
|
211
281
|
} else {
|
|
212
282
|
pass('rubric.json: thresholds present');
|
|
@@ -224,7 +294,10 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
224
294
|
}
|
|
225
295
|
}
|
|
226
296
|
if (missingDims.size > 0) {
|
|
227
|
-
fail(
|
|
297
|
+
fail(
|
|
298
|
+
`rubric.json: constitution references dimensions not in rubric: ${[...missingDims].join(', ')}`,
|
|
299
|
+
`Add [${[...missingDims].map(d => `"${d}"`).join(', ')}] to rubric.json's "dimensions" array, or fix the referencing rule(s) in constitution.json.`
|
|
300
|
+
);
|
|
228
301
|
failures++;
|
|
229
302
|
} else {
|
|
230
303
|
pass('rubric.json: all constitution dimensions present in rubric');
|
|
@@ -239,7 +312,10 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
239
312
|
for (const [mode, ruleList] of Object.entries(rubric.failure_to_rules)) {
|
|
240
313
|
for (const ruleId of ruleList) {
|
|
241
314
|
if (!ruleIds.has(ruleId)) {
|
|
242
|
-
fail(
|
|
315
|
+
fail(
|
|
316
|
+
`rubric.json: failure mode "${mode}" references unknown rule "${ruleId}"`,
|
|
317
|
+
`Add a rule with id "${ruleId}" to constitution.json, or remove it from rubric.json's failure_to_rules["${mode}"].`
|
|
318
|
+
);
|
|
243
319
|
badRefs++;
|
|
244
320
|
}
|
|
245
321
|
}
|
|
@@ -269,7 +345,10 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
269
345
|
const order = terminology[orderKey] || [];
|
|
270
346
|
for (const name of order) {
|
|
271
347
|
if (!terminology.groups[name]) {
|
|
272
|
-
fail(
|
|
348
|
+
fail(
|
|
349
|
+
`terminology.json: ${orderKey} references unknown group "${name}"`,
|
|
350
|
+
`Add "${name}" to terminology.json's "groups" object, or remove it from "${orderKey}".`
|
|
351
|
+
);
|
|
273
352
|
failures++;
|
|
274
353
|
}
|
|
275
354
|
}
|
|
@@ -280,11 +359,23 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
280
359
|
for (const [gName, group] of Object.entries(terminology.groups)) {
|
|
281
360
|
for (const pattern of group.id_patterns || []) {
|
|
282
361
|
try { new RegExp(pattern); }
|
|
283
|
-
catch {
|
|
362
|
+
catch {
|
|
363
|
+
fail(
|
|
364
|
+
`terminology.json: group "${gName}" has invalid id_pattern "${pattern}"`,
|
|
365
|
+
`Fix the regular expression syntax for group "${gName}"'s id_patterns entry "${pattern}".`
|
|
366
|
+
);
|
|
367
|
+
patternErrors++;
|
|
368
|
+
}
|
|
284
369
|
}
|
|
285
370
|
for (const pattern of group.prompt_patterns || []) {
|
|
286
371
|
try { new RegExp(pattern); }
|
|
287
|
-
catch {
|
|
372
|
+
catch {
|
|
373
|
+
fail(
|
|
374
|
+
`terminology.json: group "${gName}" has invalid prompt_pattern "${pattern}"`,
|
|
375
|
+
`Fix the regular expression syntax for group "${gName}"'s prompt_patterns entry "${pattern}".`
|
|
376
|
+
);
|
|
377
|
+
patternErrors++;
|
|
378
|
+
}
|
|
288
379
|
}
|
|
289
380
|
}
|
|
290
381
|
if (patternErrors === 0) { pass('terminology.json: all detection patterns compile'); passes++; }
|
|
@@ -305,46 +396,56 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
305
396
|
let wfErrors = 0;
|
|
306
397
|
for (const file of wfFiles) {
|
|
307
398
|
const { data, error } = tryParseJson(join(workflowDir, file));
|
|
308
|
-
if (error) {
|
|
399
|
+
if (error) {
|
|
400
|
+
fail(`workflows/profiles/${file} — invalid JSON: ${error}`, `Open workflows/profiles/${file} and fix the syntax error above.`);
|
|
401
|
+
wfErrors++;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
309
404
|
|
|
310
405
|
const missing = ['workflow_id', 'label', 'lane_id', 'output_mode', 'output_count',
|
|
311
406
|
'prompt_strategy', 'negative_strategy', 'canon_focus', 'drift_guards', 'runtime_defaults']
|
|
312
407
|
.filter(f => data[f] === undefined || data[f] === null);
|
|
313
408
|
if (missing.length > 0) {
|
|
314
|
-
fail(`workflows/profiles/${file} — missing fields: ${missing.join(', ')}`);
|
|
409
|
+
fail(`workflows/profiles/${file} — missing fields: ${missing.join(', ')}`, `Add [${missing.join(', ')}] to workflows/profiles/${file}.`);
|
|
315
410
|
wfErrors++;
|
|
316
411
|
continue;
|
|
317
412
|
}
|
|
318
413
|
|
|
319
414
|
if (data.lane_id && laneIds.size > 0 && !laneIds.has(data.lane_id)) {
|
|
320
|
-
fail(
|
|
415
|
+
fail(
|
|
416
|
+
`workflows/profiles/${file} — lane_id "${data.lane_id}" not found in lanes.json`,
|
|
417
|
+
`Use one of lanes.json's existing lane ids [${[...laneIds].join(', ')}], or add "${data.lane_id}" as a new lane.`
|
|
418
|
+
);
|
|
321
419
|
wfErrors++;
|
|
322
420
|
}
|
|
323
421
|
|
|
324
422
|
if (data.subject_mode && !VALID_SUBJECT_MODES.includes(data.subject_mode)) {
|
|
325
|
-
fail(`workflows/profiles/${file} — invalid subject_mode "${data.subject_mode}"`);
|
|
423
|
+
fail(`workflows/profiles/${file} — invalid subject_mode "${data.subject_mode}"`, `subject_mode must be one of: ${VALID_SUBJECT_MODES.join(', ')}.`);
|
|
326
424
|
wfErrors++;
|
|
327
425
|
}
|
|
328
426
|
|
|
329
427
|
if (!VALID_OUTPUT_MODES.includes(data.output_mode)) {
|
|
330
|
-
fail(`workflows/profiles/${file} — invalid output_mode "${data.output_mode}"`);
|
|
428
|
+
fail(`workflows/profiles/${file} — invalid output_mode "${data.output_mode}"`, `output_mode must be one of: ${VALID_OUTPUT_MODES.join(', ')}.`);
|
|
331
429
|
wfErrors++;
|
|
332
430
|
}
|
|
333
431
|
|
|
334
432
|
const ps = data.prompt_strategy;
|
|
335
433
|
if (!ps || !Array.isArray(ps.style_prefix) || !ps.structure || !Array.isArray(ps.must_include)) {
|
|
336
|
-
fail(
|
|
434
|
+
fail(
|
|
435
|
+
`workflows/profiles/${file} — prompt_strategy needs style_prefix[], structure, must_include[]`,
|
|
436
|
+
`Add prompt_strategy.style_prefix (array), .structure (string), and .must_include (array) to workflows/profiles/${file}.`
|
|
437
|
+
);
|
|
337
438
|
wfErrors++;
|
|
338
439
|
}
|
|
339
440
|
|
|
340
441
|
const ns = data.negative_strategy;
|
|
341
442
|
if (!ns || !Array.isArray(ns.must_avoid)) {
|
|
342
|
-
fail(`workflows/profiles/${file} — negative_strategy needs must_avoid[]`);
|
|
443
|
+
fail(`workflows/profiles/${file} — negative_strategy needs must_avoid[]`, `Add negative_strategy.must_avoid (array) to workflows/profiles/${file}.`);
|
|
343
444
|
wfErrors++;
|
|
344
445
|
}
|
|
345
446
|
|
|
346
447
|
if (!data.runtime_defaults?.adapter_target) {
|
|
347
|
-
fail(`workflows/profiles/${file} — runtime_defaults needs adapter_target`);
|
|
448
|
+
fail(`workflows/profiles/${file} — runtime_defaults needs adapter_target`, `Add runtime_defaults.adapter_target (e.g. "comfyui") to workflows/profiles/${file}.`);
|
|
348
449
|
wfErrors++;
|
|
349
450
|
}
|
|
350
451
|
}
|
|
@@ -377,34 +478,41 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
377
478
|
let bmErrors = 0;
|
|
378
479
|
for (const file of bmFiles) {
|
|
379
480
|
const { data, error } = tryParseJson(join(batchModesDir, file));
|
|
380
|
-
if (error) {
|
|
481
|
+
if (error) {
|
|
482
|
+
fail(`workflows/batch-modes/${file} — invalid JSON: ${error}`, `Open workflows/batch-modes/${file} and fix the syntax error above.`);
|
|
483
|
+
bmErrors++;
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
381
486
|
|
|
382
|
-
if (!data.mode_id) { fail(`workflows/batch-modes/${file} — missing mode_id`); bmErrors++; }
|
|
487
|
+
if (!data.mode_id) { fail(`workflows/batch-modes/${file} — missing mode_id`, `Add a "mode_id" field to workflows/batch-modes/${file}.`); bmErrors++; }
|
|
383
488
|
if (!data.batch_type || !VALID_BATCH_TYPES.includes(data.batch_type)) {
|
|
384
|
-
fail(`workflows/batch-modes/${file} — invalid batch_type "${data.batch_type}"`);
|
|
489
|
+
fail(`workflows/batch-modes/${file} — invalid batch_type "${data.batch_type}"`, `batch_type must be one of: ${VALID_BATCH_TYPES.join(', ')}.`);
|
|
385
490
|
bmErrors++;
|
|
386
491
|
}
|
|
387
492
|
if (data.subject_mode && !VALID_SUBJECT_MODES_BM.includes(data.subject_mode)) {
|
|
388
|
-
fail(`workflows/batch-modes/${file} — invalid subject_mode "${data.subject_mode}"`);
|
|
493
|
+
fail(`workflows/batch-modes/${file} — invalid subject_mode "${data.subject_mode}"`, `subject_mode must be one of: ${VALID_SUBJECT_MODES_BM.join(', ')}.`);
|
|
389
494
|
bmErrors++;
|
|
390
495
|
}
|
|
391
496
|
if (data.base_workflow_id && wfFiles.length > 0 && !wfFiles.includes(data.base_workflow_id)) {
|
|
392
|
-
fail(
|
|
497
|
+
fail(
|
|
498
|
+
`workflows/batch-modes/${file} — base_workflow_id "${data.base_workflow_id}" not found in profiles`,
|
|
499
|
+
`Use one of the existing workflow ids [${wfFiles.join(', ')}], or add a matching profile under workflows/profiles/.`
|
|
500
|
+
);
|
|
393
501
|
bmErrors++;
|
|
394
502
|
}
|
|
395
503
|
if (!Array.isArray(data.variant_plan) || data.variant_plan.length === 0) {
|
|
396
|
-
fail(`workflows/batch-modes/${file} — variant_plan must be non-empty array`);
|
|
504
|
+
fail(`workflows/batch-modes/${file} — variant_plan must be non-empty array`, `Add at least one slot to variant_plan in workflows/batch-modes/${file}.`);
|
|
397
505
|
bmErrors++;
|
|
398
506
|
} else {
|
|
399
507
|
const slotIds = data.variant_plan.map(s => s.slot_id);
|
|
400
508
|
const dupes = slotIds.filter((id, i) => slotIds.indexOf(id) !== i);
|
|
401
509
|
if (dupes.length > 0) {
|
|
402
|
-
fail(`workflows/batch-modes/${file} — duplicate slot_ids: ${[...new Set(dupes)].join(', ')}`);
|
|
510
|
+
fail(`workflows/batch-modes/${file} — duplicate slot_ids: ${[...new Set(dupes)].join(', ')}`, `Give each entry in variant_plan a unique slot_id.`);
|
|
403
511
|
bmErrors++;
|
|
404
512
|
}
|
|
405
513
|
}
|
|
406
514
|
if (data.assembly?.layout && !VALID_LAYOUTS.includes(data.assembly.layout)) {
|
|
407
|
-
fail(`workflows/batch-modes/${file} — invalid assembly.layout "${data.assembly.layout}"`);
|
|
515
|
+
fail(`workflows/batch-modes/${file} — invalid assembly.layout "${data.assembly.layout}"`, `assembly.layout must be one of: ${VALID_LAYOUTS.join(', ')}.`);
|
|
408
516
|
bmErrors++;
|
|
409
517
|
}
|
|
410
518
|
}
|
|
@@ -424,17 +532,26 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
424
532
|
// ── 11. Runtime templates check ──
|
|
425
533
|
{
|
|
426
534
|
console.log('');
|
|
427
|
-
|
|
535
|
+
// PACKAGE-relative, not workspace-relative — `runtime/` ships inside the
|
|
536
|
+
// npm tarball (see getPackageRoot() in lib/paths.js). Joining it to
|
|
537
|
+
// REPO_ROOT made doctor report "runtime/comfyui/ — directory not found"
|
|
538
|
+
// for every installed user while the templates sat in the package.
|
|
539
|
+
const runtimeDir = join(getRuntimeDir(), 'comfyui');
|
|
428
540
|
if (existsSync(runtimeDir)) {
|
|
429
541
|
const templateFiles = readdirSync(runtimeDir).filter(f => f.endsWith('.json'));
|
|
430
542
|
if (templateFiles.length > 0) {
|
|
431
543
|
let templateErrors = 0;
|
|
432
544
|
for (const file of templateFiles) {
|
|
433
545
|
const { data, error } = tryParseJson(join(runtimeDir, file));
|
|
434
|
-
if (error) {
|
|
435
|
-
|
|
546
|
+
if (error) {
|
|
547
|
+
fail(`runtime/comfyui/${file}: invalid JSON — ${error}`, `Open ${join(runtimeDir, file)} and fix the syntax error above.`);
|
|
548
|
+
templateErrors++;
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (!data.template_id) { fail(`runtime/comfyui/${file}: missing template_id`, `Add a "template_id" field to ${file}.`); templateErrors++; }
|
|
436
552
|
if (!data.compatible_modes || !Array.isArray(data.compatible_modes)) {
|
|
437
|
-
fail(`runtime/comfyui/${file}: missing compatible_modes[]`);
|
|
553
|
+
fail(`runtime/comfyui/${file}: missing compatible_modes[]`, `Add a "compatible_modes" array to ${file}.`);
|
|
554
|
+
templateErrors++;
|
|
438
555
|
}
|
|
439
556
|
}
|
|
440
557
|
if (templateErrors === 0) {
|
|
@@ -464,8 +581,13 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
464
581
|
const sample = recordFiles.slice(0, 3);
|
|
465
582
|
for (const f of sample) {
|
|
466
583
|
const { data, error } = tryParseJson(join(recordsDir, f));
|
|
467
|
-
if (error) {
|
|
468
|
-
|
|
584
|
+
if (error) {
|
|
585
|
+
fail(`records/${f}: invalid JSON`, `Open records/${f} and fix the syntax error, or delete/regenerate it if it's an aborted write.`);
|
|
586
|
+
recordErrors++;
|
|
587
|
+
} else if (!data.id) {
|
|
588
|
+
fail(`records/${f}: missing "id"`, `Add an "id" field to records/${f} matching its filename stem.`);
|
|
589
|
+
recordErrors++;
|
|
590
|
+
}
|
|
469
591
|
}
|
|
470
592
|
if (recordErrors === 0) {
|
|
471
593
|
pass(`${recordFiles.length} records (spot-check passed)`);
|
|
@@ -485,7 +607,11 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
485
607
|
console.log(`═══ ${status} — ${passes} passed, ${failures} failed, ${warnings} warnings ═══`);
|
|
486
608
|
|
|
487
609
|
if (failures > 0) {
|
|
488
|
-
throw runtimeError(
|
|
610
|
+
throw runtimeError(
|
|
611
|
+
'PROJECT_UNHEALTHY',
|
|
612
|
+
`Project "${projectName}" has ${failures} issue(s).`,
|
|
613
|
+
`Fix the ✗ lines above — each one now carries its own Hint: with a concrete next action — then re-run: sdlab project doctor --project ${projectName}`
|
|
614
|
+
);
|
|
489
615
|
}
|
|
490
616
|
}
|
|
491
617
|
|
package/scripts/eligibility.js
CHANGED
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
import { readdir, readFile } from 'node:fs/promises';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { parseArgs, getProjectName } from '../lib/args.js';
|
|
13
|
-
import {
|
|
13
|
+
import { getProjectRoot } from '../lib/paths.js';
|
|
14
14
|
import { loadProjectConfig, loadSelectionProfile } from '../lib/config.js';
|
|
15
15
|
import { handleCliError } from '../lib/errors.js';
|
|
16
16
|
import { evaluateEligibility, categorizeExclusions } from '../lib/eligibility.js';
|
|
17
|
+
import { auditJudgmentProvenance, formatProvenanceWarning, formatReuseNote } from '../lib/judgment-provenance.js';
|
|
17
18
|
|
|
18
19
|
export async function run(argv = process.argv.slice(2)) {
|
|
19
20
|
const { flags } = parseArgs(argv, {
|
|
@@ -26,7 +27,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
26
27
|
});
|
|
27
28
|
|
|
28
29
|
const projectName = flags.project || getProjectName(argv);
|
|
29
|
-
const projectRoot =
|
|
30
|
+
const projectRoot = getProjectRoot(projectName);
|
|
30
31
|
|
|
31
32
|
const profile = loadSelectionProfile(projectRoot, flags.profile);
|
|
32
33
|
const config = loadProjectConfig(projectRoot);
|
|
@@ -42,10 +43,15 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
42
43
|
const eligible = [];
|
|
43
44
|
const excluded = [];
|
|
44
45
|
const nearMiss = [];
|
|
46
|
+
// Retained for the judgment-provenance audit below, which is a corpus-level
|
|
47
|
+
// question (is this rationale reused across records?) and cannot be answered
|
|
48
|
+
// one record at a time.
|
|
49
|
+
const allRecords = [];
|
|
45
50
|
|
|
46
51
|
for (const file of files) {
|
|
47
52
|
const raw = await readFile(join(recordsDir, file), 'utf-8');
|
|
48
53
|
const record = JSON.parse(raw);
|
|
54
|
+
allRecords.push(record);
|
|
49
55
|
|
|
50
56
|
const result = evaluateEligibility(record, profile, config.lanes, config.terminology);
|
|
51
57
|
|
|
@@ -72,6 +78,40 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
72
78
|
}
|
|
73
79
|
}
|
|
74
80
|
|
|
81
|
+
// Judgment provenance (Law 2). Eligibility asks "does this record have a
|
|
82
|
+
// judgment"; it has never asked "does that judgment describe THIS image."
|
|
83
|
+
// On star-freight the difference is 948 of 1,182 records — rationales a
|
|
84
|
+
// deleted bulk script selected by id-prefix, one string reused on 144
|
|
85
|
+
// images. Those records pass every eligibility check and land in training
|
|
86
|
+
// sets carrying an explanation about a category, not a picture.
|
|
87
|
+
//
|
|
88
|
+
// Read-only: nothing here mutates a record (Director's ruling, 2026-08-04).
|
|
89
|
+
const provenance = auditJudgmentProvenance(allRecords);
|
|
90
|
+
const provenanceWarning = formatProvenanceWarning(provenance);
|
|
91
|
+
if (provenanceWarning) {
|
|
92
|
+
console.log('');
|
|
93
|
+
console.log(` \x1b[33m⚠\x1b[0m ${provenanceWarning}`);
|
|
94
|
+
if (provenance.top_reused.length > 0) {
|
|
95
|
+
console.log(' Most-reused rationales:');
|
|
96
|
+
for (const r of provenance.top_reused.slice(0, 3)) {
|
|
97
|
+
const snippet = r.explanation.length > 62
|
|
98
|
+
? `${r.explanation.slice(0, 62)}…`
|
|
99
|
+
: r.explanation;
|
|
100
|
+
console.log(` ×${String(r.count).padStart(3)} "${snippet}"`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
// No script reviewer. Reuse here is usually a real systematic defect
|
|
105
|
+
// described accurately across the wave it affected — informational, not a
|
|
106
|
+
// finding. Printed at the same indent but without the warning glyph, so it
|
|
107
|
+
// reads as context rather than an accusation.
|
|
108
|
+
const reuseNote = formatReuseNote(provenance);
|
|
109
|
+
if (reuseNote) {
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log(` ${reuseNote}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
75
115
|
if (nearMiss.length > 0) {
|
|
76
116
|
console.log(`\n Improvement opportunities (${nearMiss.length} records with 1 failing check):`);
|
|
77
117
|
const byReason = {};
|
package/scripts/eval-pack.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { readFile } from 'node:fs/promises';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { parseArgs, getProjectName } from '../lib/args.js';
|
|
15
|
-
import {
|
|
15
|
+
import { getProjectRoot } from '../lib/paths.js';
|
|
16
16
|
import { inputError, handleCliError } from '../lib/errors.js';
|
|
17
17
|
import { buildEvalPack, listEvalPacks, loadEvalPack } from '../lib/eval-pack.js';
|
|
18
18
|
|
|
@@ -23,7 +23,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
const projectName = flags.project || getProjectName(argv);
|
|
26
|
-
const projectRoot =
|
|
26
|
+
const projectRoot = getProjectRoot(projectName);
|
|
27
27
|
const subcommand = positionals[0] || 'list';
|
|
28
28
|
|
|
29
29
|
if (subcommand === 'build') {
|
package/scripts/eval-run.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { parseArgs, getProjectName } from '../lib/args.js';
|
|
15
|
-
import { REPO_ROOT, resolveSafeProjectPath } from '../lib/paths.js';
|
|
15
|
+
import { REPO_ROOT, getProjectRoot, resolveSafeProjectPath } from '../lib/paths.js';
|
|
16
16
|
import { inputError, handleCliError } from '../lib/errors.js';
|
|
17
17
|
import { listTrainingManifests } from '../lib/training-manifests.js';
|
|
18
18
|
import { listEvalPacks } from '../lib/eval-pack.js';
|
|
@@ -30,7 +30,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
const projectName = flags.project || getProjectName(argv);
|
|
33
|
-
const projectRoot =
|
|
33
|
+
const projectRoot = getProjectRoot(projectName);
|
|
34
34
|
// First non-flag positional that isn't an er-id is the subcommand
|
|
35
35
|
const subcommand = positionals.find(a => !a.startsWith('er-')) || 'list';
|
|
36
36
|
|
package/scripts/export.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { parseArgs, getProjectName } from '../lib/args.js';
|
|
13
|
-
import {
|
|
13
|
+
import { getProjectRoot } from '../lib/paths.js';
|
|
14
14
|
import { inputError, handleCliError } from '../lib/errors.js';
|
|
15
15
|
import { listSnapshots } from '../lib/snapshot.js';
|
|
16
16
|
import { listSplits } from '../lib/split.js';
|
|
@@ -29,7 +29,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
const projectName = flags.project || getProjectName(argv);
|
|
32
|
-
const projectRoot =
|
|
32
|
+
const projectRoot = getProjectRoot(projectName);
|
|
33
33
|
const subcommand = positionals[0] || 'list';
|
|
34
34
|
|
|
35
35
|
if (subcommand === 'build') {
|