@mcptoolshop/style-dataset-lab 3.3.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/README.es.md +117 -55
- package/README.fr.md +125 -65
- package/README.hi.md +117 -68
- package/README.it.md +119 -58
- package/README.ja.md +124 -74
- package/README.md +73 -7
- package/README.pt-BR.md +128 -63
- package/README.zh.md +116 -65
- package/bin/sdlab.js +100 -4
- package/lib/adapters/ai-toolkit.js +224 -35
- package/lib/adapters/comfyui-runner.js +79 -26
- package/lib/adapters/comfyui-workflows.js +94 -11
- 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/asset-ingest.js +575 -0
- package/lib/asset-masks.js +323 -0
- package/lib/asset-source.js +1176 -0
- 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-author.js +350 -0
- package/lib/canon-build/build.js +146 -93
- package/lib/canon-review-gate.js +205 -0
- package/lib/captions.js +57 -0
- package/lib/card.js +74 -2
- package/lib/colab-launcher-guards.js +285 -0
- package/lib/comfyui.js +116 -18
- package/lib/config.js +116 -12
- package/lib/critique-engine.js +36 -2
- 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 +16 -3
- package/lib/freeze-stamp.js +28 -3
- package/lib/generated-provenance.js +194 -6
- package/lib/implementation-packs.js +95 -17
- package/lib/ingest.js +396 -0
- package/lib/judgment-provenance.js +220 -0
- package/lib/lane1-render.js +668 -0
- package/lib/measure.js +422 -0
- package/lib/memory-store.js +663 -0
- package/lib/model-licences.js +305 -0
- package/lib/npy-meta.js +130 -0
- package/lib/paths.js +196 -7
- package/lib/png-meta.js +512 -0
- package/lib/records.js +54 -8
- package/lib/reingest-selected.js +113 -17
- package/lib/reingest.js +27 -3
- package/lib/run-manifest.js +159 -25
- package/lib/run-summary.js +12 -6
- package/lib/runtime-runs.js +45 -4
- package/lib/snapshot.js +114 -15
- package/lib/split.js +364 -48
- package/lib/train-runs.js +214 -0
- package/lib/training-manifests.js +114 -21
- package/lib/training-packages.js +147 -15
- package/lib/verifier.js +3 -0
- package/lib/verify-chain.js +367 -0
- package/lib/workflow-profiles.js +24 -3
- package/package.json +4 -3
- 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/asset-ingest.js +160 -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-draft.js +82 -0
- package/scripts/canon-drift.js +16 -7
- package/scripts/canon-freeze-status.js +14 -6
- package/scripts/canon-freeze.js +31 -11
- package/scripts/canon-unfreeze.js +17 -9
- package/scripts/card.js +2 -2
- 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/compare.js +2 -2
- package/scripts/critique.js +16 -1
- package/scripts/curate.js +2 -2
- package/scripts/doctor.js +182 -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 +86 -17
- 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 +138 -20
- package/scripts/reingest.js +2 -2
- 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/verify.js +67 -0
- package/scripts/workflow-list.js +2 -1
- package/scripts/workflow-show.js +2 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Remote kernel runner executed via `colab exec -s <session> -f remote_runner.py`.
|
|
3
|
+
|
|
4
|
+
Runs the uploaded remote shell script (`SDLAB_REMOTE_SCRIPT`, e.g.
|
|
5
|
+
`/content/remote_lane1_stub.sh` or `/content/remote_lane3_stub.sh`) via
|
|
6
|
+
`subprocess.Popen`, streaming each stdout/stderr line to the IPython kernel's
|
|
7
|
+
stdout as it arrives so the rig's vitals and DEAD HARNESS watcher read it in
|
|
8
|
+
real time, and finishes by printing a `SDLAB_REMOTE_RC=<rc>` marker line.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _resolve_bash(env: dict[str, str]) -> str:
|
|
20
|
+
git_bash = "C:/Program Files/Git/bin/bash.exe"
|
|
21
|
+
if os.name == "nt" and os.path.exists(git_bash):
|
|
22
|
+
return git_bash
|
|
23
|
+
found = shutil.which("bash", path=env.get("PATH"))
|
|
24
|
+
return found or "bash"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def run_remote_script(
|
|
28
|
+
script_path: str | None = None,
|
|
29
|
+
*,
|
|
30
|
+
env: dict[str, str] | None = None,
|
|
31
|
+
) -> int:
|
|
32
|
+
effective_env = dict(os.environ if env is None else env)
|
|
33
|
+
target_script = script_path or effective_env.get("SDLAB_REMOTE_SCRIPT", "").strip()
|
|
34
|
+
if not target_script:
|
|
35
|
+
print("REFUSED: SDLAB_REMOTE_SCRIPT is required", file=sys.stderr, flush=True)
|
|
36
|
+
print("SDLAB_REMOTE_RC=2", flush=True)
|
|
37
|
+
return 2
|
|
38
|
+
|
|
39
|
+
cmd = [_resolve_bash(effective_env), target_script]
|
|
40
|
+
proc = subprocess.Popen(
|
|
41
|
+
cmd,
|
|
42
|
+
stdin=subprocess.DEVNULL,
|
|
43
|
+
stdout=subprocess.PIPE,
|
|
44
|
+
stderr=subprocess.STDOUT,
|
|
45
|
+
text=True,
|
|
46
|
+
bufsize=1,
|
|
47
|
+
env=effective_env,
|
|
48
|
+
)
|
|
49
|
+
assert proc.stdout is not None
|
|
50
|
+
for line in proc.stdout:
|
|
51
|
+
sys.stdout.write(line)
|
|
52
|
+
sys.stdout.flush()
|
|
53
|
+
rc = int(proc.wait())
|
|
54
|
+
print(f"SDLAB_REMOTE_RC={rc}", flush=True)
|
|
55
|
+
return rc
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
run_remote_script()
|
package/scripts/compare.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { writeFile, readdir, mkdir, readFile } from "node:fs/promises";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { parseArgs, getProjectName } from "../lib/args.js";
|
|
14
|
-
import {
|
|
14
|
+
import { getProjectRoot } from "../lib/paths.js";
|
|
15
15
|
import { readJsonFile } from "../lib/config.js";
|
|
16
16
|
import { inputError, handleCliError } from "../lib/errors.js";
|
|
17
17
|
|
|
@@ -25,7 +25,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
const projectName = flags.project || getProjectName(argv);
|
|
28
|
-
const GAME_ROOT =
|
|
28
|
+
const GAME_ROOT = getProjectRoot(projectName);
|
|
29
29
|
|
|
30
30
|
const [assetAId, assetBId, winner, ...reasonParts] = positionals;
|
|
31
31
|
|
package/scripts/critique.js
CHANGED
|
@@ -84,9 +84,24 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
84
84
|
const action = report.recommended_action;
|
|
85
85
|
console.log(`Critique saved to: runs/${flags.run}/critique.json`);
|
|
86
86
|
|
|
87
|
+
// H2: `refine_from_one` fires today purely from every candidate sitting
|
|
88
|
+
// at the unreviewed default overall_fit "usable" (nothing in the
|
|
89
|
+
// codebase currently sets it to anything else) — action.reviewed
|
|
90
|
+
// (lib/critique-engine.js recommendAction) distinguishes that from an
|
|
91
|
+
// actually-assessed run. Printing "Next: ..." with the same confidence
|
|
92
|
+
// either way presented brief-keyword drift tiebreaking as a considered
|
|
93
|
+
// recommendation. Qualify instead of suppressing outright — the pick is
|
|
94
|
+
// still a reasonable place to look first, it's just not a judgment.
|
|
87
95
|
if (action.mode === 'refine_from_one' && action.preferred_candidate) {
|
|
88
96
|
console.log('');
|
|
89
|
-
|
|
97
|
+
if (action.reviewed) {
|
|
98
|
+
info(`Next: sdlab refine --run ${flags.run} --pick ${action.preferred_candidate}`);
|
|
99
|
+
} else {
|
|
100
|
+
info(`No candidate has been reviewed yet — every image is at the rule-based pass's default "usable" ` +
|
|
101
|
+
`until a human or LLM judge assesses fit.`);
|
|
102
|
+
info(`If you want to proceed anyway: sdlab refine --run ${flags.run} --pick ${action.preferred_candidate} ` +
|
|
103
|
+
`(picked by fewest rule-based drift issues, not by looking at the image).`);
|
|
104
|
+
}
|
|
90
105
|
} else if (action.mode === 'accept_one' && action.preferred_candidate) {
|
|
91
106
|
console.log('');
|
|
92
107
|
info(`Preferred candidate: ${action.preferred_candidate}`);
|
package/scripts/curate.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { writeFile, rename, readdir, mkdir, readFile } from "node:fs/promises";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { parseArgs, getProjectName } from "../lib/args.js";
|
|
16
|
-
import {
|
|
16
|
+
import { getProjectRoot } from "../lib/paths.js";
|
|
17
17
|
import { readJsonFile } from "../lib/config.js";
|
|
18
18
|
import { inputError, runtimeError, handleCliError } from "../lib/errors.js";
|
|
19
19
|
import { result } from "../lib/log.js";
|
|
@@ -35,7 +35,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
const projectName = flags.project || getProjectName(argv);
|
|
38
|
-
const GAME_ROOT =
|
|
38
|
+
const GAME_ROOT = getProjectRoot(projectName);
|
|
39
39
|
const dryRun = flags['dry-run'] || flags.dryRun;
|
|
40
40
|
|
|
41
41
|
if (flags.list) {
|
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,10 +147,11 @@ 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++; }
|
|
154
|
+
if (meta.display_name === 'TEMPLATE') { fail('project.json: display_name is still "TEMPLATE"', 'Set a real display_name in project.json instead of the unedited template placeholder.'); failures++; }
|
|
128
155
|
}
|
|
129
156
|
|
|
130
157
|
// ── 5. Constitution validation ──
|
|
@@ -133,21 +160,42 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
133
160
|
console.log('');
|
|
134
161
|
const rules = constitution.rules || (Array.isArray(constitution) ? constitution : []);
|
|
135
162
|
if (rules.length === 0) {
|
|
136
|
-
fail(
|
|
163
|
+
fail(
|
|
164
|
+
'constitution.json: no rules defined',
|
|
165
|
+
'Add at least one rule object to the "rules" array — see templates/domains/*/constitution.json for the shape.'
|
|
166
|
+
);
|
|
137
167
|
failures++;
|
|
138
168
|
} else {
|
|
139
169
|
pass(`constitution.json: ${rules.length} rules`);
|
|
140
170
|
passes++;
|
|
141
171
|
|
|
142
172
|
// Check required rule fields
|
|
143
|
-
|
|
173
|
+
// M1: localize WHICH index is missing an id, and WHICH TWO indices
|
|
174
|
+
// collide on a duplicate — a rule id is not the record's only
|
|
175
|
+
// identity, and "duplicate rule ID X" without saying where left the
|
|
176
|
+
// operator grepping the file by hand.
|
|
177
|
+
const ruleIdFirstIndex = new Map(); // id -> first index that defined it
|
|
144
178
|
let ruleErrors = 0;
|
|
145
|
-
for (const rule of rules) {
|
|
146
|
-
if (!rule.id) {
|
|
147
|
-
|
|
148
|
-
|
|
179
|
+
for (const [i, rule] of rules.entries()) {
|
|
180
|
+
if (!rule.id) {
|
|
181
|
+
fail(`constitution.json: rule missing "id"`, `Add an "id" field to rules[${i}] (e.g. "STY-002").`);
|
|
182
|
+
ruleErrors++;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (ruleIdFirstIndex.has(rule.id)) {
|
|
186
|
+
fail(
|
|
187
|
+
`constitution.json: duplicate rule ID "${rule.id}"`,
|
|
188
|
+
`rules[${ruleIdFirstIndex.get(rule.id)}] and rules[${i}] both use id "${rule.id}" — give one of them a unique id.`
|
|
189
|
+
);
|
|
190
|
+
ruleErrors++;
|
|
191
|
+
} else {
|
|
192
|
+
ruleIdFirstIndex.set(rule.id, i);
|
|
193
|
+
}
|
|
149
194
|
if (!rule.dims || rule.dims.length === 0) {
|
|
150
|
-
fail(
|
|
195
|
+
fail(
|
|
196
|
+
`constitution.json: rule ${rule.id} has no "dims" (scoring dimensions)`,
|
|
197
|
+
`Add a non-empty "dims" array to rules[${i}] (e.g. ["style_consistency"]) — see rubric.json's "dimensions" for valid values.`
|
|
198
|
+
);
|
|
151
199
|
ruleErrors++;
|
|
152
200
|
}
|
|
153
201
|
}
|
|
@@ -168,18 +216,35 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
168
216
|
pass(`lanes.json: ${laneList.length} lanes`);
|
|
169
217
|
passes++;
|
|
170
218
|
|
|
171
|
-
// Check lane IDs unique
|
|
172
|
-
const
|
|
219
|
+
// Check lane IDs unique — same index-localization as constitution rules above.
|
|
220
|
+
const laneIdFirstIndex = new Map();
|
|
173
221
|
let laneErrors = 0;
|
|
174
|
-
for (const lane of laneList) {
|
|
175
|
-
if (!lane.id) {
|
|
176
|
-
|
|
177
|
-
|
|
222
|
+
for (const [i, lane] of laneList.entries()) {
|
|
223
|
+
if (!lane.id) {
|
|
224
|
+
fail('lanes.json: lane missing "id"', `Add an "id" field to lanes[${i}].`);
|
|
225
|
+
laneErrors++;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (laneIdFirstIndex.has(lane.id)) {
|
|
229
|
+
fail(
|
|
230
|
+
`lanes.json: duplicate lane ID "${lane.id}"`,
|
|
231
|
+
`lanes[${laneIdFirstIndex.get(lane.id)}] and lanes[${i}] both use id "${lane.id}" — give one of them a unique id.`
|
|
232
|
+
);
|
|
233
|
+
laneErrors++;
|
|
234
|
+
} else {
|
|
235
|
+
laneIdFirstIndex.set(lane.id, i);
|
|
236
|
+
}
|
|
178
237
|
|
|
179
238
|
// Check patterns compile
|
|
180
239
|
for (const pattern of lane.id_patterns || []) {
|
|
181
240
|
try { new RegExp(pattern); }
|
|
182
|
-
catch {
|
|
241
|
+
catch {
|
|
242
|
+
fail(
|
|
243
|
+
`lanes.json: lane "${lane.id}" has invalid pattern "${pattern}"`,
|
|
244
|
+
`Fix the regular expression syntax for lane "${lane.id}"'s id_patterns entry "${pattern}".`
|
|
245
|
+
);
|
|
246
|
+
laneErrors++;
|
|
247
|
+
}
|
|
183
248
|
}
|
|
184
249
|
}
|
|
185
250
|
if (laneErrors === 0) { pass('lanes.json: all lane IDs unique, patterns compile'); passes++; }
|
|
@@ -198,7 +263,10 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
198
263
|
console.log('');
|
|
199
264
|
const dims = rubric.dimensions || [];
|
|
200
265
|
if (dims.length === 0) {
|
|
201
|
-
fail(
|
|
266
|
+
fail(
|
|
267
|
+
'rubric.json: no dimensions defined',
|
|
268
|
+
'Add at least one dimension string to "dimensions" (e.g. ["style_consistency", "composition"]).'
|
|
269
|
+
);
|
|
202
270
|
failures++;
|
|
203
271
|
} else {
|
|
204
272
|
pass(`rubric.json: ${dims.length} dimensions`);
|
|
@@ -206,7 +274,10 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
206
274
|
}
|
|
207
275
|
|
|
208
276
|
if (!rubric.thresholds) {
|
|
209
|
-
fail(
|
|
277
|
+
fail(
|
|
278
|
+
'rubric.json: missing "thresholds"',
|
|
279
|
+
'Add a "thresholds" object with approved/borderline/rejected bands — see templates/domains/*/rubric.json for the shape.'
|
|
280
|
+
);
|
|
210
281
|
failures++;
|
|
211
282
|
} else {
|
|
212
283
|
pass('rubric.json: thresholds present');
|
|
@@ -224,7 +295,10 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
224
295
|
}
|
|
225
296
|
}
|
|
226
297
|
if (missingDims.size > 0) {
|
|
227
|
-
fail(
|
|
298
|
+
fail(
|
|
299
|
+
`rubric.json: constitution references dimensions not in rubric: ${[...missingDims].join(', ')}`,
|
|
300
|
+
`Add [${[...missingDims].map(d => `"${d}"`).join(', ')}] to rubric.json's "dimensions" array, or fix the referencing rule(s) in constitution.json.`
|
|
301
|
+
);
|
|
228
302
|
failures++;
|
|
229
303
|
} else {
|
|
230
304
|
pass('rubric.json: all constitution dimensions present in rubric');
|
|
@@ -239,7 +313,10 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
239
313
|
for (const [mode, ruleList] of Object.entries(rubric.failure_to_rules)) {
|
|
240
314
|
for (const ruleId of ruleList) {
|
|
241
315
|
if (!ruleIds.has(ruleId)) {
|
|
242
|
-
fail(
|
|
316
|
+
fail(
|
|
317
|
+
`rubric.json: failure mode "${mode}" references unknown rule "${ruleId}"`,
|
|
318
|
+
`Add a rule with id "${ruleId}" to constitution.json, or remove it from rubric.json's failure_to_rules["${mode}"].`
|
|
319
|
+
);
|
|
243
320
|
badRefs++;
|
|
244
321
|
}
|
|
245
322
|
}
|
|
@@ -269,7 +346,10 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
269
346
|
const order = terminology[orderKey] || [];
|
|
270
347
|
for (const name of order) {
|
|
271
348
|
if (!terminology.groups[name]) {
|
|
272
|
-
fail(
|
|
349
|
+
fail(
|
|
350
|
+
`terminology.json: ${orderKey} references unknown group "${name}"`,
|
|
351
|
+
`Add "${name}" to terminology.json's "groups" object, or remove it from "${orderKey}".`
|
|
352
|
+
);
|
|
273
353
|
failures++;
|
|
274
354
|
}
|
|
275
355
|
}
|
|
@@ -280,11 +360,23 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
280
360
|
for (const [gName, group] of Object.entries(terminology.groups)) {
|
|
281
361
|
for (const pattern of group.id_patterns || []) {
|
|
282
362
|
try { new RegExp(pattern); }
|
|
283
|
-
catch {
|
|
363
|
+
catch {
|
|
364
|
+
fail(
|
|
365
|
+
`terminology.json: group "${gName}" has invalid id_pattern "${pattern}"`,
|
|
366
|
+
`Fix the regular expression syntax for group "${gName}"'s id_patterns entry "${pattern}".`
|
|
367
|
+
);
|
|
368
|
+
patternErrors++;
|
|
369
|
+
}
|
|
284
370
|
}
|
|
285
371
|
for (const pattern of group.prompt_patterns || []) {
|
|
286
372
|
try { new RegExp(pattern); }
|
|
287
|
-
catch {
|
|
373
|
+
catch {
|
|
374
|
+
fail(
|
|
375
|
+
`terminology.json: group "${gName}" has invalid prompt_pattern "${pattern}"`,
|
|
376
|
+
`Fix the regular expression syntax for group "${gName}"'s prompt_patterns entry "${pattern}".`
|
|
377
|
+
);
|
|
378
|
+
patternErrors++;
|
|
379
|
+
}
|
|
288
380
|
}
|
|
289
381
|
}
|
|
290
382
|
if (patternErrors === 0) { pass('terminology.json: all detection patterns compile'); passes++; }
|
|
@@ -305,46 +397,56 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
305
397
|
let wfErrors = 0;
|
|
306
398
|
for (const file of wfFiles) {
|
|
307
399
|
const { data, error } = tryParseJson(join(workflowDir, file));
|
|
308
|
-
if (error) {
|
|
400
|
+
if (error) {
|
|
401
|
+
fail(`workflows/profiles/${file} — invalid JSON: ${error}`, `Open workflows/profiles/${file} and fix the syntax error above.`);
|
|
402
|
+
wfErrors++;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
309
405
|
|
|
310
406
|
const missing = ['workflow_id', 'label', 'lane_id', 'output_mode', 'output_count',
|
|
311
407
|
'prompt_strategy', 'negative_strategy', 'canon_focus', 'drift_guards', 'runtime_defaults']
|
|
312
408
|
.filter(f => data[f] === undefined || data[f] === null);
|
|
313
409
|
if (missing.length > 0) {
|
|
314
|
-
fail(`workflows/profiles/${file} — missing fields: ${missing.join(', ')}`);
|
|
410
|
+
fail(`workflows/profiles/${file} — missing fields: ${missing.join(', ')}`, `Add [${missing.join(', ')}] to workflows/profiles/${file}.`);
|
|
315
411
|
wfErrors++;
|
|
316
412
|
continue;
|
|
317
413
|
}
|
|
318
414
|
|
|
319
415
|
if (data.lane_id && laneIds.size > 0 && !laneIds.has(data.lane_id)) {
|
|
320
|
-
fail(
|
|
416
|
+
fail(
|
|
417
|
+
`workflows/profiles/${file} — lane_id "${data.lane_id}" not found in lanes.json`,
|
|
418
|
+
`Use one of lanes.json's existing lane ids [${[...laneIds].join(', ')}], or add "${data.lane_id}" as a new lane.`
|
|
419
|
+
);
|
|
321
420
|
wfErrors++;
|
|
322
421
|
}
|
|
323
422
|
|
|
324
423
|
if (data.subject_mode && !VALID_SUBJECT_MODES.includes(data.subject_mode)) {
|
|
325
|
-
fail(`workflows/profiles/${file} — invalid subject_mode "${data.subject_mode}"`);
|
|
424
|
+
fail(`workflows/profiles/${file} — invalid subject_mode "${data.subject_mode}"`, `subject_mode must be one of: ${VALID_SUBJECT_MODES.join(', ')}.`);
|
|
326
425
|
wfErrors++;
|
|
327
426
|
}
|
|
328
427
|
|
|
329
428
|
if (!VALID_OUTPUT_MODES.includes(data.output_mode)) {
|
|
330
|
-
fail(`workflows/profiles/${file} — invalid output_mode "${data.output_mode}"`);
|
|
429
|
+
fail(`workflows/profiles/${file} — invalid output_mode "${data.output_mode}"`, `output_mode must be one of: ${VALID_OUTPUT_MODES.join(', ')}.`);
|
|
331
430
|
wfErrors++;
|
|
332
431
|
}
|
|
333
432
|
|
|
334
433
|
const ps = data.prompt_strategy;
|
|
335
434
|
if (!ps || !Array.isArray(ps.style_prefix) || !ps.structure || !Array.isArray(ps.must_include)) {
|
|
336
|
-
fail(
|
|
435
|
+
fail(
|
|
436
|
+
`workflows/profiles/${file} — prompt_strategy needs style_prefix[], structure, must_include[]`,
|
|
437
|
+
`Add prompt_strategy.style_prefix (array), .structure (string), and .must_include (array) to workflows/profiles/${file}.`
|
|
438
|
+
);
|
|
337
439
|
wfErrors++;
|
|
338
440
|
}
|
|
339
441
|
|
|
340
442
|
const ns = data.negative_strategy;
|
|
341
443
|
if (!ns || !Array.isArray(ns.must_avoid)) {
|
|
342
|
-
fail(`workflows/profiles/${file} — negative_strategy needs must_avoid[]`);
|
|
444
|
+
fail(`workflows/profiles/${file} — negative_strategy needs must_avoid[]`, `Add negative_strategy.must_avoid (array) to workflows/profiles/${file}.`);
|
|
343
445
|
wfErrors++;
|
|
344
446
|
}
|
|
345
447
|
|
|
346
448
|
if (!data.runtime_defaults?.adapter_target) {
|
|
347
|
-
fail(`workflows/profiles/${file} — runtime_defaults needs adapter_target`);
|
|
449
|
+
fail(`workflows/profiles/${file} — runtime_defaults needs adapter_target`, `Add runtime_defaults.adapter_target (e.g. "comfyui") to workflows/profiles/${file}.`);
|
|
348
450
|
wfErrors++;
|
|
349
451
|
}
|
|
350
452
|
}
|
|
@@ -377,34 +479,41 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
377
479
|
let bmErrors = 0;
|
|
378
480
|
for (const file of bmFiles) {
|
|
379
481
|
const { data, error } = tryParseJson(join(batchModesDir, file));
|
|
380
|
-
if (error) {
|
|
482
|
+
if (error) {
|
|
483
|
+
fail(`workflows/batch-modes/${file} — invalid JSON: ${error}`, `Open workflows/batch-modes/${file} and fix the syntax error above.`);
|
|
484
|
+
bmErrors++;
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
381
487
|
|
|
382
|
-
if (!data.mode_id) { fail(`workflows/batch-modes/${file} — missing mode_id`); bmErrors++; }
|
|
488
|
+
if (!data.mode_id) { fail(`workflows/batch-modes/${file} — missing mode_id`, `Add a "mode_id" field to workflows/batch-modes/${file}.`); bmErrors++; }
|
|
383
489
|
if (!data.batch_type || !VALID_BATCH_TYPES.includes(data.batch_type)) {
|
|
384
|
-
fail(`workflows/batch-modes/${file} — invalid batch_type "${data.batch_type}"`);
|
|
490
|
+
fail(`workflows/batch-modes/${file} — invalid batch_type "${data.batch_type}"`, `batch_type must be one of: ${VALID_BATCH_TYPES.join(', ')}.`);
|
|
385
491
|
bmErrors++;
|
|
386
492
|
}
|
|
387
493
|
if (data.subject_mode && !VALID_SUBJECT_MODES_BM.includes(data.subject_mode)) {
|
|
388
|
-
fail(`workflows/batch-modes/${file} — invalid subject_mode "${data.subject_mode}"`);
|
|
494
|
+
fail(`workflows/batch-modes/${file} — invalid subject_mode "${data.subject_mode}"`, `subject_mode must be one of: ${VALID_SUBJECT_MODES_BM.join(', ')}.`);
|
|
389
495
|
bmErrors++;
|
|
390
496
|
}
|
|
391
497
|
if (data.base_workflow_id && wfFiles.length > 0 && !wfFiles.includes(data.base_workflow_id)) {
|
|
392
|
-
fail(
|
|
498
|
+
fail(
|
|
499
|
+
`workflows/batch-modes/${file} — base_workflow_id "${data.base_workflow_id}" not found in profiles`,
|
|
500
|
+
`Use one of the existing workflow ids [${wfFiles.join(', ')}], or add a matching profile under workflows/profiles/.`
|
|
501
|
+
);
|
|
393
502
|
bmErrors++;
|
|
394
503
|
}
|
|
395
504
|
if (!Array.isArray(data.variant_plan) || data.variant_plan.length === 0) {
|
|
396
|
-
fail(`workflows/batch-modes/${file} — variant_plan must be non-empty array`);
|
|
505
|
+
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
506
|
bmErrors++;
|
|
398
507
|
} else {
|
|
399
508
|
const slotIds = data.variant_plan.map(s => s.slot_id);
|
|
400
509
|
const dupes = slotIds.filter((id, i) => slotIds.indexOf(id) !== i);
|
|
401
510
|
if (dupes.length > 0) {
|
|
402
|
-
fail(`workflows/batch-modes/${file} — duplicate slot_ids: ${[...new Set(dupes)].join(', ')}`);
|
|
511
|
+
fail(`workflows/batch-modes/${file} — duplicate slot_ids: ${[...new Set(dupes)].join(', ')}`, `Give each entry in variant_plan a unique slot_id.`);
|
|
403
512
|
bmErrors++;
|
|
404
513
|
}
|
|
405
514
|
}
|
|
406
515
|
if (data.assembly?.layout && !VALID_LAYOUTS.includes(data.assembly.layout)) {
|
|
407
|
-
fail(`workflows/batch-modes/${file} — invalid assembly.layout "${data.assembly.layout}"`);
|
|
516
|
+
fail(`workflows/batch-modes/${file} — invalid assembly.layout "${data.assembly.layout}"`, `assembly.layout must be one of: ${VALID_LAYOUTS.join(', ')}.`);
|
|
408
517
|
bmErrors++;
|
|
409
518
|
}
|
|
410
519
|
}
|
|
@@ -424,17 +533,26 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
424
533
|
// ── 11. Runtime templates check ──
|
|
425
534
|
{
|
|
426
535
|
console.log('');
|
|
427
|
-
|
|
536
|
+
// PACKAGE-relative, not workspace-relative — `runtime/` ships inside the
|
|
537
|
+
// npm tarball (see getPackageRoot() in lib/paths.js). Joining it to
|
|
538
|
+
// REPO_ROOT made doctor report "runtime/comfyui/ — directory not found"
|
|
539
|
+
// for every installed user while the templates sat in the package.
|
|
540
|
+
const runtimeDir = join(getRuntimeDir(), 'comfyui');
|
|
428
541
|
if (existsSync(runtimeDir)) {
|
|
429
542
|
const templateFiles = readdirSync(runtimeDir).filter(f => f.endsWith('.json'));
|
|
430
543
|
if (templateFiles.length > 0) {
|
|
431
544
|
let templateErrors = 0;
|
|
432
545
|
for (const file of templateFiles) {
|
|
433
546
|
const { data, error } = tryParseJson(join(runtimeDir, file));
|
|
434
|
-
if (error) {
|
|
435
|
-
|
|
547
|
+
if (error) {
|
|
548
|
+
fail(`runtime/comfyui/${file}: invalid JSON — ${error}`, `Open ${join(runtimeDir, file)} and fix the syntax error above.`);
|
|
549
|
+
templateErrors++;
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (!data.template_id) { fail(`runtime/comfyui/${file}: missing template_id`, `Add a "template_id" field to ${file}.`); templateErrors++; }
|
|
436
553
|
if (!data.compatible_modes || !Array.isArray(data.compatible_modes)) {
|
|
437
|
-
fail(`runtime/comfyui/${file}: missing compatible_modes[]`);
|
|
554
|
+
fail(`runtime/comfyui/${file}: missing compatible_modes[]`, `Add a "compatible_modes" array to ${file}.`);
|
|
555
|
+
templateErrors++;
|
|
438
556
|
}
|
|
439
557
|
}
|
|
440
558
|
if (templateErrors === 0) {
|
|
@@ -464,8 +582,13 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
464
582
|
const sample = recordFiles.slice(0, 3);
|
|
465
583
|
for (const f of sample) {
|
|
466
584
|
const { data, error } = tryParseJson(join(recordsDir, f));
|
|
467
|
-
if (error) {
|
|
468
|
-
|
|
585
|
+
if (error) {
|
|
586
|
+
fail(`records/${f}: invalid JSON`, `Open records/${f} and fix the syntax error, or delete/regenerate it if it's an aborted write.`);
|
|
587
|
+
recordErrors++;
|
|
588
|
+
} else if (!data.id) {
|
|
589
|
+
fail(`records/${f}: missing "id"`, `Add an "id" field to records/${f} matching its filename stem.`);
|
|
590
|
+
recordErrors++;
|
|
591
|
+
}
|
|
469
592
|
}
|
|
470
593
|
if (recordErrors === 0) {
|
|
471
594
|
pass(`${recordFiles.length} records (spot-check passed)`);
|
|
@@ -485,7 +608,11 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
485
608
|
console.log(`═══ ${status} — ${passes} passed, ${failures} failed, ${warnings} warnings ═══`);
|
|
486
609
|
|
|
487
610
|
if (failures > 0) {
|
|
488
|
-
throw runtimeError(
|
|
611
|
+
throw runtimeError(
|
|
612
|
+
'PROJECT_UNHEALTHY',
|
|
613
|
+
`Project "${projectName}" has ${failures} issue(s).`,
|
|
614
|
+
`Fix the ✗ lines above — each one now carries its own Hint: with a concrete next action — then re-run: sdlab project doctor --project ${projectName}`
|
|
615
|
+
);
|
|
489
616
|
}
|
|
490
617
|
}
|
|
491
618
|
|
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 = {};
|