@mcptoolshop/style-dataset-lab 2.3.0 → 3.0.1
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 +178 -0
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +71 -0
- package/README.md +82 -9
- package/SECURITY.md +6 -5
- package/bin/sdlab.js +359 -11
- package/lib/adapters/comfyui-runner.js +170 -0
- package/lib/adapters/comfyui-workflows.js +163 -0
- package/lib/adapters/diffusers-lora.js +7 -2
- package/lib/adapters/generic-image-caption.js +6 -1
- package/lib/args.js +200 -13
- package/lib/batch-compiler.js +122 -0
- package/lib/batch-modes.js +111 -0
- package/lib/batch-runs.js +237 -0
- package/lib/batch-sheet-render.js +209 -0
- package/lib/brief-compiler.js +625 -0
- package/lib/brief-render.js +205 -0
- package/lib/card.js +11 -4
- package/lib/comfyui.js +100 -6
- package/lib/config.js +68 -3
- package/lib/critique-engine.js +413 -0
- package/lib/critique-render.js +171 -0
- package/lib/deprecation.js +1 -1
- package/lib/eval-pack.js +94 -4
- package/lib/eval-runs.js +42 -3
- package/lib/export.js +144 -34
- package/lib/generated-provenance.js +87 -0
- package/lib/implementation-packs.js +15 -1
- package/lib/log.js +41 -4
- package/lib/paths.js +72 -3
- package/lib/refine-briefs.js +336 -0
- package/lib/reingest-selected.js +184 -0
- package/lib/reingest.js +15 -1
- package/lib/run-summary.js +132 -0
- package/lib/runtime-runs.js +218 -0
- package/lib/selections.js +375 -0
- package/lib/snapshot.js +100 -7
- package/lib/split.js +175 -17
- package/lib/training-manifests.js +17 -3
- package/lib/training-packages.js +28 -14
- package/lib/workflow-profiles.js +161 -0
- package/package.json +13 -4
- package/scripts/batch-generate.js +176 -0
- package/scripts/batch-sheet.js +46 -0
- package/scripts/batch-show.js +86 -0
- package/scripts/brief-compile.js +134 -0
- package/scripts/brief-show.js +53 -0
- package/scripts/canon-bind.js +7 -9
- package/scripts/card.js +22 -25
- package/scripts/compare.js +38 -33
- package/scripts/critique-show.js +50 -0
- package/scripts/critique.js +70 -0
- package/scripts/curate.js +91 -46
- package/scripts/doctor.js +172 -6
- package/scripts/eligibility.js +17 -14
- package/scripts/eval-pack.js +13 -11
- package/scripts/eval-run.js +30 -29
- package/scripts/export.js +27 -31
- package/scripts/generate-controlnet.js +31 -17
- package/scripts/generate-identity.js +37 -20
- package/scripts/generate-ipadapter.js +22 -16
- package/scripts/generate.js +68 -13
- package/scripts/implementation-pack.js +19 -14
- package/scripts/init.js +137 -17
- package/scripts/painterly.js +8 -10
- package/scripts/refine.js +119 -0
- package/scripts/reingest-selected.js +78 -0
- package/scripts/reingest.js +37 -18
- package/scripts/run-generate.js +76 -0
- package/scripts/run-list.js +58 -0
- package/scripts/run-show.js +63 -0
- package/scripts/select.js +119 -0
- package/scripts/selection-show.js +88 -0
- package/scripts/snapshot.js +22 -24
- package/scripts/split.js +26 -31
- package/scripts/training-manifest.js +29 -24
- package/scripts/training-package.js +24 -20
- package/scripts/training-profile.js +13 -8
- package/scripts/workflow-list.js +61 -0
- package/scripts/workflow-show.js +97 -0
- package/templates/domains/architecture/inputs/prompts/example-wave.json +32 -0
- package/templates/domains/architecture/terminology.json +10 -8
- package/templates/domains/architecture/workflows/profiles/environment-moodboard.json +62 -0
- package/templates/domains/architecture/workflows/profiles/interior-variant-pack.json +60 -0
- package/templates/domains/character-design/inputs/prompts/example-wave.json +32 -0
- package/templates/domains/character-design/project.json +4 -2
- package/templates/domains/character-design/workflows/profiles/character-portrait-set.json +61 -0
- package/templates/domains/character-design/workflows/profiles/expression-sheet.json +59 -0
- package/templates/domains/creature-design/inputs/prompts/example-wave.json +32 -0
- package/templates/domains/creature-design/project.json +4 -2
- package/templates/domains/creature-design/terminology.json +1 -0
- package/templates/domains/creature-design/workflows/profiles/creature-portrait-set.json +61 -0
- package/templates/domains/creature-design/workflows/profiles/silhouette-exploration.json +58 -0
- package/templates/domains/game-art/inputs/prompts/example-wave.json +37 -0
- package/templates/domains/game-art/project.json +4 -2
- package/templates/domains/game-art/workflows/profiles/character-portrait-set.json +62 -0
- package/templates/domains/game-art/workflows/profiles/environment-moodboard.json +61 -0
- package/templates/domains/vehicle-mech/inputs/prompts/example-wave.json +37 -0
- package/templates/domains/vehicle-mech/project.json +4 -2
- package/templates/domains/vehicle-mech/workflows/profiles/variant-pack.json +60 -0
- package/templates/domains/vehicle-mech/workflows/profiles/vehicle-silhouette-pack.json +59 -0
- package/scripts/bulk-curate-wave2-5.js +0 -138
- package/scripts/bulk-curate-waves11-18.js +0 -413
- package/scripts/curate-wave25.js +0 -209
- package/scripts/migrate-records.js +0 -141
- package/templates/inputs/prompts/example-wave.json +0 -27
package/lib/log.js
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Levels: quiet < normal < verbose
|
|
5
5
|
* Set via --quiet or --verbose flags (parsed by caller).
|
|
6
|
+
*
|
|
7
|
+
* Context prefix:
|
|
8
|
+
* info('generate', 'starting wave 1') → '[generate] starting wave 1'
|
|
9
|
+
* info('starting wave 1') → 'starting wave 1' (no prefix)
|
|
10
|
+
* The first argument is treated as a command/context label when it is a
|
|
11
|
+
* short single-token string with no whitespace; otherwise the call is
|
|
12
|
+
* prefix-less and backwards-compatible.
|
|
6
13
|
*/
|
|
7
14
|
|
|
8
15
|
let level = 'normal'; // quiet | normal | verbose
|
|
@@ -15,22 +22,52 @@ export function getLogLevel() {
|
|
|
15
22
|
return level;
|
|
16
23
|
}
|
|
17
24
|
|
|
25
|
+
// Accept a leading short-token context label as the first arg.
|
|
26
|
+
// Heuristic: string, no whitespace, ≤ 32 chars, and there is at least
|
|
27
|
+
// one additional argument after it. This keeps existing call sites
|
|
28
|
+
// (where args is typically a full message) unchanged.
|
|
29
|
+
function applyContextPrefix(args) {
|
|
30
|
+
if (args.length < 2) return args;
|
|
31
|
+
const first = args[0];
|
|
32
|
+
if (typeof first !== 'string') return args;
|
|
33
|
+
if (first.length > 32) return args;
|
|
34
|
+
if (/\s/.test(first)) return args;
|
|
35
|
+
// Avoid treating a message that happens to start with a format token as
|
|
36
|
+
// a context: require it to look like an identifier (word chars, dots, dashes).
|
|
37
|
+
if (!/^[A-Za-z][\w.:-]*$/.test(first)) return args;
|
|
38
|
+
return [`[${first}]`, ...args.slice(1)];
|
|
39
|
+
}
|
|
40
|
+
|
|
18
41
|
/** Always shown (errors, final results). */
|
|
19
42
|
export function log(...args) {
|
|
20
|
-
console.log(...args);
|
|
43
|
+
console.log(...applyContextPrefix(args));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Artifact / final-output path helper.
|
|
48
|
+
* Always prints regardless of verbosity (survives --quiet) so scripts
|
|
49
|
+
* that produce files have a machine-parseable path on stdout.
|
|
50
|
+
*/
|
|
51
|
+
export function result(...args) {
|
|
52
|
+
console.log(...applyContextPrefix(args));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Always shown (used for final success lines). */
|
|
56
|
+
export function success(...args) {
|
|
57
|
+
console.log(...applyContextPrefix(args));
|
|
21
58
|
}
|
|
22
59
|
|
|
23
60
|
/** Shown at normal and verbose levels (progress, summaries). */
|
|
24
61
|
export function info(...args) {
|
|
25
|
-
if (level !== 'quiet') console.log(...args);
|
|
62
|
+
if (level !== 'quiet') console.log(...applyContextPrefix(args));
|
|
26
63
|
}
|
|
27
64
|
|
|
28
65
|
/** Shown only at verbose level (per-record details, debug context). */
|
|
29
66
|
export function verbose(...args) {
|
|
30
|
-
if (level === 'verbose') console.log(...args);
|
|
67
|
+
if (level === 'verbose') console.log(...applyContextPrefix(args));
|
|
31
68
|
}
|
|
32
69
|
|
|
33
70
|
/** Always shown on stderr. */
|
|
34
71
|
export function warn(...args) {
|
|
35
|
-
console.error('\x1b[33m⚠\x1b[0m', ...args);
|
|
72
|
+
console.error('\x1b[33m⚠\x1b[0m', ...applyContextPrefix(args));
|
|
36
73
|
}
|
package/lib/paths.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { dirname, join } from 'node:path';
|
|
6
|
+
import { dirname, join, resolve, relative, isAbsolute } from 'node:path';
|
|
7
7
|
import { existsSync } from 'node:fs';
|
|
8
|
+
import { inputError } from './errors.js';
|
|
8
9
|
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
11
|
const __dirname = dirname(__filename);
|
|
@@ -18,10 +19,18 @@ export const REPO_ROOT = join(__dirname, '..');
|
|
|
18
19
|
* @returns {string} absolute path to projects/<name>/
|
|
19
20
|
*/
|
|
20
21
|
export function getProjectRoot(name) {
|
|
22
|
+
if (!name || typeof name !== 'string') {
|
|
23
|
+
throw inputError(
|
|
24
|
+
'INPUT_MISSING_PROJECT',
|
|
25
|
+
'Project name is required.',
|
|
26
|
+
'Pass --project <name>.'
|
|
27
|
+
);
|
|
28
|
+
}
|
|
21
29
|
const root = join(REPO_ROOT, 'projects', name);
|
|
22
30
|
if (!existsSync(root)) {
|
|
23
|
-
throw
|
|
24
|
-
|
|
31
|
+
throw inputError(
|
|
32
|
+
'INPUT_UNKNOWN_PROJECT',
|
|
33
|
+
`Project "${name}" not found at ${root}.`,
|
|
25
34
|
`Run: sdlab init ${name} --domain <domain>`
|
|
26
35
|
);
|
|
27
36
|
}
|
|
@@ -36,3 +45,63 @@ export function getProjectRoot(name) {
|
|
|
36
45
|
export function resolveProjectPath(name, ...segments) {
|
|
37
46
|
return join(getProjectRoot(name), ...segments);
|
|
38
47
|
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve a user-supplied path relative to a project root and verify
|
|
51
|
+
* it does not escape the root (no `..` traversal, no absolute-path escapes
|
|
52
|
+
* out of root). Returns the absolute resolved path.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} projectRoot — absolute project root (trusted)
|
|
55
|
+
* @param {string} userPath — user-supplied, possibly malicious path
|
|
56
|
+
* @param {{baseRoot?:string, flagName?:string}} [opts]
|
|
57
|
+
* @returns {string} absolute path guaranteed to be within baseRoot
|
|
58
|
+
*/
|
|
59
|
+
export function resolveSafeProjectPath(projectRoot, userPath, opts = {}) {
|
|
60
|
+
const flagName = opts.flagName || 'path';
|
|
61
|
+
if (userPath == null || userPath === '') {
|
|
62
|
+
throw inputError('INPUT_MISSING_VALUE', `Flag --${flagName} requires a path.`);
|
|
63
|
+
}
|
|
64
|
+
const baseRoot = opts.baseRoot || projectRoot;
|
|
65
|
+
const absBase = resolve(baseRoot);
|
|
66
|
+
const resolved = isAbsolute(userPath) ? resolve(userPath) : resolve(absBase, userPath);
|
|
67
|
+
const rel = relative(absBase, resolved);
|
|
68
|
+
if (rel.startsWith('..') || (isAbsolute(rel) && rel !== '')) {
|
|
69
|
+
throw inputError(
|
|
70
|
+
'INPUT_PATH_TRAVERSAL',
|
|
71
|
+
`Path "${userPath}" resolves outside of ${absBase}.`,
|
|
72
|
+
`Paths passed via --${flagName} must stay within the project tree.`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return resolved;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Return the workflow profiles directory for a project root.
|
|
80
|
+
* @param {string} projectRoot — absolute path to project
|
|
81
|
+
*/
|
|
82
|
+
export function getWorkflowProfilesDir(projectRoot) {
|
|
83
|
+
return join(projectRoot, 'workflows', 'profiles');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Return the briefs directory for a project root.
|
|
88
|
+
* @param {string} projectRoot — absolute path to project
|
|
89
|
+
*/
|
|
90
|
+
export function getBriefsDir(projectRoot) {
|
|
91
|
+
return join(projectRoot, 'briefs');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Return the runs directory for a project root.
|
|
96
|
+
* @param {string} projectRoot — absolute path to project
|
|
97
|
+
*/
|
|
98
|
+
export function getRunsDir(projectRoot) {
|
|
99
|
+
return join(projectRoot, 'runs');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Return the runtime templates directory (repo-level).
|
|
104
|
+
*/
|
|
105
|
+
export function getRuntimeDir() {
|
|
106
|
+
return join(REPO_ROOT, 'runtime');
|
|
107
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refine briefs — derive a next-pass brief from critique of a previous run.
|
|
3
|
+
*
|
|
4
|
+
* A refined brief is NOT a brand-new brief. It is a delta layer:
|
|
5
|
+
* - Preserves: original workflow, subject, lane, canon constraints, runtime plan
|
|
6
|
+
* - Adds: preserve/push/suppress instructions derived from critique
|
|
7
|
+
* - Updates: prompt and negative prompt with targeted refinements
|
|
8
|
+
*
|
|
9
|
+
* This keeps the loop stable — each pass improves, never scrambles.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { writeFile, mkdir } from 'node:fs/promises';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
15
|
+
import { getRunsDir } from './paths.js';
|
|
16
|
+
import { inputError } from './errors.js';
|
|
17
|
+
|
|
18
|
+
// ─── Refined brief ID generation ─────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate refined brief ID: <parent_brief_id>_refine_NN
|
|
22
|
+
*/
|
|
23
|
+
function generateRefinedBriefId(refineDir, parentBriefId) {
|
|
24
|
+
const prefix = `${parentBriefId}_refine_`;
|
|
25
|
+
|
|
26
|
+
let maxSeq = 0;
|
|
27
|
+
if (existsSync(refineDir)) {
|
|
28
|
+
const existing = readdirSync(refineDir)
|
|
29
|
+
.filter(f => f.startsWith(prefix) && f.endsWith('.json'));
|
|
30
|
+
for (const f of existing) {
|
|
31
|
+
const seqStr = f.slice(prefix.length, -5);
|
|
32
|
+
const seq = parseInt(seqStr, 10);
|
|
33
|
+
if (!isNaN(seq) && seq > maxSeq) maxSeq = seq;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const seq = String(maxSeq + 1).padStart(2, '0');
|
|
38
|
+
return `${prefix}${seq}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── Prompt refinement ───────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Render a refined prompt from parent prompt + delta instructions.
|
|
45
|
+
*
|
|
46
|
+
* Strategy:
|
|
47
|
+
* - Start with the original prompt
|
|
48
|
+
* - Append preserve notes as reinforcement
|
|
49
|
+
* - Append push notes as emphasis
|
|
50
|
+
* - Do NOT remove anything from the original — only add emphasis
|
|
51
|
+
*/
|
|
52
|
+
export function renderRefinedPrompt(parentPrompt, delta) {
|
|
53
|
+
const parts = [parentPrompt];
|
|
54
|
+
|
|
55
|
+
// Preserve instructions — reinforce what worked
|
|
56
|
+
if (delta.preserve?.length > 0) {
|
|
57
|
+
parts.push(`[preserve: ${delta.preserve.join(', ')}]`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Push instructions — emphasize what needs to be stronger
|
|
61
|
+
if (delta.push?.length > 0) {
|
|
62
|
+
parts.push(`[emphasize: ${delta.push.join(', ')}]`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return parts.join(', ');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Render a refined negative prompt from parent negative + suppress instructions.
|
|
70
|
+
*
|
|
71
|
+
* Strategy:
|
|
72
|
+
* - Keep the entire original negative prompt
|
|
73
|
+
* - Append suppress items that aren't already present
|
|
74
|
+
*/
|
|
75
|
+
export function renderRefinedNegative(parentNegative, delta) {
|
|
76
|
+
// PB-007: coerce missing/null/non-string negatives to '' so we don't
|
|
77
|
+
// throw on legacy briefs or hand-edited JSON that dropped the field.
|
|
78
|
+
const parent = typeof parentNegative === 'string' ? parentNegative : '';
|
|
79
|
+
const existing = parent
|
|
80
|
+
.split(',')
|
|
81
|
+
.map(s => s.trim().toLowerCase())
|
|
82
|
+
.filter(s => s.length > 0);
|
|
83
|
+
const additions = [];
|
|
84
|
+
|
|
85
|
+
for (const item of delta.suppress || []) {
|
|
86
|
+
const lower = item.toLowerCase().trim();
|
|
87
|
+
// Don't duplicate if already in negative (token-exact match)
|
|
88
|
+
if (lower.length > 0 && !existing.includes(lower)) {
|
|
89
|
+
additions.push(item);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (additions.length === 0) return parent;
|
|
94
|
+
if (parent.length === 0) return additions.join(', ');
|
|
95
|
+
return `${parent}, ${additions.join(', ')}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── Delta extraction from critique ──────────────────────────────
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Extract a refine delta from a critique report + picked candidate.
|
|
102
|
+
*
|
|
103
|
+
* @param {Object} critique — critique report
|
|
104
|
+
* @param {string} pickedFilename — filename of the picked candidate
|
|
105
|
+
* @param {Object} cliOverrides — { preserve: string[], push: string[], suppress: string[] }
|
|
106
|
+
* @returns {{ preserve: string[], push: string[], suppress: string[] }}
|
|
107
|
+
*/
|
|
108
|
+
export function extractDelta(critique, pickedFilename, cliOverrides = {}) {
|
|
109
|
+
// Find the picked candidate in the critique
|
|
110
|
+
const picked = critique.candidates.find(c => c.filename === pickedFilename);
|
|
111
|
+
if (!picked) {
|
|
112
|
+
throw inputError(
|
|
113
|
+
'REFINE_CANDIDATE_NOT_FOUND',
|
|
114
|
+
`Candidate "${pickedFilename}" not found in critique for run "${critique.run_id}"`,
|
|
115
|
+
`Available candidates: ${critique.candidates.map(c => c.filename).join(', ')}`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Start with critique-derived instructions
|
|
120
|
+
const preserve = [...(picked.preserve_next_pass || [])];
|
|
121
|
+
const push = [];
|
|
122
|
+
const suppress = [];
|
|
123
|
+
|
|
124
|
+
// Derive push from drift issues that need correction
|
|
125
|
+
for (const issue of picked.correct_next_pass || []) {
|
|
126
|
+
push.push(issue);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Derive suppress from drift issues
|
|
130
|
+
for (const drift of picked.drift_issues || []) {
|
|
131
|
+
// Strip "check:" and "negative check:" prefixes for cleaner suppress
|
|
132
|
+
const cleaned = drift
|
|
133
|
+
.replace(/^\[.*?\]\s*check:\s*/i, '')
|
|
134
|
+
.replace(/^negative check:\s*/i, '')
|
|
135
|
+
.trim();
|
|
136
|
+
if (cleaned) suppress.push(cleaned);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// CLI overrides — append without duplicating
|
|
140
|
+
if (cliOverrides.preserve) {
|
|
141
|
+
for (const p of cliOverrides.preserve) {
|
|
142
|
+
if (!preserve.includes(p)) preserve.push(p);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (cliOverrides.push) {
|
|
146
|
+
for (const p of cliOverrides.push) {
|
|
147
|
+
if (!push.includes(p)) push.push(p);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (cliOverrides.suppress) {
|
|
151
|
+
for (const s of cliOverrides.suppress) {
|
|
152
|
+
if (!suppress.includes(s)) suppress.push(s);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { preserve, push, suppress };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ─── Build refined brief ─────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Build a refined brief from parent brief, critique, and delta.
|
|
163
|
+
*
|
|
164
|
+
* @param {Object} opts
|
|
165
|
+
* @param {Object} opts.parentBrief — original compiled brief
|
|
166
|
+
* @param {Object} opts.critique — critique report
|
|
167
|
+
* @param {string} opts.pickedCandidate — filename of selected candidate
|
|
168
|
+
* @param {Object} opts.delta — { preserve, push, suppress }
|
|
169
|
+
* @param {string} opts.runId — source run ID
|
|
170
|
+
* @param {string} opts.refineDir — path to write refined brief
|
|
171
|
+
* @returns {Object} refined brief
|
|
172
|
+
*/
|
|
173
|
+
export function buildRefinedBrief({ parentBrief, critique, pickedCandidate, delta, runId, refineDir }) {
|
|
174
|
+
const briefId = generateRefinedBriefId(refineDir, parentBrief.brief_id);
|
|
175
|
+
|
|
176
|
+
// Render updated prompts — PB-007 guards missing fields upstream.
|
|
177
|
+
const prompt = renderRefinedPrompt(parentBrief.prompt ?? '', delta);
|
|
178
|
+
const negativePrompt = renderRefinedNegative(parentBrief.negative_prompt ?? '', delta);
|
|
179
|
+
|
|
180
|
+
const refined = {
|
|
181
|
+
brief_id: briefId,
|
|
182
|
+
parent_brief_id: parentBrief.brief_id,
|
|
183
|
+
source_run_id: runId,
|
|
184
|
+
source_candidate: pickedCandidate,
|
|
185
|
+
|
|
186
|
+
project_id: parentBrief.project_id,
|
|
187
|
+
workflow_id: parentBrief.workflow_id,
|
|
188
|
+
lane_id: parentBrief.lane_id,
|
|
189
|
+
compiled_at: new Date().toISOString(),
|
|
190
|
+
config_fingerprint: parentBrief.config_fingerprint,
|
|
191
|
+
|
|
192
|
+
prompt,
|
|
193
|
+
negative_prompt: negativePrompt,
|
|
194
|
+
|
|
195
|
+
refine_delta: delta,
|
|
196
|
+
|
|
197
|
+
// Carry forward from parent
|
|
198
|
+
canon_constraints: parentBrief.canon_constraints || [],
|
|
199
|
+
drift_warnings: parentBrief.drift_warnings || [],
|
|
200
|
+
runtime_plan: { ...parentBrief.runtime_plan },
|
|
201
|
+
expected_outputs: { ...parentBrief.expected_outputs },
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Optional fields
|
|
205
|
+
if (parentBrief.subject_id) refined.subject_id = parentBrief.subject_id;
|
|
206
|
+
if (parentBrief.subject_constraints) refined.subject_constraints = parentBrief.subject_constraints;
|
|
207
|
+
if (parentBrief.reference_selection) refined.reference_selection = parentBrief.reference_selection;
|
|
208
|
+
|
|
209
|
+
return refined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ─── Markdown renderer ──────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Render a refined brief as Markdown.
|
|
216
|
+
*/
|
|
217
|
+
export function renderRefinedBriefMarkdown(brief) {
|
|
218
|
+
const lines = [];
|
|
219
|
+
|
|
220
|
+
lines.push(`# Refined Brief: ${brief.brief_id}`);
|
|
221
|
+
lines.push('');
|
|
222
|
+
lines.push(`**Parent Brief:** ${brief.parent_brief_id}`);
|
|
223
|
+
lines.push(`**Source Run:** ${brief.source_run_id}`);
|
|
224
|
+
lines.push(`**Picked Candidate:** ${brief.source_candidate}`);
|
|
225
|
+
lines.push('');
|
|
226
|
+
lines.push(`**Workflow:** ${brief.workflow_id}`);
|
|
227
|
+
lines.push(`**Project:** ${brief.project_id}`);
|
|
228
|
+
lines.push(`**Lane:** ${brief.lane_id}`);
|
|
229
|
+
if (brief.subject_id) lines.push(`**Subject:** ${brief.subject_id}`);
|
|
230
|
+
lines.push('');
|
|
231
|
+
|
|
232
|
+
// Refine delta — the key section
|
|
233
|
+
lines.push('## Refine Delta');
|
|
234
|
+
lines.push('');
|
|
235
|
+
|
|
236
|
+
if (brief.refine_delta?.preserve?.length > 0) {
|
|
237
|
+
lines.push('### Preserve');
|
|
238
|
+
for (const p of brief.refine_delta.preserve) lines.push(`- ✓ ${p}`);
|
|
239
|
+
lines.push('');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (brief.refine_delta?.push?.length > 0) {
|
|
243
|
+
lines.push('### Push');
|
|
244
|
+
for (const p of brief.refine_delta.push) lines.push(`- → ${p}`);
|
|
245
|
+
lines.push('');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (brief.refine_delta?.suppress?.length > 0) {
|
|
249
|
+
lines.push('### Suppress');
|
|
250
|
+
for (const s of brief.refine_delta.suppress) lines.push(`- ✗ ${s}`);
|
|
251
|
+
lines.push('');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Prompt
|
|
255
|
+
lines.push('## Prompt');
|
|
256
|
+
lines.push('');
|
|
257
|
+
lines.push('```');
|
|
258
|
+
lines.push(brief.prompt);
|
|
259
|
+
lines.push('```');
|
|
260
|
+
lines.push('');
|
|
261
|
+
|
|
262
|
+
// Negative prompt
|
|
263
|
+
lines.push('## Negative Prompt');
|
|
264
|
+
lines.push('');
|
|
265
|
+
lines.push('```');
|
|
266
|
+
lines.push(brief.negative_prompt);
|
|
267
|
+
lines.push('```');
|
|
268
|
+
lines.push('');
|
|
269
|
+
|
|
270
|
+
// Canon constraints
|
|
271
|
+
if (brief.canon_constraints?.length > 0) {
|
|
272
|
+
lines.push('## Canon Constraints');
|
|
273
|
+
lines.push('');
|
|
274
|
+
for (const c of brief.canon_constraints) lines.push(`- ${c}`);
|
|
275
|
+
lines.push('');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Drift warnings
|
|
279
|
+
if (brief.drift_warnings?.length > 0) {
|
|
280
|
+
lines.push('## Drift Warnings');
|
|
281
|
+
lines.push('');
|
|
282
|
+
for (const w of brief.drift_warnings) lines.push(`- ⚠ ${w}`);
|
|
283
|
+
lines.push('');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Runtime plan
|
|
287
|
+
lines.push('## Runtime Plan');
|
|
288
|
+
lines.push('');
|
|
289
|
+
lines.push('| Parameter | Value |');
|
|
290
|
+
lines.push('|-----------|-------|');
|
|
291
|
+
const rp = brief.runtime_plan || {};
|
|
292
|
+
lines.push(`| Adapter | ${rp.adapter_target || '?'} |`);
|
|
293
|
+
lines.push(`| Width | ${rp.width || '?'} |`);
|
|
294
|
+
lines.push(`| Height | ${rp.height || '?'} |`);
|
|
295
|
+
lines.push(`| Steps | ${rp.steps || '?'} |`);
|
|
296
|
+
lines.push(`| CFG | ${rp.cfg || '?'} |`);
|
|
297
|
+
lines.push(`| Sampler | ${rp.sampler || '?'} |`);
|
|
298
|
+
lines.push(`| Seed Mode | ${rp.seed_mode || '?'} |`);
|
|
299
|
+
lines.push('');
|
|
300
|
+
|
|
301
|
+
// Expected outputs
|
|
302
|
+
lines.push('## Expected Outputs');
|
|
303
|
+
lines.push('');
|
|
304
|
+
const eo = brief.expected_outputs || {};
|
|
305
|
+
lines.push(`- **Mode:** ${eo.output_mode || '?'}`);
|
|
306
|
+
lines.push(`- **Count:** ${eo.output_count || '?'}`);
|
|
307
|
+
lines.push('');
|
|
308
|
+
|
|
309
|
+
// Metadata
|
|
310
|
+
lines.push('---');
|
|
311
|
+
lines.push('');
|
|
312
|
+
lines.push(`Compiled: ${brief.compiled_at}`);
|
|
313
|
+
lines.push(`Config fingerprint: \`${brief.config_fingerprint || '—'}\``);
|
|
314
|
+
lines.push('');
|
|
315
|
+
|
|
316
|
+
return lines.join('\n');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ─── Save ────────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Save a refined brief to the run's refine directory.
|
|
323
|
+
*/
|
|
324
|
+
export async function saveRefinedBrief(projectRoot, runId, refinedBrief) {
|
|
325
|
+
const refineDir = join(getRunsDir(projectRoot), runId, 'refine');
|
|
326
|
+
await mkdir(refineDir, { recursive: true });
|
|
327
|
+
|
|
328
|
+
const jsonPath = join(refineDir, `${refinedBrief.brief_id}.json`);
|
|
329
|
+
await writeFile(jsonPath, JSON.stringify(refinedBrief, null, 2) + '\n');
|
|
330
|
+
|
|
331
|
+
const md = renderRefinedBriefMarkdown(refinedBrief);
|
|
332
|
+
const mdPath = join(refineDir, `${refinedBrief.brief_id}.md`);
|
|
333
|
+
await writeFile(mdPath, md);
|
|
334
|
+
|
|
335
|
+
return { jsonPath, mdPath, refineDir };
|
|
336
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-ingest selected outputs as new candidate records.
|
|
3
|
+
*
|
|
4
|
+
* Selected outputs re-enter the project as generated candidates
|
|
5
|
+
* with full provenance. They go through the same review and
|
|
6
|
+
* canon-binding flow as everything else. No bypass.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { writeFile, mkdir, copyFile } from 'node:fs/promises';
|
|
10
|
+
import { join, extname } from 'node:path';
|
|
11
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
12
|
+
import { getRunsDir } from './paths.js';
|
|
13
|
+
import { getBatchesDir } from './batch-runs.js';
|
|
14
|
+
import { getSelectionsDir, loadSelection } from './selections.js';
|
|
15
|
+
import { buildGeneratedProvenance } from './generated-provenance.js';
|
|
16
|
+
import { buildBaseRecord } from './records.js';
|
|
17
|
+
import { inputError } from './errors.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Re-ingest a selection — creates candidate records from chosen outputs.
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} opts
|
|
23
|
+
* @param {string} opts.projectRoot
|
|
24
|
+
* @param {string} opts.projectId
|
|
25
|
+
* @param {string} opts.selectionId
|
|
26
|
+
* @param {boolean} [opts.dryRun]
|
|
27
|
+
* @returns {Promise<{created: string[], skipped: string[]}>}
|
|
28
|
+
*/
|
|
29
|
+
export async function reingestSelection({ projectRoot, projectId, selectionId, dryRun }) {
|
|
30
|
+
const manifest = loadSelection(projectRoot, selectionId);
|
|
31
|
+
|
|
32
|
+
if (!manifest.reingest_ready) {
|
|
33
|
+
throw inputError(
|
|
34
|
+
'SELECTION_NOT_READY',
|
|
35
|
+
`Selection "${selectionId}" is not marked reingest_ready`,
|
|
36
|
+
'Set reingest_ready: true in the selection manifest.',
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const selectionDir = join(getSelectionsDir(projectRoot), selectionId);
|
|
41
|
+
const recordsDir = join(projectRoot, 'records');
|
|
42
|
+
const inboxDir = join(projectRoot, 'inbox', 'generated');
|
|
43
|
+
|
|
44
|
+
if (!dryRun) {
|
|
45
|
+
await mkdir(recordsDir, { recursive: true });
|
|
46
|
+
await mkdir(inboxDir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Load source manifests for provenance
|
|
50
|
+
const runsDir = getRunsDir(projectRoot);
|
|
51
|
+
const runManifestCache = new Map();
|
|
52
|
+
|
|
53
|
+
function getRunManifest(runId) {
|
|
54
|
+
if (runManifestCache.has(runId)) return runManifestCache.get(runId);
|
|
55
|
+
const mPath = join(runsDir, runId, 'manifest.json');
|
|
56
|
+
if (!existsSync(mPath)) return null;
|
|
57
|
+
const m = JSON.parse(readFileSync(mPath, 'utf-8'));
|
|
58
|
+
runManifestCache.set(runId, m);
|
|
59
|
+
return m;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let batchManifest = null;
|
|
63
|
+
if (manifest.source_type === 'batch') {
|
|
64
|
+
const bmPath = join(getBatchesDir(projectRoot), manifest.source_id, 'manifest.json');
|
|
65
|
+
if (existsSync(bmPath)) {
|
|
66
|
+
batchManifest = JSON.parse(readFileSync(bmPath, 'utf-8'));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const created = [];
|
|
71
|
+
const skipped = [];
|
|
72
|
+
const provenanceLines = [];
|
|
73
|
+
|
|
74
|
+
// D-010: use entries() — not indexOf() — to avoid O(n^2) behavior and
|
|
75
|
+
// silent duplicate-ID bugs when manifest.items contains structurally
|
|
76
|
+
// equal or repeated references. Assert uniqueness of generated IDs.
|
|
77
|
+
const seenIds = new Set();
|
|
78
|
+
for (const [i, item] of manifest.items.entries()) {
|
|
79
|
+
const idx = String(i + 1).padStart(3, '0');
|
|
80
|
+
const recordId = `gen_${selectionId}_${idx}`;
|
|
81
|
+
if (seenIds.has(recordId)) {
|
|
82
|
+
throw inputError(
|
|
83
|
+
'REINGEST_DUPLICATE_RECORD_ID',
|
|
84
|
+
`Duplicate record ID generated during re-ingest: ${recordId}`,
|
|
85
|
+
'This indicates a bug in record ID derivation; report this along with the selection manifest.',
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
seenIds.add(recordId);
|
|
89
|
+
const recordPath = join(recordsDir, `${recordId}.json`);
|
|
90
|
+
|
|
91
|
+
if (existsSync(recordPath)) {
|
|
92
|
+
skipped.push(recordId);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Resolve the run manifest for this item
|
|
97
|
+
let runManifest;
|
|
98
|
+
if (manifest.source_type === 'run') {
|
|
99
|
+
runManifest = getRunManifest(manifest.source_id);
|
|
100
|
+
} else if (manifest.source_type === 'batch' && batchManifest) {
|
|
101
|
+
// Find which run this slot belongs to
|
|
102
|
+
const slotId = item.slot_or_output.split(':')[0];
|
|
103
|
+
const slot = batchManifest.slots?.find(s => s.slot_id === slotId);
|
|
104
|
+
if (slot) {
|
|
105
|
+
runManifest = getRunManifest(slot.run_id);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!runManifest) {
|
|
110
|
+
// Fallback: minimal provenance
|
|
111
|
+
runManifest = {
|
|
112
|
+
run_id: manifest.source_id,
|
|
113
|
+
brief_id: 'unknown',
|
|
114
|
+
workflow_template_id: manifest.workflow_id || 'unknown',
|
|
115
|
+
created_at: manifest.created_at,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Build provenance
|
|
120
|
+
const prov = buildGeneratedProvenance({
|
|
121
|
+
sourceType: manifest.source_type,
|
|
122
|
+
sourceId: manifest.source_id,
|
|
123
|
+
runManifest,
|
|
124
|
+
batchManifest,
|
|
125
|
+
item,
|
|
126
|
+
projectRoot,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Image source in selection/chosen/
|
|
130
|
+
const srcImage = join(selectionDir, 'chosen', item.filename);
|
|
131
|
+
const ext = extname(item.filename) || '.png';
|
|
132
|
+
const destImage = `${recordId}${ext}`;
|
|
133
|
+
const inboxPath = join(inboxDir, destImage);
|
|
134
|
+
const assetPath = `inbox/generated/${destImage}`;
|
|
135
|
+
|
|
136
|
+
// Get file size
|
|
137
|
+
let fileBytes = 0;
|
|
138
|
+
if (existsSync(srcImage)) {
|
|
139
|
+
fileBytes = statSync(srcImage).size;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Build record
|
|
143
|
+
const record = buildBaseRecord(recordId, assetPath, {
|
|
144
|
+
width: null,
|
|
145
|
+
height: null,
|
|
146
|
+
bytes: fileBytes,
|
|
147
|
+
}, {
|
|
148
|
+
source: 'generated',
|
|
149
|
+
selection_id: selectionId,
|
|
150
|
+
generation_provenance: prov,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
record.schema_version = '2.2.0';
|
|
154
|
+
|
|
155
|
+
// Tags
|
|
156
|
+
const recordTags = ['generated', 'selected'];
|
|
157
|
+
if (manifest.workflow_id) recordTags.push(manifest.workflow_id);
|
|
158
|
+
if (manifest.subject_id) recordTags.push(manifest.subject_id);
|
|
159
|
+
if (item.tags?.length) recordTags.push(...item.tags);
|
|
160
|
+
record.tags = [...new Set(recordTags)];
|
|
161
|
+
|
|
162
|
+
if (!dryRun) {
|
|
163
|
+
// Copy image into inbox
|
|
164
|
+
if (existsSync(srcImage)) {
|
|
165
|
+
await copyFile(srcImage, inboxPath);
|
|
166
|
+
}
|
|
167
|
+
// Write record
|
|
168
|
+
await writeFile(recordPath, JSON.stringify(record, null, 2) + '\n');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
created.push(recordId);
|
|
172
|
+
provenanceLines.push(JSON.stringify({ record_id: recordId, ...prov }));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Write provenance log
|
|
176
|
+
if (!dryRun && provenanceLines.length > 0) {
|
|
177
|
+
await writeFile(
|
|
178
|
+
join(selectionDir, 'provenance.jsonl'),
|
|
179
|
+
provenanceLines.join('\n') + '\n',
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { created, skipped };
|
|
184
|
+
}
|