@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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* _resume.js — internal helper module (NOT a CLI command; underscore prefix
|
|
3
|
+
* matches the existing scripts/_batch_cut.py / _seam_cutout.py convention
|
|
4
|
+
* for "helper, not dispatched by bin/sdlab.js").
|
|
5
|
+
*
|
|
6
|
+
* H5: generate.js was the only one of the four ComfyUI generator scripts
|
|
7
|
+
* that implemented `--resume` — skip an asset whose provenance record AND
|
|
8
|
+
* output image already exist on disk, WITHOUT skipping its seed slot, so a
|
|
9
|
+
* resumed run stays bit-identical to an uninterrupted one. generate-identity
|
|
10
|
+
* / generate-controlnet / generate-ipadapter had no --resume at all, which
|
|
11
|
+
* is expensive specifically for generate-identity (the priciest per-image
|
|
12
|
+
* generator in the pipeline): interrupting a 12-subject run after subject 9
|
|
13
|
+
* meant regenerating all 12 from scratch.
|
|
14
|
+
*
|
|
15
|
+
* Extracted here (once) so all four generators — and any future one — get
|
|
16
|
+
* the same discipline by importing it, instead of by copy-pasting the check
|
|
17
|
+
* next to generate.js's inline version. `lib/` is outside this fix's file
|
|
18
|
+
* ownership, so this lives under scripts/** instead; see the dispatch
|
|
19
|
+
* report for the full rationale.
|
|
20
|
+
*
|
|
21
|
+
* `requireRecord` exists because not every generator writes a provenance
|
|
22
|
+
* record: generate.js and generate-identity.js both write
|
|
23
|
+
* records/<id>.json, but generate-controlnet.js and generate-ipadapter.js
|
|
24
|
+
* are lighter-weight discovery scripts that only ever write the output PNG
|
|
25
|
+
* — they have no records/ write path at all. Requiring a record that a
|
|
26
|
+
* script structurally never writes would make --resume a permanent no-op
|
|
27
|
+
* for those two, which is worse than not having the flag. Pass
|
|
28
|
+
* `{ requireRecord: false }` for generators with no records/ write path.
|
|
29
|
+
*/
|
|
30
|
+
import { existsSync } from 'node:fs';
|
|
31
|
+
import { join } from 'node:path';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} projectRoot — GAME_ROOT
|
|
35
|
+
* @param {string} assetId — the record id / output filename stem (no extension)
|
|
36
|
+
* @param {Object} [opts]
|
|
37
|
+
* @param {boolean} [opts.requireRecord=true] — also require records/<id>.json
|
|
38
|
+
* @param {string} [opts.recordsSubdir='records']
|
|
39
|
+
* @param {string} [opts.candidatesSubdir='outputs/candidates']
|
|
40
|
+
* @returns {boolean} true when the asset is safe to skip on --resume
|
|
41
|
+
*/
|
|
42
|
+
export function isResumable(projectRoot, assetId, opts = {}) {
|
|
43
|
+
const requireRecord = opts.requireRecord !== false;
|
|
44
|
+
const candidatesSubdir = opts.candidatesSubdir || 'outputs/candidates';
|
|
45
|
+
const imagePath = join(projectRoot, candidatesSubdir, `${assetId}.png`);
|
|
46
|
+
if (!existsSync(imagePath)) return false;
|
|
47
|
+
|
|
48
|
+
if (!requireRecord) return true;
|
|
49
|
+
|
|
50
|
+
const recordsSubdir = opts.recordsSubdir || 'records';
|
|
51
|
+
const recordPath = join(projectRoot, recordsSubdir, `${assetId}.json`);
|
|
52
|
+
return existsSync(recordPath);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Print the standard "(resumed — skipped)" progress line — matches
|
|
57
|
+
* generate.js's console.log verbatim so all four generators read
|
|
58
|
+
* identically under --resume.
|
|
59
|
+
*/
|
|
60
|
+
export function logResumedSkip(index, total, assetId) {
|
|
61
|
+
console.log(` [${index}/${total}] ${assetId} \x1b[2m(resumed — skipped)\x1b[0m`);
|
|
62
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* _shared-args.js — internal helper module (NOT a CLI command; underscore
|
|
3
|
+
* prefix matches the existing scripts/_batch_cut.py / _seam_cutout.py
|
|
4
|
+
* convention for "helper, not dispatched by bin/sdlab.js").
|
|
5
|
+
*
|
|
6
|
+
* H4 (second half): the five canon-*.js scripts (canon-build.js,
|
|
7
|
+
* canon-drift.js, canon-freeze.js, canon-freeze-status.js,
|
|
8
|
+
* canon-unfreeze.js) all use Node's built-in `util.parseArgs({strict:
|
|
9
|
+
* false})`. strict:false is there deliberately, so a bare positional
|
|
10
|
+
* argument (the entity id) isn't rejected — but it has a side effect: ANY
|
|
11
|
+
* unrecognized `--flag` is silently accepted too. A misspelled OPTIONAL
|
|
12
|
+
* flag (`--staus follow` instead of `--status follow`, `--jsn` instead of
|
|
13
|
+
* `--json`) produces no error and no warning — the option you meant to set
|
|
14
|
+
* just quietly keeps its default, and Node's parser stores the typo under
|
|
15
|
+
* ITS OWN key in `parsed.values`, never touching the one you meant.
|
|
16
|
+
*
|
|
17
|
+
* assertKnownOptions() closes that gap without giving up strict:false's
|
|
18
|
+
* positional leniency: after parsing, walk `parsed.values`'s keys — since
|
|
19
|
+
* every DECLARED option (default or explicit) and every UNDECLARED flag
|
|
20
|
+
* alike lands there — and reject any key outside the known option set.
|
|
21
|
+
* Mirrors lib/args.js's own UNKNOWN_FLAG shape (same code, same
|
|
22
|
+
* "Did you mean" hint via findClosest) so a typo on a canon-*.js flag reads
|
|
23
|
+
* identically to a typo anywhere else in the CLI.
|
|
24
|
+
*/
|
|
25
|
+
import { inputError } from '../lib/errors.js';
|
|
26
|
+
import { findClosest } from '../lib/args.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {Object} parsedValues — the `.values` object returned by
|
|
30
|
+
* node:util's parseArgs({strict:false, ...})
|
|
31
|
+
* @param {Object} optionsSpec — the SAME `options` object passed to parseArgs
|
|
32
|
+
* @param {{ command?: string }} [opts] — command label for the hint text
|
|
33
|
+
*/
|
|
34
|
+
export function assertKnownOptions(parsedValues, optionsSpec, opts = {}) {
|
|
35
|
+
const known = new Set(Object.keys(optionsSpec));
|
|
36
|
+
for (const key of Object.keys(parsedValues || {})) {
|
|
37
|
+
if (known.has(key)) continue;
|
|
38
|
+
const suggestion = findClosest(key, [...known]);
|
|
39
|
+
const hint = suggestion
|
|
40
|
+
? `Did you mean --${suggestion}? (Run "sdlab ${opts.command ? opts.command + ' ' : ''}--help" to see all supported flags.)`
|
|
41
|
+
: `Run "sdlab ${opts.command ? opts.command + ' ' : ''}--help" to see supported flags.`;
|
|
42
|
+
throw inputError('UNKNOWN_FLAG', `Unknown flag: --${key}`, hint);
|
|
43
|
+
}
|
|
44
|
+
}
|
package/scripts/brief-compile.js
CHANGED
|
@@ -14,6 +14,7 @@ import { parseArgs, getProjectName, parseNumberFlag } from '../lib/args.js';
|
|
|
14
14
|
import { getProjectRoot } from '../lib/paths.js';
|
|
15
15
|
import { compileBrief, saveCompiledBrief } from '../lib/brief-compiler.js';
|
|
16
16
|
import { info } from '../lib/log.js';
|
|
17
|
+
import { handleCliError } from '../lib/errors.js';
|
|
17
18
|
|
|
18
19
|
export async function run(argv = process.argv.slice(2)) {
|
|
19
20
|
const { flags } = parseArgs(argv, {
|
|
@@ -130,5 +131,5 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
if (process.argv[1] && (process.argv[1].endsWith('brief-compile.js') || process.argv[1].endsWith('brief-compile'))) {
|
|
133
|
-
run().catch(
|
|
134
|
+
run().catch(handleCliError);
|
|
134
135
|
}
|
package/scripts/brief-show.js
CHANGED
|
@@ -13,6 +13,7 @@ import { parseArgs, getProjectName } from '../lib/args.js';
|
|
|
13
13
|
import { getProjectRoot } from '../lib/paths.js';
|
|
14
14
|
import { loadCompiledBrief } from '../lib/brief-compiler.js';
|
|
15
15
|
import { renderBriefText, renderBriefMarkdown } from '../lib/brief-render.js';
|
|
16
|
+
import { handleCliError } from '../lib/errors.js';
|
|
16
17
|
|
|
17
18
|
export async function run(argv = process.argv.slice(2)) {
|
|
18
19
|
const { flags, positionals } = parseArgs(argv, {
|
|
@@ -49,5 +50,5 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
if (process.argv[1] && (process.argv[1].endsWith('brief-show.js') || process.argv[1].endsWith('brief-show'))) {
|
|
52
|
-
run().catch(
|
|
53
|
+
run().catch(handleCliError);
|
|
53
54
|
}
|
package/scripts/canon-bind.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { readFileSync, writeFileSync, readdirSync, existsSync } from 'fs';
|
|
20
20
|
import { join } from 'path';
|
|
21
21
|
import { getProjectName } from '../lib/args.js';
|
|
22
|
-
import {
|
|
22
|
+
import { getProjectRoot } from '../lib/paths.js';
|
|
23
23
|
import { loadConstitution, loadLanes, loadRubric, loadTerminology, detectLane, detectGroup } from '../lib/config.js';
|
|
24
24
|
import { inputError, handleCliError } from '../lib/errors.js';
|
|
25
25
|
|
|
@@ -155,7 +155,7 @@ function generateAssertions(record, constitutionRules, lanesConfig, rubricConfig
|
|
|
155
155
|
|
|
156
156
|
export async function run(argv = process.argv.slice(2)) {
|
|
157
157
|
const projectName = getProjectName(argv);
|
|
158
|
-
const PROJECT_ROOT =
|
|
158
|
+
const PROJECT_ROOT = getProjectRoot(projectName);
|
|
159
159
|
const RECORDS_DIR = join(PROJECT_ROOT, 'records');
|
|
160
160
|
const DRY_RUN = argv.includes('--dry-run');
|
|
161
161
|
const STATS_ONLY = argv.includes('--stats');
|
package/scripts/canon-build.js
CHANGED
|
@@ -7,27 +7,35 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { parseArgs } from 'node:util';
|
|
10
|
-
import {
|
|
11
|
-
import { existsSync } from 'node:fs';
|
|
10
|
+
import { getProjectRoot } from '../lib/paths.js';
|
|
12
11
|
import { runBuild } from '../lib/canon-build/build.js';
|
|
13
12
|
import { inputError } from '../lib/errors.js';
|
|
13
|
+
import { assertKnownOptions } from './_shared-args.js';
|
|
14
14
|
|
|
15
15
|
export async function run(argv) {
|
|
16
|
+
const options = {
|
|
17
|
+
project: { type: 'string' },
|
|
18
|
+
game: { type: 'string' }, // deprecated alias
|
|
19
|
+
full: { type: 'boolean', default: false },
|
|
20
|
+
'no-cache': { type: 'boolean', default: false },
|
|
21
|
+
'dry-run': { type: 'boolean', default: false },
|
|
22
|
+
only: { type: 'string' },
|
|
23
|
+
json: { type: 'boolean', default: false },
|
|
24
|
+
quiet: { type: 'boolean', default: false },
|
|
25
|
+
};
|
|
16
26
|
const parsed = parseArgs({
|
|
17
27
|
args: argv,
|
|
18
|
-
options
|
|
19
|
-
project: { type: 'string' },
|
|
20
|
-
game: { type: 'string' }, // deprecated alias
|
|
21
|
-
full: { type: 'boolean', default: false },
|
|
22
|
-
'no-cache': { type: 'boolean', default: false },
|
|
23
|
-
'dry-run': { type: 'boolean', default: false },
|
|
24
|
-
only: { type: 'string' },
|
|
25
|
-
json: { type: 'boolean', default: false },
|
|
26
|
-
quiet: { type: 'boolean', default: false },
|
|
27
|
-
},
|
|
28
|
+
options,
|
|
28
29
|
allowPositionals: true,
|
|
29
30
|
strict: false,
|
|
30
31
|
});
|
|
32
|
+
// H4: strict:false is deliberate (it lets the entity-id positional through
|
|
33
|
+
// unrejected) but its side effect is that ANY unrecognized --flag is
|
|
34
|
+
// silently accepted too — a misspelled OPTIONAL flag (--ful instead of
|
|
35
|
+
// --full, --dry-Run) produces no error, no warning, and the option you
|
|
36
|
+
// meant to set just quietly keeps its default. Post-validate instead of
|
|
37
|
+
// trusting strict:false's leniency for everything.
|
|
38
|
+
assertKnownOptions(parsed.values, options, { command: 'canon build' });
|
|
31
39
|
|
|
32
40
|
const projectName = parsed.values.project || parsed.values.game;
|
|
33
41
|
if (!projectName) {
|
|
@@ -38,14 +46,13 @@ export async function run(argv) {
|
|
|
38
46
|
);
|
|
39
47
|
}
|
|
40
48
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
+
// SDL-H12: resolve via the hardened workspace-root-aware resolver instead of
|
|
50
|
+
// join(process.cwd(), 'projects', projectName) — for a globally-installed
|
|
51
|
+
// sdlab the user is never inside the package directory, so process.cwd()
|
|
52
|
+
// silently pointed at the wrong place. getProjectRoot() also subsumes the
|
|
53
|
+
// existence check this used to do by hand (same INPUT_UNKNOWN_PROJECT shape
|
|
54
|
+
// as every other command, with an accurate "sdlab init" hint).
|
|
55
|
+
const projectRoot = getProjectRoot(projectName);
|
|
49
56
|
|
|
50
57
|
const only = parsed.values.only
|
|
51
58
|
? parsed.values.only.split(',').map((s) => s.trim()).filter(Boolean)
|
package/scripts/canon-drift.js
CHANGED
|
@@ -19,24 +19,33 @@ import { loadSchema } from '../lib/canon-build/load-schema.js';
|
|
|
19
19
|
import { readFreezeStatus, resolveWatchFields, computeWatchHash } from '../lib/freeze-stamp.js';
|
|
20
20
|
import { readEventsSince } from '../lib/freeze-events.js';
|
|
21
21
|
import { inputError } from '../lib/errors.js';
|
|
22
|
+
import { getProjectRoot } from '../lib/paths.js';
|
|
23
|
+
import { assertKnownOptions } from './_shared-args.js';
|
|
22
24
|
|
|
23
25
|
export async function run(argv) {
|
|
26
|
+
const options = {
|
|
27
|
+
project: { type: 'string' },
|
|
28
|
+
game: { type: 'string' },
|
|
29
|
+
since: { type: 'string' },
|
|
30
|
+
json: { type: 'boolean', default: false },
|
|
31
|
+
};
|
|
24
32
|
const parsed = parseArgs({
|
|
25
33
|
args: argv,
|
|
26
|
-
options
|
|
27
|
-
project: { type: 'string' },
|
|
28
|
-
game: { type: 'string' },
|
|
29
|
-
since: { type: 'string' },
|
|
30
|
-
json: { type: 'boolean', default: false },
|
|
31
|
-
},
|
|
34
|
+
options,
|
|
32
35
|
allowPositionals: true,
|
|
33
36
|
strict: false,
|
|
34
37
|
});
|
|
38
|
+
// H4: see canon-build.js's comment — strict:false's leniency on unknown
|
|
39
|
+
// flags means a typo'd optional flag (e.g. --sinse instead of --since)
|
|
40
|
+
// silently no-ops instead of erroring.
|
|
41
|
+
assertKnownOptions(parsed.values, options, { command: 'canon drift' });
|
|
35
42
|
|
|
36
43
|
const projectName = parsed.values.project || parsed.values.game;
|
|
37
44
|
if (!projectName) throw inputError('INPUT_MISSING_FLAG', '--project required');
|
|
38
45
|
|
|
39
|
-
|
|
46
|
+
// SDL-H12: resolve via the hardened workspace-root-aware resolver, not
|
|
47
|
+
// process.cwd() (wrong base for a globally-installed sdlab).
|
|
48
|
+
const projectRoot = getProjectRoot(projectName);
|
|
40
49
|
const config = await loadBuildConfig(join(projectRoot, 'canon-build', 'config.json'), projectRoot);
|
|
41
50
|
|
|
42
51
|
// Load latest build manifest for the stamped watch-hashes
|
|
@@ -10,25 +10,33 @@ import { loadCanonEntriesInDir } from '../lib/canon-build/load-entry.js';
|
|
|
10
10
|
import { readFreezeStatus, resolveWatchFields } from '../lib/freeze-stamp.js';
|
|
11
11
|
import { readEventsFor } from '../lib/freeze-events.js';
|
|
12
12
|
import { inputError } from '../lib/errors.js';
|
|
13
|
+
import { getProjectRoot } from '../lib/paths.js';
|
|
14
|
+
import { assertKnownOptions } from './_shared-args.js';
|
|
13
15
|
|
|
14
16
|
export async function run(argv) {
|
|
17
|
+
const options = {
|
|
18
|
+
project: { type: 'string' },
|
|
19
|
+
game: { type: 'string' },
|
|
20
|
+
json: { type: 'boolean', default: false },
|
|
21
|
+
};
|
|
15
22
|
const parsed = parseArgs({
|
|
16
23
|
args: argv,
|
|
17
|
-
options
|
|
18
|
-
project: { type: 'string' },
|
|
19
|
-
game: { type: 'string' },
|
|
20
|
-
json: { type: 'boolean', default: false },
|
|
21
|
-
},
|
|
24
|
+
options,
|
|
22
25
|
allowPositionals: true,
|
|
23
26
|
strict: false,
|
|
24
27
|
});
|
|
28
|
+
// H4: see canon-build.js's comment — strict:false's leniency on unknown
|
|
29
|
+
// flags means a typo'd optional flag silently no-ops instead of erroring.
|
|
30
|
+
assertKnownOptions(parsed.values, options, { command: 'canon freeze-status' });
|
|
25
31
|
|
|
26
32
|
const entityIdArg = (parsed.positionals || [])[0];
|
|
27
33
|
if (!entityIdArg) throw inputError('INPUT_MISSING_ARG', 'sdlab canon freeze-status requires an entity id.');
|
|
28
34
|
const projectName = parsed.values.project || parsed.values.game;
|
|
29
35
|
if (!projectName) throw inputError('INPUT_MISSING_FLAG', '--project required');
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
// SDL-H12: resolve via the hardened workspace-root-aware resolver, not
|
|
38
|
+
// process.cwd() (wrong base for a globally-installed sdlab).
|
|
39
|
+
const projectRoot = getProjectRoot(projectName);
|
|
32
40
|
const config = await loadBuildConfig(join(projectRoot, 'canon-build', 'config.json'), projectRoot);
|
|
33
41
|
|
|
34
42
|
let resolved = null;
|
package/scripts/canon-freeze.js
CHANGED
|
@@ -18,23 +18,33 @@ import { loadCanonEntriesInDir, entryId } from '../lib/canon-build/load-entry.js
|
|
|
18
18
|
import { buildFreezeBlock, FREEZE_STATUSES, resolveWatchFields } from '../lib/freeze-stamp.js';
|
|
19
19
|
import { appendEvent } from '../lib/freeze-events.js';
|
|
20
20
|
import { inputError } from '../lib/errors.js';
|
|
21
|
+
import { getProjectRoot } from '../lib/paths.js';
|
|
22
|
+
import { assertKnownOptions } from './_shared-args.js';
|
|
21
23
|
|
|
22
24
|
export async function run(argv) {
|
|
25
|
+
const options = {
|
|
26
|
+
project: { type: 'string' },
|
|
27
|
+
game: { type: 'string' },
|
|
28
|
+
reason: { type: 'string' },
|
|
29
|
+
status: { type: 'string', default: 'frozen' },
|
|
30
|
+
watch: { type: 'string' },
|
|
31
|
+
build: { type: 'string' },
|
|
32
|
+
by: { type: 'string', default: 'mike' },
|
|
33
|
+
json: { type: 'boolean', default: false },
|
|
34
|
+
};
|
|
23
35
|
const parsed = parseArgs({
|
|
24
36
|
args: argv,
|
|
25
|
-
options
|
|
26
|
-
project: { type: 'string' },
|
|
27
|
-
game: { type: 'string' },
|
|
28
|
-
reason: { type: 'string' },
|
|
29
|
-
status: { type: 'string', default: 'frozen' },
|
|
30
|
-
watch: { type: 'string' },
|
|
31
|
-
build: { type: 'string' },
|
|
32
|
-
by: { type: 'string', default: 'mike' },
|
|
33
|
-
json: { type: 'boolean', default: false },
|
|
34
|
-
},
|
|
37
|
+
options,
|
|
35
38
|
allowPositionals: true,
|
|
36
39
|
strict: false,
|
|
37
40
|
});
|
|
41
|
+
// H4: see canon-build.js's comment — strict:false's leniency on unknown
|
|
42
|
+
// flags means a typo'd optional flag silently no-ops instead of erroring.
|
|
43
|
+
// This one matters more than most: `sdlab canon freeze X --staus
|
|
44
|
+
// soft-advisory` (typo) would silently keep status at its default
|
|
45
|
+
// 'frozen' — a HARD block — instead of the 'soft-advisory' the operator
|
|
46
|
+
// typed, and a typo'd --watch/--build silently drops a real override too.
|
|
47
|
+
assertKnownOptions(parsed.values, options, { command: 'canon freeze' });
|
|
38
48
|
|
|
39
49
|
const positional = parsed.positionals || [];
|
|
40
50
|
const entityIdArg = positional[0];
|
|
@@ -72,7 +82,13 @@ export async function run(argv) {
|
|
|
72
82
|
);
|
|
73
83
|
}
|
|
74
84
|
|
|
75
|
-
|
|
85
|
+
// SDL-H12: was join(process.cwd(), 'projects', projectName) — wrong base for
|
|
86
|
+
// a globally-installed sdlab. getProjectRoot() resolves from the workspace
|
|
87
|
+
// root (not cwd) and throws a clear INPUT_UNKNOWN_PROJECT before ever
|
|
88
|
+
// reaching loadBuildConfig(), instead of a deeper, misleading
|
|
89
|
+
// CANON_BUILD_CONFIG_NOT_FOUND pointed at a config that "already exists" —
|
|
90
|
+
// just under a different (correct) project root.
|
|
91
|
+
const projectRoot = getProjectRoot(projectName);
|
|
76
92
|
const configPath = join(projectRoot, 'canon-build', 'config.json');
|
|
77
93
|
const config = await loadBuildConfig(configPath, projectRoot);
|
|
78
94
|
|
|
@@ -18,21 +18,27 @@ import { loadCanonEntriesInDir } from '../lib/canon-build/load-entry.js';
|
|
|
18
18
|
import { appendOverrideToFreezeBlock, readFreezeStatus } from '../lib/freeze-stamp.js';
|
|
19
19
|
import { appendEvent } from '../lib/freeze-events.js';
|
|
20
20
|
import { inputError } from '../lib/errors.js';
|
|
21
|
+
import { getProjectRoot } from '../lib/paths.js';
|
|
22
|
+
import { assertKnownOptions } from './_shared-args.js';
|
|
21
23
|
|
|
22
24
|
export async function run(argv) {
|
|
25
|
+
const options = {
|
|
26
|
+
project: { type: 'string' },
|
|
27
|
+
game: { type: 'string' },
|
|
28
|
+
reason: { type: 'string' },
|
|
29
|
+
by: { type: 'string', default: 'mike' },
|
|
30
|
+
build: { type: 'string' },
|
|
31
|
+
json: { type: 'boolean', default: false },
|
|
32
|
+
};
|
|
23
33
|
const parsed = parseArgs({
|
|
24
34
|
args: argv,
|
|
25
|
-
options
|
|
26
|
-
project: { type: 'string' },
|
|
27
|
-
game: { type: 'string' },
|
|
28
|
-
reason: { type: 'string' },
|
|
29
|
-
by: { type: 'string', default: 'mike' },
|
|
30
|
-
build: { type: 'string' },
|
|
31
|
-
json: { type: 'boolean', default: false },
|
|
32
|
-
},
|
|
35
|
+
options,
|
|
33
36
|
allowPositionals: true,
|
|
34
37
|
strict: false,
|
|
35
38
|
});
|
|
39
|
+
// H4: see canon-build.js's comment — strict:false's leniency on unknown
|
|
40
|
+
// flags means a typo'd optional flag silently no-ops instead of erroring.
|
|
41
|
+
assertKnownOptions(parsed.values, options, { command: 'canon unfreeze' });
|
|
36
42
|
|
|
37
43
|
const entityIdArg = (parsed.positionals || [])[0];
|
|
38
44
|
if (!entityIdArg) {
|
|
@@ -48,7 +54,9 @@ export async function run(argv) {
|
|
|
48
54
|
);
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
|
|
57
|
+
// SDL-H12: resolve via the hardened workspace-root-aware resolver, not
|
|
58
|
+
// process.cwd() (wrong base for a globally-installed sdlab).
|
|
59
|
+
const projectRoot = getProjectRoot(projectName);
|
|
52
60
|
const config = await loadBuildConfig(join(projectRoot, 'canon-build', 'config.json'), projectRoot);
|
|
53
61
|
const resolved = await findEntryFile(config, entityIdArg);
|
|
54
62
|
if (!resolved) {
|
package/scripts/card.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { writeFile } from 'node:fs/promises';
|
|
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';
|
|
@@ -27,7 +27,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
const projectName = flags.project || getProjectName(argv);
|
|
30
|
-
const projectRoot =
|
|
30
|
+
const projectRoot = getProjectRoot(projectName);
|
|
31
31
|
const subcommand = positionals[0] || 'generate';
|
|
32
32
|
|
|
33
33
|
if (subcommand === 'generate') {
|
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
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
* sdlab critique --project star-freight --run run_2026-04-15_001 --json
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { parseArgs, getProjectName } from '../lib/args.js';
|
|
11
|
+
import { parseArgs, getProjectName, parseNumberFlag } from '../lib/args.js';
|
|
12
12
|
import { getProjectRoot } from '../lib/paths.js';
|
|
13
|
-
import { critiqueRun, saveCritique } from '../lib/critique-engine.js';
|
|
14
|
-
import { renderCritiqueMarkdown, renderCritiqueText } from '../lib/critique-render.js';
|
|
13
|
+
import { critiqueRun, saveCritique, triageCandidates, DEFAULT_DRIFT_THRESHOLD } from '../lib/critique-engine.js';
|
|
14
|
+
import { renderCritiqueMarkdown, renderCritiqueText, renderCritiqueTriage } from '../lib/critique-render.js';
|
|
15
15
|
import { info } from '../lib/log.js';
|
|
16
16
|
|
|
17
17
|
export async function run(argv = process.argv.slice(2)) {
|
|
@@ -20,16 +20,20 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
20
20
|
project: { type: 'string' },
|
|
21
21
|
run: { type: 'string' },
|
|
22
22
|
json: { type: 'boolean' },
|
|
23
|
+
triage: { type: 'boolean' },
|
|
24
|
+
'drift-threshold': { type: 'string' },
|
|
23
25
|
},
|
|
24
26
|
deprecated: { game: 'project' },
|
|
25
27
|
});
|
|
26
28
|
|
|
27
29
|
if (!flags.run) {
|
|
28
|
-
console.log('Usage: sdlab critique --run <id> [--project <name>]');
|
|
30
|
+
console.log('Usage: sdlab critique --run <id> [--project <name>] [--triage]');
|
|
29
31
|
console.log('');
|
|
30
32
|
console.log('Options:');
|
|
31
|
-
console.log(' --run <id>
|
|
32
|
-
console.log(' --
|
|
33
|
+
console.log(' --run <id> Run to critique (required)');
|
|
34
|
+
console.log(' --triage Show only candidates needing attention (off-model OR ≥ threshold drift issues)');
|
|
35
|
+
console.log(` --drift-threshold <n> Drift-issue count that flags a candidate under --triage (default ${DEFAULT_DRIFT_THRESHOLD})`);
|
|
36
|
+
console.log(' --json Output raw JSON');
|
|
33
37
|
return;
|
|
34
38
|
}
|
|
35
39
|
|
|
@@ -48,10 +52,31 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
48
52
|
runId: flags.run,
|
|
49
53
|
});
|
|
50
54
|
|
|
51
|
-
// Save
|
|
55
|
+
// Save the FULL report — triage is a view over it, never a mutation.
|
|
52
56
|
const md = renderCritiqueMarkdown(report);
|
|
53
57
|
await saveCritique(projectRoot, flags.run, report, md);
|
|
54
58
|
|
|
59
|
+
// UNCERTAINTY_GATED_HUMANS: --triage surfaces only the candidates that need a
|
|
60
|
+
// human — off-model OR >= threshold drift issues — so attention isn't spent on
|
|
61
|
+
// every item. The full critique.json still holds all candidates.
|
|
62
|
+
if (flags.triage) {
|
|
63
|
+
const driftThreshold = flags['drift-threshold']
|
|
64
|
+
? parseNumberFlag('drift-threshold', flags['drift-threshold'], { int: true, min: 0 })
|
|
65
|
+
: DEFAULT_DRIFT_THRESHOLD;
|
|
66
|
+
const triage = triageCandidates(report.candidates, { driftThreshold });
|
|
67
|
+
if (flags.json) {
|
|
68
|
+
console.log(JSON.stringify({
|
|
69
|
+
run_id: report.run_id,
|
|
70
|
+
triage: { drift_threshold: driftThreshold, flagged_count: triage.flagged.length, suppressed_count: triage.suppressed.length },
|
|
71
|
+
candidates: triage.flagged,
|
|
72
|
+
}, null, 2));
|
|
73
|
+
} else {
|
|
74
|
+
console.log(renderCritiqueTriage(report, triage));
|
|
75
|
+
console.log(`Full critique saved to: runs/${flags.run}/critique.json`);
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
55
80
|
if (flags.json) {
|
|
56
81
|
console.log(JSON.stringify(report, null, 2));
|
|
57
82
|
} else {
|
|
@@ -59,9 +84,24 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
59
84
|
const action = report.recommended_action;
|
|
60
85
|
console.log(`Critique saved to: runs/${flags.run}/critique.json`);
|
|
61
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.
|
|
62
95
|
if (action.mode === 'refine_from_one' && action.preferred_candidate) {
|
|
63
96
|
console.log('');
|
|
64
|
-
|
|
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
|
+
}
|
|
65
105
|
} else if (action.mode === 'accept_one' && action.preferred_candidate) {
|
|
66
106
|
console.log('');
|
|
67
107
|
info(`Preferred candidate: ${action.preferred_candidate}`);
|
package/scripts/curate.js
CHANGED
|
@@ -13,11 +13,12 @@
|
|
|
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";
|
|
20
20
|
import { assertNotFrozenByAssetPath } from "../lib/freeze-gate.js";
|
|
21
|
+
import { deriveGeneratorModel, warnIfSelfVerification, HUMAN_JUDGE } from "../lib/verifier.js";
|
|
21
22
|
|
|
22
23
|
export async function run(argv = process.argv.slice(2)) {
|
|
23
24
|
const { flags, positionals } = parseArgs(argv, {
|
|
@@ -34,7 +35,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
34
35
|
});
|
|
35
36
|
|
|
36
37
|
const projectName = flags.project || getProjectName(argv);
|
|
37
|
-
const GAME_ROOT =
|
|
38
|
+
const GAME_ROOT = getProjectRoot(projectName);
|
|
38
39
|
const dryRun = flags['dry-run'] || flags.dryRun;
|
|
39
40
|
|
|
40
41
|
if (flags.list) {
|
|
@@ -132,11 +133,19 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
132
133
|
);
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
// EXTERNAL_VERIFIER: record WHO judged (human) and WHAT generated the artifact,
|
|
137
|
+
// and warn if they ever coincide (they never do for human curation — the field
|
|
138
|
+
// + warning install the muscle for when an LLM judge enters the loop).
|
|
139
|
+
const generator_model = deriveGeneratorModel(record.provenance);
|
|
140
|
+
warnIfSelfVerification(HUMAN_JUDGE, generator_model, assetId);
|
|
141
|
+
|
|
135
142
|
// Now safely update the record. If writing fails, try to restore the image.
|
|
136
143
|
record.asset_path = newPath;
|
|
137
144
|
record.judgment = {
|
|
138
145
|
status,
|
|
139
146
|
reviewer: "human:mike",
|
|
147
|
+
judged_by_model: HUMAN_JUDGE,
|
|
148
|
+
generator_model,
|
|
140
149
|
reviewed_at: new Date().toISOString(),
|
|
141
150
|
explanation,
|
|
142
151
|
criteria_scores,
|
|
@@ -164,6 +173,18 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
164
173
|
result(` record: records/${assetId}.json`);
|
|
165
174
|
console.log(` ${explanation}`);
|
|
166
175
|
if (failure_modes.length) console.log(` Failures: ${failure_modes.join(", ")}`);
|
|
176
|
+
|
|
177
|
+
// UNCERTAINTY_GATED_HUMANS: contrastive advisory on borderline (Horvitz CHI
|
|
178
|
+
// 1999). State the default (HOLD — borderline is not training-eligible) and
|
|
179
|
+
// what a later promotion must justify, so the promoter argues against a stated
|
|
180
|
+
// default rather than nudging a status upward unexamined.
|
|
181
|
+
if (status === 'borderline') {
|
|
182
|
+
const drift = failure_modes.length ? failure_modes.join(', ') : 'the drift you noted';
|
|
183
|
+
console.log(
|
|
184
|
+
` \x1b[2mDefault: HOLD — 'borderline' is not training-eligible. ` +
|
|
185
|
+
`To later promote to 'approved', justify why ${drift} is acceptable for canon (re-run curate approved with that reason).\x1b[0m`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
167
188
|
}
|
|
168
189
|
|
|
169
190
|
async function listUncurated(GAME_ROOT) {
|