@mcptoolshop/style-dataset-lab 3.3.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.
Files changed (92) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.es.md +35 -25
  3. package/README.fr.md +38 -28
  4. package/README.hi.md +36 -26
  5. package/README.it.md +37 -27
  6. package/README.ja.md +40 -30
  7. package/README.md +15 -5
  8. package/README.pt-BR.md +35 -25
  9. package/README.zh.md +33 -23
  10. package/bin/sdlab.js +51 -4
  11. package/lib/adapters/ai-toolkit.js +37 -5
  12. package/lib/adapters/comfyui-runner.js +79 -26
  13. package/lib/adapters/comfyui-workflows.js +10 -2
  14. package/lib/adapters/diffusers-lora.js +37 -5
  15. package/lib/adapters/generic-image-caption.js +43 -5
  16. package/lib/args.js +16 -1
  17. package/lib/batch-modes.js +29 -8
  18. package/lib/batch-runs.js +29 -1
  19. package/lib/batch-sheet-render.js +13 -3
  20. package/lib/brief-compiler.js +19 -0
  21. package/lib/candidate-sheet.js +327 -0
  22. package/lib/canon-build/build.js +146 -93
  23. package/lib/captions.js +57 -0
  24. package/lib/card.js +74 -2
  25. package/lib/comfyui.js +116 -18
  26. package/lib/config.js +116 -12
  27. package/lib/critique-engine.js +36 -2
  28. package/lib/eligibility.js +121 -9
  29. package/lib/eval-pack.js +14 -14
  30. package/lib/eval-runs.js +33 -0
  31. package/lib/export.js +122 -34
  32. package/lib/freeze-gate.js +13 -3
  33. package/lib/freeze-stamp.js +28 -3
  34. package/lib/generated-provenance.js +16 -1
  35. package/lib/implementation-packs.js +95 -17
  36. package/lib/ingest.js +396 -0
  37. package/lib/judgment-provenance.js +220 -0
  38. package/lib/measure.js +422 -0
  39. package/lib/paths.js +196 -7
  40. package/lib/reingest-selected.js +98 -16
  41. package/lib/run-manifest.js +102 -16
  42. package/lib/run-summary.js +12 -6
  43. package/lib/runtime-runs.js +45 -4
  44. package/lib/snapshot.js +114 -15
  45. package/lib/split.js +364 -48
  46. package/lib/training-manifests.js +114 -21
  47. package/lib/training-packages.js +147 -15
  48. package/lib/workflow-profiles.js +24 -3
  49. package/package.json +3 -2
  50. package/runtime/comfyui/txt2img-qwen.json +38 -0
  51. package/runtime/comfyui/txt2img-standard.json +21 -0
  52. package/scripts/_resume.js +62 -0
  53. package/scripts/_shared-args.js +44 -0
  54. package/scripts/brief-compile.js +2 -1
  55. package/scripts/brief-show.js +2 -1
  56. package/scripts/canon-bind.js +2 -2
  57. package/scripts/canon-build.js +27 -20
  58. package/scripts/canon-drift.js +16 -7
  59. package/scripts/canon-freeze-status.js +14 -6
  60. package/scripts/canon-freeze.js +27 -11
  61. package/scripts/canon-unfreeze.js +17 -9
  62. package/scripts/card.js +2 -2
  63. package/scripts/compare.js +2 -2
  64. package/scripts/critique.js +16 -1
  65. package/scripts/curate.js +2 -2
  66. package/scripts/doctor.js +181 -55
  67. package/scripts/eligibility.js +42 -2
  68. package/scripts/eval-pack.js +2 -2
  69. package/scripts/eval-run.js +2 -2
  70. package/scripts/export.js +2 -2
  71. package/scripts/generate-controlnet.js +45 -25
  72. package/scripts/generate-identity.js +53 -23
  73. package/scripts/generate-ipadapter.js +44 -23
  74. package/scripts/generate.js +48 -5
  75. package/scripts/implementation-pack.js +2 -2
  76. package/scripts/ingest.js +145 -0
  77. package/scripts/init.js +73 -13
  78. package/scripts/measure.js +170 -0
  79. package/scripts/measure_image.py +458 -0
  80. package/scripts/migrate-records.js +142 -0
  81. package/scripts/painterly-test.js +4 -3
  82. package/scripts/painterly.js +7 -3
  83. package/scripts/qwen_generate.py +138 -20
  84. package/scripts/reingest.js +2 -2
  85. package/scripts/sheet.js +142 -0
  86. package/scripts/snapshot.js +2 -2
  87. package/scripts/split.js +2 -2
  88. package/scripts/training-manifest.js +2 -2
  89. package/scripts/training-package.js +2 -2
  90. package/scripts/training-profile.js +2 -2
  91. package/scripts/workflow-list.js +2 -1
  92. package/scripts/workflow-show.js +2 -1
@@ -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
+ }
@@ -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((err) => { console.error(err.message || err); process.exit(1); });
134
+ run().catch(handleCliError);
134
135
  }
@@ -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((err) => { console.error(err.message || err); process.exit(1); });
53
+ run().catch(handleCliError);
53
54
  }
@@ -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 { REPO_ROOT } from '../lib/paths.js';
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 = join(REPO_ROOT, 'projects', projectName);
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');
@@ -7,27 +7,35 @@
7
7
  */
8
8
 
9
9
  import { parseArgs } from 'node:util';
10
- import { join } from 'node:path';
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
- const projectRoot = join(process.cwd(), 'projects', projectName);
42
- if (!existsSync(projectRoot)) {
43
- throw inputError(
44
- 'INPUT_PROJECT_NOT_FOUND',
45
- `Project directory not found: ${projectRoot}`,
46
- `Run from the style-dataset-lab repo root, or scaffold the project with "sdlab init ${projectName}".`,
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)
@@ -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
- const projectRoot = join(process.cwd(), 'projects', projectName);
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
- const projectRoot = join(process.cwd(), 'projects', projectName);
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;
@@ -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
- const projectRoot = join(process.cwd(), 'projects', projectName);
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
- const projectRoot = join(process.cwd(), 'projects', projectName);
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 { REPO_ROOT } from '../lib/paths.js';
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 = join(REPO_ROOT, 'projects', projectName);
30
+ const projectRoot = getProjectRoot(projectName);
31
31
  const subcommand = positionals[0] || 'generate';
32
32
 
33
33
  if (subcommand === 'generate') {
@@ -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 { REPO_ROOT } from "../lib/paths.js";
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 = join(REPO_ROOT, 'projects', projectName);
28
+ const GAME_ROOT = getProjectRoot(projectName);
29
29
 
30
30
  const [assetAId, assetBId, winner, ...reasonParts] = positionals;
31
31
 
@@ -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
- info(`Next: sdlab refine --run ${flags.run} --pick ${action.preferred_candidate}`);
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 { REPO_ROOT } from "../lib/paths.js";
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 = join(REPO_ROOT, 'projects', projectName);
38
+ const GAME_ROOT = getProjectRoot(projectName);
39
39
  const dryRun = flags['dry-run'] || flags.dryRun;
40
40
 
41
41
  if (flags.list) {