@mcptoolshop/style-dataset-lab 1.2.1 → 2.0.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 +45 -0
- package/README.md +95 -116
- package/bin/sdlab.js +103 -0
- package/lib/args.js +106 -0
- package/lib/comfyui.js +109 -0
- package/lib/config.js +104 -0
- package/lib/deprecation.js +14 -0
- package/lib/paths.js +38 -0
- package/lib/records.js +69 -0
- package/package.json +13 -4
- package/scripts/bulk-curate-wave2-5.js +1 -1
- package/scripts/bulk-curate-waves11-18.js +1 -1
- package/scripts/canon-bind.js +268 -332
- package/scripts/compare.js +40 -30
- package/scripts/curate-wave25.js +1 -1
- package/scripts/curate.js +34 -28
- package/scripts/doctor.js +321 -0
- package/scripts/generate-controlnet.js +36 -121
- package/scripts/generate-identity.js +38 -106
- package/scripts/generate-ipadapter.js +42 -119
- package/scripts/generate.js +22 -81
- package/scripts/init.js +226 -0
- package/scripts/migrate-records.js +15 -21
- package/scripts/painterly-test.js +20 -47
- package/scripts/painterly.js +27 -106
- package/templates/domains/architecture/constitution.json +97 -0
- package/templates/domains/architecture/lanes.json +41 -0
- package/templates/domains/architecture/project.json +20 -0
- package/templates/domains/architecture/rubric.json +40 -0
- package/templates/domains/architecture/terminology.json +19 -0
- package/templates/domains/character-design/constitution.json +88 -0
- package/templates/domains/character-design/lanes.json +35 -0
- package/templates/domains/character-design/project.json +17 -0
- package/templates/domains/character-design/rubric.json +38 -0
- package/templates/domains/character-design/terminology.json +21 -0
- package/templates/domains/creature-design/constitution.json +87 -0
- package/templates/domains/creature-design/lanes.json +41 -0
- package/templates/domains/creature-design/project.json +18 -0
- package/templates/domains/creature-design/rubric.json +39 -0
- package/templates/domains/creature-design/terminology.json +20 -0
- package/templates/domains/game-art/constitution.json +113 -0
- package/templates/domains/game-art/lanes.json +47 -0
- package/templates/domains/game-art/project.json +17 -0
- package/templates/domains/game-art/rubric.json +36 -0
- package/templates/domains/game-art/terminology.json +30 -0
- package/templates/domains/vehicle-mech/constitution.json +87 -0
- package/templates/domains/vehicle-mech/lanes.json +41 -0
- package/templates/domains/vehicle-mech/project.json +17 -0
- package/templates/domains/vehicle-mech/rubric.json +40 -0
- package/templates/domains/vehicle-mech/terminology.json +21 -0
package/scripts/compare.js
CHANGED
|
@@ -5,37 +5,54 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
7
|
* node scripts/compare.js <asset_a_id> <asset_b_id> <winner: a|b|tie> <reasoning>
|
|
8
|
-
*
|
|
8
|
+
* sdlab compare iron_sword_base iron_sword_icon_mid a "Better silhouette" --project star-freight
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { readFile, writeFile, readdir, mkdir } from "node:fs/promises";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
+
import { getProjectName } from "../lib/args.js";
|
|
14
|
+
import { REPO_ROOT } from "../lib/paths.js";
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
16
|
+
function getFlagValue(args, flag) {
|
|
17
|
+
const prefix = `--${flag}=`;
|
|
18
|
+
for (const a of args) {
|
|
19
|
+
if (a.startsWith(prefix)) return a.slice(prefix.length);
|
|
20
|
+
}
|
|
21
|
+
const idx = args.indexOf(`--${flag}`);
|
|
22
|
+
if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
27
|
+
const projectName = getProjectName(argv);
|
|
28
|
+
const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
|
|
29
|
+
|
|
30
|
+
// Strip --project/--game and their values from positional parsing
|
|
31
|
+
const args = argv.filter((a, i) => {
|
|
32
|
+
if (a === '--project' || a === '--game') return false;
|
|
33
|
+
if (i > 0 && (argv[i - 1] === '--project' || argv[i - 1] === '--game')) return false;
|
|
34
|
+
return true;
|
|
35
|
+
});
|
|
17
36
|
|
|
18
|
-
async function main() {
|
|
19
|
-
const args = process.argv.slice(2);
|
|
20
37
|
const [assetAId, assetBId, winner, ...reasonParts] = args;
|
|
21
38
|
|
|
22
39
|
if (!assetAId || !assetBId || !winner) {
|
|
23
|
-
|
|
24
|
-
process.exit(1);
|
|
40
|
+
throw new Error("Usage: sdlab compare <asset_a_id> <asset_b_id> <a|b|tie> <reasoning>");
|
|
25
41
|
}
|
|
26
42
|
|
|
27
43
|
if (!["a", "b", "tie"].includes(winner)) {
|
|
28
|
-
|
|
29
|
-
process.exit(1);
|
|
44
|
+
throw new Error(`Invalid winner: ${winner}. Must be a, b, or tie.`);
|
|
30
45
|
}
|
|
31
46
|
|
|
32
|
-
const reasoning = reasonParts.join(" ") || null;
|
|
47
|
+
const reasoning = reasonParts.filter(r => !r.startsWith('--')).join(" ") || null;
|
|
33
48
|
|
|
34
49
|
// Load records to get paths
|
|
35
|
-
const recordA = await loadRecord(assetAId);
|
|
36
|
-
const recordB = await loadRecord(assetBId);
|
|
50
|
+
const recordA = await loadRecord(GAME_ROOT, assetAId);
|
|
51
|
+
const recordB = await loadRecord(GAME_ROOT, assetBId);
|
|
37
52
|
|
|
38
|
-
if (!recordA || !recordB)
|
|
53
|
+
if (!recordA || !recordB) {
|
|
54
|
+
throw new Error("Could not load one or both records.");
|
|
55
|
+
}
|
|
39
56
|
|
|
40
57
|
// Parse optional scores
|
|
41
58
|
const scoresStr = getFlagValue(args, "scores");
|
|
@@ -52,7 +69,7 @@ async function main() {
|
|
|
52
69
|
}
|
|
53
70
|
|
|
54
71
|
// Build comparison ID
|
|
55
|
-
const existingComps = await countComparisons();
|
|
72
|
+
const existingComps = await countComparisons(GAME_ROOT);
|
|
56
73
|
const cmpId = `cmp_${String(existingComps + 1).padStart(3, "0")}`;
|
|
57
74
|
|
|
58
75
|
const comparison = {
|
|
@@ -81,7 +98,7 @@ async function main() {
|
|
|
81
98
|
if (reasoning) console.log(` ${reasoning}`);
|
|
82
99
|
}
|
|
83
100
|
|
|
84
|
-
async function loadRecord(id) {
|
|
101
|
+
async function loadRecord(GAME_ROOT, id) {
|
|
85
102
|
try {
|
|
86
103
|
const path = join(GAME_ROOT, `records/${id}.json`);
|
|
87
104
|
return JSON.parse(await readFile(path, "utf-8"));
|
|
@@ -91,7 +108,7 @@ async function loadRecord(id) {
|
|
|
91
108
|
}
|
|
92
109
|
}
|
|
93
110
|
|
|
94
|
-
async function countComparisons() {
|
|
111
|
+
async function countComparisons(GAME_ROOT) {
|
|
95
112
|
try {
|
|
96
113
|
const files = await readdir(join(GAME_ROOT, "comparisons"));
|
|
97
114
|
return files.filter((f) => f.endsWith(".json")).length;
|
|
@@ -100,17 +117,10 @@ async function countComparisons() {
|
|
|
100
117
|
}
|
|
101
118
|
}
|
|
102
119
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
|
|
110
|
-
return undefined;
|
|
120
|
+
// Direct execution guard
|
|
121
|
+
if (process.argv[1] && (process.argv[1].endsWith('compare.js') || process.argv[1].endsWith('compare'))) {
|
|
122
|
+
run().catch((err) => {
|
|
123
|
+
console.error(err.message || err);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
});
|
|
111
126
|
}
|
|
112
|
-
|
|
113
|
-
main().catch((err) => {
|
|
114
|
-
console.error(err);
|
|
115
|
-
process.exit(1);
|
|
116
|
-
});
|
package/scripts/curate-wave25.js
CHANGED
|
@@ -18,7 +18,7 @@ import { join, basename } from 'path';
|
|
|
18
18
|
|
|
19
19
|
const REPO_ROOT = new URL("..", import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1");
|
|
20
20
|
const GAME = process.argv.find((a, i) => process.argv[i - 1] === '--game') || 'star-freight';
|
|
21
|
-
const GAME_ROOT = join(REPO_ROOT, '
|
|
21
|
+
const GAME_ROOT = join(REPO_ROOT, 'projects', GAME);
|
|
22
22
|
|
|
23
23
|
const RECORDS_DIR = join(GAME_ROOT, 'records');
|
|
24
24
|
const CANDIDATES_DIR = join(GAME_ROOT, 'outputs', 'candidates');
|
package/scripts/curate.js
CHANGED
|
@@ -7,32 +7,47 @@
|
|
|
7
7
|
* node scripts/curate.js <asset_id> approved "Clean silhouette, correct palette" --scores silhouette:0.9,palette:0.8
|
|
8
8
|
* node scripts/curate.js <asset_id> rejected "3D render look, alpha halo" --failures alpha_halo,3d_render_look
|
|
9
9
|
* node scripts/curate.js --list (show uncurated candidates)
|
|
10
|
+
* sdlab curate <asset_id> approved "explanation" --project star-freight
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { readFile, writeFile, rename, readdir, mkdir } from "node:fs/promises";
|
|
13
14
|
import { join } from "node:path";
|
|
15
|
+
import { getProjectName } from "../lib/args.js";
|
|
16
|
+
import { REPO_ROOT } from "../lib/paths.js";
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
+
function getFlagValue(args, flag) {
|
|
19
|
+
const prefix = `--${flag}=`;
|
|
20
|
+
for (const a of args) {
|
|
21
|
+
if (a.startsWith(prefix)) return a.slice(prefix.length);
|
|
22
|
+
}
|
|
23
|
+
const idx = args.indexOf(`--${flag}`);
|
|
24
|
+
if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
18
27
|
|
|
19
|
-
async function
|
|
20
|
-
const
|
|
28
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
29
|
+
const projectName = getProjectName(argv);
|
|
30
|
+
const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
|
|
31
|
+
|
|
32
|
+
// Strip --project/--game and their values from positional parsing
|
|
33
|
+
const args = argv.filter((a, i) => {
|
|
34
|
+
if (a === '--project' || a === '--game') return false;
|
|
35
|
+
if (i > 0 && (argv[i - 1] === '--project' || argv[i - 1] === '--game')) return false;
|
|
36
|
+
return true;
|
|
37
|
+
});
|
|
21
38
|
|
|
22
39
|
if (args.includes("--list")) {
|
|
23
|
-
return listUncurated();
|
|
40
|
+
return listUncurated(GAME_ROOT);
|
|
24
41
|
}
|
|
25
42
|
|
|
26
43
|
const [assetId, status, explanation, ...rest] = args;
|
|
27
44
|
|
|
28
45
|
if (!assetId || !status || !explanation) {
|
|
29
|
-
|
|
30
|
-
process.exit(1);
|
|
46
|
+
throw new Error("Usage: sdlab curate <asset_id> <approved|rejected|borderline> <explanation> [--scores k:v,...] [--failures f1,f2]");
|
|
31
47
|
}
|
|
32
48
|
|
|
33
49
|
if (!["approved", "rejected", "borderline"].includes(status)) {
|
|
34
|
-
|
|
35
|
-
process.exit(1);
|
|
50
|
+
throw new Error(`Invalid status: ${status}. Must be approved, rejected, or borderline.`);
|
|
36
51
|
}
|
|
37
52
|
|
|
38
53
|
// Parse optional flags
|
|
@@ -56,8 +71,7 @@ async function main() {
|
|
|
56
71
|
try {
|
|
57
72
|
record = JSON.parse(await readFile(recordPath, "utf-8"));
|
|
58
73
|
} catch {
|
|
59
|
-
|
|
60
|
-
process.exit(1);
|
|
74
|
+
throw new Error(`Record not found: ${recordPath}`);
|
|
61
75
|
}
|
|
62
76
|
|
|
63
77
|
// Update record first (write before move to avoid orphaned images)
|
|
@@ -84,8 +98,7 @@ async function main() {
|
|
|
84
98
|
try {
|
|
85
99
|
await rename(oldPath, join(GAME_ROOT, newPath));
|
|
86
100
|
} catch {
|
|
87
|
-
|
|
88
|
-
process.exit(1);
|
|
101
|
+
throw new Error(`Could not move ${oldPath} to ${newPath}`);
|
|
89
102
|
}
|
|
90
103
|
|
|
91
104
|
console.log(`\x1b[32m✓\x1b[0m ${assetId} → ${status}`);
|
|
@@ -93,7 +106,7 @@ async function main() {
|
|
|
93
106
|
if (failure_modes.length) console.log(` Failures: ${failure_modes.join(", ")}`);
|
|
94
107
|
}
|
|
95
108
|
|
|
96
|
-
async function listUncurated() {
|
|
109
|
+
async function listUncurated(GAME_ROOT) {
|
|
97
110
|
const recordsDir = join(GAME_ROOT, "records");
|
|
98
111
|
let files;
|
|
99
112
|
try {
|
|
@@ -120,17 +133,10 @@ async function listUncurated() {
|
|
|
120
133
|
console.log(`\n${uncurated} uncurated, ${curated} curated`);
|
|
121
134
|
}
|
|
122
135
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
|
|
130
|
-
return undefined;
|
|
136
|
+
// Direct execution guard
|
|
137
|
+
if (process.argv[1] && (process.argv[1].endsWith('curate.js') || process.argv[1].endsWith('curate'))) {
|
|
138
|
+
run().catch((err) => {
|
|
139
|
+
console.error(err.message || err);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
});
|
|
131
142
|
}
|
|
132
|
-
|
|
133
|
-
main().catch((err) => {
|
|
134
|
-
console.error(err);
|
|
135
|
-
process.exit(1);
|
|
136
|
-
});
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* doctor.js — Validate project config completeness and correctness.
|
|
5
|
+
*
|
|
6
|
+
* Checks structural health without deep business logic linting.
|
|
7
|
+
* Every check produces a specific, actionable message on failure.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* sdlab project doctor --project star-freight
|
|
11
|
+
* node scripts/doctor.js --project my-project
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { getProjectName } from '../lib/args.js';
|
|
17
|
+
import { REPO_ROOT } from '../lib/paths.js';
|
|
18
|
+
|
|
19
|
+
const REQUIRED_CONFIG_FILES = [
|
|
20
|
+
'project.json',
|
|
21
|
+
'constitution.json',
|
|
22
|
+
'lanes.json',
|
|
23
|
+
'rubric.json',
|
|
24
|
+
'terminology.json',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const REQUIRED_DIRS = [
|
|
28
|
+
'canon',
|
|
29
|
+
'records',
|
|
30
|
+
'comparisons',
|
|
31
|
+
'inputs/prompts',
|
|
32
|
+
'outputs/candidates',
|
|
33
|
+
'outputs/approved',
|
|
34
|
+
'outputs/rejected',
|
|
35
|
+
'exports',
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
function pass(msg) { console.log(` \x1b[32m✓\x1b[0m ${msg}`); }
|
|
39
|
+
function fail(msg) { console.log(` \x1b[31m✗\x1b[0m ${msg}`); }
|
|
40
|
+
function warn(msg) { console.log(` \x1b[33m⚠\x1b[0m ${msg}`); }
|
|
41
|
+
|
|
42
|
+
function tryParseJson(filePath) {
|
|
43
|
+
try {
|
|
44
|
+
return { data: JSON.parse(readFileSync(filePath, 'utf-8')), error: null };
|
|
45
|
+
} catch (err) {
|
|
46
|
+
return { data: null, error: err.message };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
51
|
+
const projectName = getProjectName(argv);
|
|
52
|
+
const projectDir = join(REPO_ROOT, 'projects', projectName);
|
|
53
|
+
|
|
54
|
+
console.log(`\x1b[1msdlab project doctor\x1b[0m — ${projectName}`);
|
|
55
|
+
console.log(` Path: ${projectDir}\n`);
|
|
56
|
+
|
|
57
|
+
let passes = 0;
|
|
58
|
+
let failures = 0;
|
|
59
|
+
let warnings = 0;
|
|
60
|
+
|
|
61
|
+
// ── 1. Project directory exists ──
|
|
62
|
+
if (!existsSync(projectDir)) {
|
|
63
|
+
fail(`Project directory not found: ${projectDir}`);
|
|
64
|
+
console.log(`\n Run: sdlab init ${projectName} --domain <domain>`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
pass('Project directory exists');
|
|
68
|
+
passes++;
|
|
69
|
+
|
|
70
|
+
// ── 2. Required config files present and parseable ──
|
|
71
|
+
console.log('');
|
|
72
|
+
const configs = {};
|
|
73
|
+
for (const file of REQUIRED_CONFIG_FILES) {
|
|
74
|
+
const filePath = join(projectDir, file);
|
|
75
|
+
if (!existsSync(filePath)) {
|
|
76
|
+
fail(`Missing: ${file}`);
|
|
77
|
+
failures++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const { data, error } = tryParseJson(filePath);
|
|
81
|
+
if (error) {
|
|
82
|
+
fail(`${file} — invalid JSON: ${error}`);
|
|
83
|
+
failures++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
configs[file] = data;
|
|
87
|
+
pass(`${file} — valid JSON`);
|
|
88
|
+
passes++;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── 3. Required directories ──
|
|
92
|
+
console.log('');
|
|
93
|
+
for (const dir of REQUIRED_DIRS) {
|
|
94
|
+
if (existsSync(join(projectDir, dir))) {
|
|
95
|
+
pass(`${dir}/`);
|
|
96
|
+
passes++;
|
|
97
|
+
} else {
|
|
98
|
+
warn(`${dir}/ — missing (will be created on first use)`);
|
|
99
|
+
warnings++;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── 4. Project.json validation ──
|
|
104
|
+
const meta = configs['project.json'];
|
|
105
|
+
if (meta) {
|
|
106
|
+
console.log('');
|
|
107
|
+
if (meta.name) { pass(`project.json: name = "${meta.name}"`); passes++; }
|
|
108
|
+
else { fail('project.json: missing "name"'); failures++; }
|
|
109
|
+
|
|
110
|
+
if (meta.domain) { pass(`project.json: domain = "${meta.domain}"`); passes++; }
|
|
111
|
+
else { warn('project.json: no "domain" set'); warnings++; }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── 5. Constitution validation ──
|
|
115
|
+
const constitution = configs['constitution.json'];
|
|
116
|
+
if (constitution) {
|
|
117
|
+
console.log('');
|
|
118
|
+
const rules = constitution.rules || (Array.isArray(constitution) ? constitution : []);
|
|
119
|
+
if (rules.length === 0) {
|
|
120
|
+
fail('constitution.json: no rules defined');
|
|
121
|
+
failures++;
|
|
122
|
+
} else {
|
|
123
|
+
pass(`constitution.json: ${rules.length} rules`);
|
|
124
|
+
passes++;
|
|
125
|
+
|
|
126
|
+
// Check required rule fields
|
|
127
|
+
const ruleIds = new Set();
|
|
128
|
+
let ruleErrors = 0;
|
|
129
|
+
for (const rule of rules) {
|
|
130
|
+
if (!rule.id) { fail(`constitution.json: rule missing "id"`); ruleErrors++; continue; }
|
|
131
|
+
if (ruleIds.has(rule.id)) { fail(`constitution.json: duplicate rule ID "${rule.id}"`); ruleErrors++; }
|
|
132
|
+
ruleIds.add(rule.id);
|
|
133
|
+
if (!rule.dims || rule.dims.length === 0) {
|
|
134
|
+
fail(`constitution.json: rule ${rule.id} has no "dims" (scoring dimensions)`);
|
|
135
|
+
ruleErrors++;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (ruleErrors === 0) { pass('constitution.json: all rules have id + dims'); passes++; }
|
|
139
|
+
else { failures += ruleErrors; }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── 6. Lanes validation ──
|
|
144
|
+
const lanes = configs['lanes.json'];
|
|
145
|
+
if (lanes) {
|
|
146
|
+
console.log('');
|
|
147
|
+
const laneList = lanes.lanes || [];
|
|
148
|
+
if (laneList.length === 0) {
|
|
149
|
+
warn('lanes.json: no lanes defined (default lane only)');
|
|
150
|
+
warnings++;
|
|
151
|
+
} else {
|
|
152
|
+
pass(`lanes.json: ${laneList.length} lanes`);
|
|
153
|
+
passes++;
|
|
154
|
+
|
|
155
|
+
// Check lane IDs unique
|
|
156
|
+
const laneIds = new Set();
|
|
157
|
+
let laneErrors = 0;
|
|
158
|
+
for (const lane of laneList) {
|
|
159
|
+
if (!lane.id) { fail('lanes.json: lane missing "id"'); laneErrors++; continue; }
|
|
160
|
+
if (laneIds.has(lane.id)) { fail(`lanes.json: duplicate lane ID "${lane.id}"`); laneErrors++; }
|
|
161
|
+
laneIds.add(lane.id);
|
|
162
|
+
|
|
163
|
+
// Check patterns compile
|
|
164
|
+
for (const pattern of lane.id_patterns || []) {
|
|
165
|
+
try { new RegExp(pattern); }
|
|
166
|
+
catch { fail(`lanes.json: lane "${lane.id}" has invalid pattern "${pattern}"`); laneErrors++; }
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (laneErrors === 0) { pass('lanes.json: all lane IDs unique, patterns compile'); passes++; }
|
|
170
|
+
else { failures += laneErrors; }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!lanes.default_lane) {
|
|
174
|
+
warn('lanes.json: no "default_lane" set');
|
|
175
|
+
warnings++;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── 7. Rubric validation ──
|
|
180
|
+
const rubric = configs['rubric.json'];
|
|
181
|
+
if (rubric) {
|
|
182
|
+
console.log('');
|
|
183
|
+
const dims = rubric.dimensions || [];
|
|
184
|
+
if (dims.length === 0) {
|
|
185
|
+
fail('rubric.json: no dimensions defined');
|
|
186
|
+
failures++;
|
|
187
|
+
} else {
|
|
188
|
+
pass(`rubric.json: ${dims.length} dimensions`);
|
|
189
|
+
passes++;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!rubric.thresholds) {
|
|
193
|
+
fail('rubric.json: missing "thresholds"');
|
|
194
|
+
failures++;
|
|
195
|
+
} else {
|
|
196
|
+
pass('rubric.json: thresholds present');
|
|
197
|
+
passes++;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Cross-reference: constitution dims should exist in rubric
|
|
201
|
+
if (constitution && dims.length > 0) {
|
|
202
|
+
const dimSet = new Set(dims);
|
|
203
|
+
const rules = constitution.rules || [];
|
|
204
|
+
const missingDims = new Set();
|
|
205
|
+
for (const rule of rules) {
|
|
206
|
+
for (const d of rule.dims || []) {
|
|
207
|
+
if (!dimSet.has(d)) missingDims.add(d);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (missingDims.size > 0) {
|
|
211
|
+
fail(`rubric.json: constitution references dimensions not in rubric: ${[...missingDims].join(', ')}`);
|
|
212
|
+
failures++;
|
|
213
|
+
} else {
|
|
214
|
+
pass('rubric.json: all constitution dimensions present in rubric');
|
|
215
|
+
passes++;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Check failure_to_rules references valid rule IDs
|
|
220
|
+
if (rubric.failure_to_rules && constitution) {
|
|
221
|
+
const ruleIds = new Set((constitution.rules || []).map(r => r.id));
|
|
222
|
+
let badRefs = 0;
|
|
223
|
+
for (const [mode, ruleList] of Object.entries(rubric.failure_to_rules)) {
|
|
224
|
+
for (const ruleId of ruleList) {
|
|
225
|
+
if (!ruleIds.has(ruleId)) {
|
|
226
|
+
fail(`rubric.json: failure mode "${mode}" references unknown rule "${ruleId}"`);
|
|
227
|
+
badRefs++;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (badRefs === 0 && Object.keys(rubric.failure_to_rules).length > 0) {
|
|
232
|
+
pass(`rubric.json: ${Object.keys(rubric.failure_to_rules).length} failure modes, all reference valid rules`);
|
|
233
|
+
passes++;
|
|
234
|
+
}
|
|
235
|
+
if (badRefs > 0) failures += badRefs;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── 8. Terminology validation ──
|
|
240
|
+
const terminology = configs['terminology.json'];
|
|
241
|
+
if (terminology) {
|
|
242
|
+
console.log('');
|
|
243
|
+
const groups = Object.keys(terminology.groups || {});
|
|
244
|
+
if (groups.length === 0) {
|
|
245
|
+
pass('terminology.json: no groups defined (valid for projects without faction/group system)');
|
|
246
|
+
passes++;
|
|
247
|
+
} else {
|
|
248
|
+
pass(`terminology.json: ${groups.length} groups (${groups.join(', ')})`);
|
|
249
|
+
passes++;
|
|
250
|
+
|
|
251
|
+
// Check detection order references valid groups
|
|
252
|
+
for (const orderKey of ['id_detection_order', 'prompt_detection_order']) {
|
|
253
|
+
const order = terminology[orderKey] || [];
|
|
254
|
+
for (const name of order) {
|
|
255
|
+
if (!terminology.groups[name]) {
|
|
256
|
+
fail(`terminology.json: ${orderKey} references unknown group "${name}"`);
|
|
257
|
+
failures++;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Check group patterns compile
|
|
263
|
+
let patternErrors = 0;
|
|
264
|
+
for (const [gName, group] of Object.entries(terminology.groups)) {
|
|
265
|
+
for (const pattern of group.id_patterns || []) {
|
|
266
|
+
try { new RegExp(pattern); }
|
|
267
|
+
catch { fail(`terminology.json: group "${gName}" has invalid id_pattern "${pattern}"`); patternErrors++; }
|
|
268
|
+
}
|
|
269
|
+
for (const pattern of group.prompt_patterns || []) {
|
|
270
|
+
try { new RegExp(pattern); }
|
|
271
|
+
catch { fail(`terminology.json: group "${gName}" has invalid prompt_pattern "${pattern}"`); patternErrors++; }
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (patternErrors === 0) { pass('terminology.json: all detection patterns compile'); passes++; }
|
|
275
|
+
else { failures += patternErrors; }
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── 9. Records sanity check ──
|
|
280
|
+
const recordsDir = join(projectDir, 'records');
|
|
281
|
+
if (existsSync(recordsDir)) {
|
|
282
|
+
console.log('');
|
|
283
|
+
const recordFiles = readdirSync(recordsDir).filter(f => f.endsWith('.json'));
|
|
284
|
+
if (recordFiles.length > 0) {
|
|
285
|
+
// Spot-check first 3 records
|
|
286
|
+
let recordErrors = 0;
|
|
287
|
+
const sample = recordFiles.slice(0, 3);
|
|
288
|
+
for (const f of sample) {
|
|
289
|
+
const { data, error } = tryParseJson(join(recordsDir, f));
|
|
290
|
+
if (error) { fail(`records/${f}: invalid JSON`); recordErrors++; }
|
|
291
|
+
else if (!data.id) { fail(`records/${f}: missing "id"`); recordErrors++; }
|
|
292
|
+
}
|
|
293
|
+
if (recordErrors === 0) {
|
|
294
|
+
pass(`${recordFiles.length} records (spot-check passed)`);
|
|
295
|
+
passes++;
|
|
296
|
+
} else {
|
|
297
|
+
failures += recordErrors;
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
pass('records/ — empty (new project)');
|
|
301
|
+
passes++;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ── Summary ──
|
|
306
|
+
console.log('');
|
|
307
|
+
const status = failures === 0 ? '\x1b[32mHEALTHY\x1b[0m' : '\x1b[31mFAILED\x1b[0m';
|
|
308
|
+
console.log(`═══ ${status} — ${passes} passed, ${failures} failed, ${warnings} warnings ═══`);
|
|
309
|
+
|
|
310
|
+
if (failures > 0) {
|
|
311
|
+
throw new Error(`Project "${projectName}" has ${failures} issue(s).`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Direct execution guard
|
|
316
|
+
if (process.argv[1] && (process.argv[1].endsWith('doctor.js') || process.argv[1].endsWith('doctor'))) {
|
|
317
|
+
run().catch((err) => {
|
|
318
|
+
console.error(err.message || err);
|
|
319
|
+
process.exit(1);
|
|
320
|
+
});
|
|
321
|
+
}
|