@mcptoolshop/style-dataset-lab 2.3.0 → 3.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 +125 -0
- package/README.md +36 -8
- package/bin/sdlab.js +129 -0
- package/lib/adapters/comfyui-runner.js +170 -0
- package/lib/adapters/comfyui-workflows.js +163 -0
- package/lib/batch-compiler.js +118 -0
- package/lib/batch-modes.js +111 -0
- package/lib/batch-runs.js +168 -0
- package/lib/batch-sheet-render.js +206 -0
- package/lib/brief-compiler.js +598 -0
- package/lib/brief-render.js +205 -0
- package/lib/critique-engine.js +413 -0
- package/lib/critique-render.js +171 -0
- package/lib/generated-provenance.js +87 -0
- package/lib/paths.js +31 -0
- package/lib/refine-briefs.js +329 -0
- package/lib/reingest-selected.js +173 -0
- package/lib/run-summary.js +131 -0
- package/lib/runtime-runs.js +168 -0
- package/lib/selections.js +323 -0
- package/lib/workflow-profiles.js +161 -0
- package/package.json +6 -3
- package/scripts/batch-generate.js +167 -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 +52 -0
- package/scripts/critique-show.js +50 -0
- package/scripts/critique.js +70 -0
- package/scripts/doctor.js +169 -1
- package/scripts/init.js +22 -0
- package/scripts/refine.js +119 -0
- package/scripts/reingest-selected.js +78 -0
- package/scripts/reingest.js +7 -1
- 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 +118 -0
- package/scripts/selection-show.js +88 -0
- package/scripts/workflow-list.js +61 -0
- package/scripts/workflow-show.js +96 -0
- 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/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/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/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/workflows/profiles/variant-pack.json +60 -0
- package/templates/domains/vehicle-mech/workflows/profiles/vehicle-silhouette-pack.json +59 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Critique renderer — converts critique report to human-readable formats.
|
|
3
|
+
*
|
|
4
|
+
* Reads like a working note, not a report card.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ─── Fit badge ───────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
const FIT_BADGES = {
|
|
10
|
+
strong: '✓ strong',
|
|
11
|
+
usable: '○ usable',
|
|
12
|
+
'off-model': '✗ off-model',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const FIT_EMOJI = {
|
|
16
|
+
strong: '🟢',
|
|
17
|
+
usable: '🟡',
|
|
18
|
+
'off-model': '🔴',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// ─── Markdown renderer ──────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Render a critique report as Markdown.
|
|
25
|
+
*/
|
|
26
|
+
export function renderCritiqueMarkdown(report) {
|
|
27
|
+
const lines = [];
|
|
28
|
+
|
|
29
|
+
lines.push(`# Critique: ${report.run_id}`);
|
|
30
|
+
lines.push('');
|
|
31
|
+
lines.push(`**Brief:** ${report.brief_id}`);
|
|
32
|
+
lines.push(`**Workflow:** ${report.workflow_id}`);
|
|
33
|
+
lines.push(`**Project:** ${report.project_id}`);
|
|
34
|
+
if (report.subject_id) lines.push(`**Subject:** ${report.subject_id}`);
|
|
35
|
+
lines.push(`**Created:** ${report.created_at}`);
|
|
36
|
+
lines.push('');
|
|
37
|
+
|
|
38
|
+
// Recommended action
|
|
39
|
+
const action = report.recommended_action;
|
|
40
|
+
const modeLabel = {
|
|
41
|
+
accept_one: '✓ Accept one candidate',
|
|
42
|
+
refine_from_one: '→ Refine from one candidate',
|
|
43
|
+
rerun_broader: '↻ Rerun with broader parameters',
|
|
44
|
+
discard_run: '✗ Discard this run',
|
|
45
|
+
};
|
|
46
|
+
lines.push('## Recommended Action');
|
|
47
|
+
lines.push('');
|
|
48
|
+
lines.push(`**${modeLabel[action.mode] || action.mode}**`);
|
|
49
|
+
if (action.preferred_candidate) {
|
|
50
|
+
lines.push(`Preferred: **${action.preferred_candidate}**`);
|
|
51
|
+
}
|
|
52
|
+
if (action.reason) lines.push(`Reason: ${action.reason}`);
|
|
53
|
+
lines.push('');
|
|
54
|
+
|
|
55
|
+
// Active dimensions
|
|
56
|
+
lines.push('## Active Dimensions');
|
|
57
|
+
lines.push('');
|
|
58
|
+
for (const dim of report.active_dimensions) {
|
|
59
|
+
lines.push(`- ${dim.replace(/_/g, ' ')}`);
|
|
60
|
+
}
|
|
61
|
+
lines.push('');
|
|
62
|
+
|
|
63
|
+
// Candidates
|
|
64
|
+
lines.push('## Candidates');
|
|
65
|
+
lines.push('');
|
|
66
|
+
|
|
67
|
+
for (const c of report.candidates) {
|
|
68
|
+
const badge = FIT_EMOJI[c.overall_fit] || '?';
|
|
69
|
+
lines.push(`### ${badge} ${c.filename} (seed: ${c.seed ?? '?'})`);
|
|
70
|
+
lines.push('');
|
|
71
|
+
lines.push(`**Fit:** ${c.overall_fit}`);
|
|
72
|
+
lines.push('');
|
|
73
|
+
|
|
74
|
+
if (c.strengths.length > 0) {
|
|
75
|
+
lines.push('**Strengths:**');
|
|
76
|
+
for (const s of c.strengths) lines.push(`- ${s}`);
|
|
77
|
+
lines.push('');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (c.drift_issues.length > 0) {
|
|
81
|
+
lines.push('**Drift Issues:**');
|
|
82
|
+
for (const d of c.drift_issues) lines.push(`- ⚠ ${d}`);
|
|
83
|
+
lines.push('');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (c.preserve_next_pass.length > 0) {
|
|
87
|
+
lines.push('**Preserve Next Pass:**');
|
|
88
|
+
for (const p of c.preserve_next_pass) lines.push(`- ✓ ${p}`);
|
|
89
|
+
lines.push('');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (c.correct_next_pass.length > 0) {
|
|
93
|
+
lines.push('**Correct Next Pass:**');
|
|
94
|
+
for (const x of c.correct_next_pass) lines.push(`- → ${x}`);
|
|
95
|
+
lines.push('');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Next-pass direction summary
|
|
100
|
+
lines.push('## Next-Pass Direction');
|
|
101
|
+
lines.push('');
|
|
102
|
+
|
|
103
|
+
// Aggregate preserve/correct across all candidates
|
|
104
|
+
const allPreserve = new Set();
|
|
105
|
+
const allCorrect = new Set();
|
|
106
|
+
for (const c of report.candidates) {
|
|
107
|
+
for (const p of c.preserve_next_pass) allPreserve.add(p);
|
|
108
|
+
for (const x of c.correct_next_pass) allCorrect.add(x);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (allPreserve.size > 0) {
|
|
112
|
+
lines.push('**Preserve:**');
|
|
113
|
+
for (const p of allPreserve) lines.push(`- ${p}`);
|
|
114
|
+
lines.push('');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (allCorrect.size > 0) {
|
|
118
|
+
lines.push('**Correct:**');
|
|
119
|
+
for (const x of allCorrect) lines.push(`- ${x}`);
|
|
120
|
+
lines.push('');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return lines.join('\n');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── Terminal text renderer ──────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Render a critique report as plain text for terminal display.
|
|
130
|
+
*/
|
|
131
|
+
export function renderCritiqueText(report) {
|
|
132
|
+
const lines = [];
|
|
133
|
+
|
|
134
|
+
lines.push(`Critique: ${report.run_id}`);
|
|
135
|
+
lines.push(`Brief: ${report.brief_id} Workflow: ${report.workflow_id}`);
|
|
136
|
+
if (report.subject_id) lines.push(`Subject: ${report.subject_id}`);
|
|
137
|
+
lines.push('');
|
|
138
|
+
|
|
139
|
+
// Action
|
|
140
|
+
const action = report.recommended_action;
|
|
141
|
+
const actionIcon = {
|
|
142
|
+
accept_one: '✓',
|
|
143
|
+
refine_from_one: '→',
|
|
144
|
+
rerun_broader: '↻',
|
|
145
|
+
discard_run: '✗',
|
|
146
|
+
};
|
|
147
|
+
lines.push(`${actionIcon[action.mode] || '?'} ${action.mode.replace(/_/g, ' ')}`);
|
|
148
|
+
if (action.preferred_candidate) lines.push(` Preferred: ${action.preferred_candidate}`);
|
|
149
|
+
if (action.reason) lines.push(` ${action.reason}`);
|
|
150
|
+
lines.push('');
|
|
151
|
+
|
|
152
|
+
// Dimensions
|
|
153
|
+
lines.push(`Dimensions: ${report.active_dimensions.map(d => d.replace(/_/g, ' ')).join(', ')}`);
|
|
154
|
+
lines.push('');
|
|
155
|
+
|
|
156
|
+
// Candidates
|
|
157
|
+
for (const c of report.candidates) {
|
|
158
|
+
const badge = FIT_BADGES[c.overall_fit] || '? unknown';
|
|
159
|
+
lines.push(` ${badge} ${c.filename} (seed: ${c.seed ?? '?'})`);
|
|
160
|
+
|
|
161
|
+
if (c.strengths.length > 0) {
|
|
162
|
+
lines.push(` strengths: ${c.strengths.slice(0, 3).join('; ')}`);
|
|
163
|
+
}
|
|
164
|
+
if (c.drift_issues.length > 0) {
|
|
165
|
+
lines.push(` drift: ${c.drift_issues.slice(0, 3).join('; ')}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
lines.push('');
|
|
169
|
+
|
|
170
|
+
return lines.join('\n');
|
|
171
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated provenance builder — constructs the provenance block
|
|
3
|
+
* attached to records created from selected run/batch outputs.
|
|
4
|
+
*
|
|
5
|
+
* Normalizes source context (run vs batch) into a single shape
|
|
6
|
+
* that traces back through brief → workflow → seed → config.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build a generation provenance payload for a selected output.
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} opts
|
|
17
|
+
* @param {'run'|'batch'} opts.sourceType
|
|
18
|
+
* @param {string} opts.sourceId — run_id or batch_id
|
|
19
|
+
* @param {Object} opts.runManifest — the run manifest containing this output
|
|
20
|
+
* @param {Object} [opts.batchManifest] — batch manifest if source is batch
|
|
21
|
+
* @param {Object} opts.item — the selection item (slot_or_output, filename, seed)
|
|
22
|
+
* @param {string} opts.projectRoot
|
|
23
|
+
* @returns {Object} generation_provenance block
|
|
24
|
+
*/
|
|
25
|
+
export function buildGeneratedProvenance({
|
|
26
|
+
sourceType,
|
|
27
|
+
sourceId,
|
|
28
|
+
runManifest,
|
|
29
|
+
batchManifest,
|
|
30
|
+
item,
|
|
31
|
+
projectRoot,
|
|
32
|
+
}) {
|
|
33
|
+
const prov = {
|
|
34
|
+
source_type: sourceType,
|
|
35
|
+
source_id: sourceId,
|
|
36
|
+
run_id: runManifest.run_id,
|
|
37
|
+
brief_id: runManifest.brief_id,
|
|
38
|
+
workflow_id: runManifest.workflow_template_id,
|
|
39
|
+
selected_filename: item.filename,
|
|
40
|
+
generated_at: runManifest.created_at,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if (sourceType === 'batch' && batchManifest) {
|
|
44
|
+
prov.batch_id = batchManifest.batch_id;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Seed from the item or from the run manifest outputs
|
|
48
|
+
if (item.seed !== undefined) {
|
|
49
|
+
prov.seed = item.seed;
|
|
50
|
+
} else if (runManifest.outputs) {
|
|
51
|
+
const match = runManifest.outputs.find(o => o.filename === item.filename);
|
|
52
|
+
if (match?.seed !== undefined) prov.seed = match.seed;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Adapter target from run manifest
|
|
56
|
+
if (runManifest.adapter_target) {
|
|
57
|
+
prov.adapter_target = runManifest.adapter_target;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Subject from run brief or batch manifest
|
|
61
|
+
if (batchManifest?.subject_id) {
|
|
62
|
+
prov.subject_id = batchManifest.subject_id;
|
|
63
|
+
} else {
|
|
64
|
+
// Try to read subject from brief
|
|
65
|
+
const briefPath = join(projectRoot, 'briefs', `${runManifest.brief_id}.json`);
|
|
66
|
+
if (existsSync(briefPath)) {
|
|
67
|
+
try {
|
|
68
|
+
const brief = JSON.parse(readFileSync(briefPath, 'utf-8'));
|
|
69
|
+
if (brief.subject_id) prov.subject_id = brief.subject_id;
|
|
70
|
+
if (brief.parent_brief_id) prov.parent_brief_id = brief.parent_brief_id;
|
|
71
|
+
} catch { /* skip */ }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Config fingerprint — hash the run manifest for reproducibility tracing
|
|
76
|
+
prov.config_fingerprint = createHash('sha256')
|
|
77
|
+
.update(JSON.stringify({
|
|
78
|
+
brief_id: runManifest.brief_id,
|
|
79
|
+
workflow_template_id: runManifest.workflow_template_id,
|
|
80
|
+
adapter_target: runManifest.adapter_target,
|
|
81
|
+
seed_plan: runManifest.seed_plan,
|
|
82
|
+
}))
|
|
83
|
+
.digest('hex')
|
|
84
|
+
.slice(0, 16);
|
|
85
|
+
|
|
86
|
+
return prov;
|
|
87
|
+
}
|
package/lib/paths.js
CHANGED
|
@@ -36,3 +36,34 @@ export function getProjectRoot(name) {
|
|
|
36
36
|
export function resolveProjectPath(name, ...segments) {
|
|
37
37
|
return join(getProjectRoot(name), ...segments);
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Return the workflow profiles directory for a project root.
|
|
42
|
+
* @param {string} projectRoot — absolute path to project
|
|
43
|
+
*/
|
|
44
|
+
export function getWorkflowProfilesDir(projectRoot) {
|
|
45
|
+
return join(projectRoot, 'workflows', 'profiles');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Return the briefs directory for a project root.
|
|
50
|
+
* @param {string} projectRoot — absolute path to project
|
|
51
|
+
*/
|
|
52
|
+
export function getBriefsDir(projectRoot) {
|
|
53
|
+
return join(projectRoot, 'briefs');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Return the runs directory for a project root.
|
|
58
|
+
* @param {string} projectRoot — absolute path to project
|
|
59
|
+
*/
|
|
60
|
+
export function getRunsDir(projectRoot) {
|
|
61
|
+
return join(projectRoot, 'runs');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Return the runtime templates directory (repo-level).
|
|
66
|
+
*/
|
|
67
|
+
export function getRuntimeDir() {
|
|
68
|
+
return join(REPO_ROOT, 'runtime');
|
|
69
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
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
|
+
const existing = parentNegative.split(',').map(s => s.trim().toLowerCase());
|
|
77
|
+
const additions = [];
|
|
78
|
+
|
|
79
|
+
for (const item of delta.suppress || []) {
|
|
80
|
+
const lower = item.toLowerCase();
|
|
81
|
+
// Don't duplicate if already in negative
|
|
82
|
+
if (!existing.some(e => e.includes(lower) || lower.includes(e))) {
|
|
83
|
+
additions.push(item);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (additions.length === 0) return parentNegative;
|
|
88
|
+
return `${parentNegative}, ${additions.join(', ')}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Delta extraction from critique ──────────────────────────────
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Extract a refine delta from a critique report + picked candidate.
|
|
95
|
+
*
|
|
96
|
+
* @param {Object} critique — critique report
|
|
97
|
+
* @param {string} pickedFilename — filename of the picked candidate
|
|
98
|
+
* @param {Object} cliOverrides — { preserve: string[], push: string[], suppress: string[] }
|
|
99
|
+
* @returns {{ preserve: string[], push: string[], suppress: string[] }}
|
|
100
|
+
*/
|
|
101
|
+
export function extractDelta(critique, pickedFilename, cliOverrides = {}) {
|
|
102
|
+
// Find the picked candidate in the critique
|
|
103
|
+
const picked = critique.candidates.find(c => c.filename === pickedFilename);
|
|
104
|
+
if (!picked) {
|
|
105
|
+
throw inputError(
|
|
106
|
+
'REFINE_CANDIDATE_NOT_FOUND',
|
|
107
|
+
`Candidate "${pickedFilename}" not found in critique for run "${critique.run_id}"`,
|
|
108
|
+
`Available candidates: ${critique.candidates.map(c => c.filename).join(', ')}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Start with critique-derived instructions
|
|
113
|
+
const preserve = [...(picked.preserve_next_pass || [])];
|
|
114
|
+
const push = [];
|
|
115
|
+
const suppress = [];
|
|
116
|
+
|
|
117
|
+
// Derive push from drift issues that need correction
|
|
118
|
+
for (const issue of picked.correct_next_pass || []) {
|
|
119
|
+
push.push(issue);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Derive suppress from drift issues
|
|
123
|
+
for (const drift of picked.drift_issues || []) {
|
|
124
|
+
// Strip "check:" and "negative check:" prefixes for cleaner suppress
|
|
125
|
+
const cleaned = drift
|
|
126
|
+
.replace(/^\[.*?\]\s*check:\s*/i, '')
|
|
127
|
+
.replace(/^negative check:\s*/i, '')
|
|
128
|
+
.trim();
|
|
129
|
+
if (cleaned) suppress.push(cleaned);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// CLI overrides — append without duplicating
|
|
133
|
+
if (cliOverrides.preserve) {
|
|
134
|
+
for (const p of cliOverrides.preserve) {
|
|
135
|
+
if (!preserve.includes(p)) preserve.push(p);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (cliOverrides.push) {
|
|
139
|
+
for (const p of cliOverrides.push) {
|
|
140
|
+
if (!push.includes(p)) push.push(p);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (cliOverrides.suppress) {
|
|
144
|
+
for (const s of cliOverrides.suppress) {
|
|
145
|
+
if (!suppress.includes(s)) suppress.push(s);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { preserve, push, suppress };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ─── Build refined brief ─────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Build a refined brief from parent brief, critique, and delta.
|
|
156
|
+
*
|
|
157
|
+
* @param {Object} opts
|
|
158
|
+
* @param {Object} opts.parentBrief — original compiled brief
|
|
159
|
+
* @param {Object} opts.critique — critique report
|
|
160
|
+
* @param {string} opts.pickedCandidate — filename of selected candidate
|
|
161
|
+
* @param {Object} opts.delta — { preserve, push, suppress }
|
|
162
|
+
* @param {string} opts.runId — source run ID
|
|
163
|
+
* @param {string} opts.refineDir — path to write refined brief
|
|
164
|
+
* @returns {Object} refined brief
|
|
165
|
+
*/
|
|
166
|
+
export function buildRefinedBrief({ parentBrief, critique, pickedCandidate, delta, runId, refineDir }) {
|
|
167
|
+
const briefId = generateRefinedBriefId(refineDir, parentBrief.brief_id);
|
|
168
|
+
|
|
169
|
+
// Render updated prompts
|
|
170
|
+
const prompt = renderRefinedPrompt(parentBrief.prompt, delta);
|
|
171
|
+
const negativePrompt = renderRefinedNegative(parentBrief.negative_prompt, delta);
|
|
172
|
+
|
|
173
|
+
const refined = {
|
|
174
|
+
brief_id: briefId,
|
|
175
|
+
parent_brief_id: parentBrief.brief_id,
|
|
176
|
+
source_run_id: runId,
|
|
177
|
+
source_candidate: pickedCandidate,
|
|
178
|
+
|
|
179
|
+
project_id: parentBrief.project_id,
|
|
180
|
+
workflow_id: parentBrief.workflow_id,
|
|
181
|
+
lane_id: parentBrief.lane_id,
|
|
182
|
+
compiled_at: new Date().toISOString(),
|
|
183
|
+
config_fingerprint: parentBrief.config_fingerprint,
|
|
184
|
+
|
|
185
|
+
prompt,
|
|
186
|
+
negative_prompt: negativePrompt,
|
|
187
|
+
|
|
188
|
+
refine_delta: delta,
|
|
189
|
+
|
|
190
|
+
// Carry forward from parent
|
|
191
|
+
canon_constraints: parentBrief.canon_constraints || [],
|
|
192
|
+
drift_warnings: parentBrief.drift_warnings || [],
|
|
193
|
+
runtime_plan: { ...parentBrief.runtime_plan },
|
|
194
|
+
expected_outputs: { ...parentBrief.expected_outputs },
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Optional fields
|
|
198
|
+
if (parentBrief.subject_id) refined.subject_id = parentBrief.subject_id;
|
|
199
|
+
if (parentBrief.subject_constraints) refined.subject_constraints = parentBrief.subject_constraints;
|
|
200
|
+
if (parentBrief.reference_selection) refined.reference_selection = parentBrief.reference_selection;
|
|
201
|
+
|
|
202
|
+
return refined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ─── Markdown renderer ──────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Render a refined brief as Markdown.
|
|
209
|
+
*/
|
|
210
|
+
export function renderRefinedBriefMarkdown(brief) {
|
|
211
|
+
const lines = [];
|
|
212
|
+
|
|
213
|
+
lines.push(`# Refined Brief: ${brief.brief_id}`);
|
|
214
|
+
lines.push('');
|
|
215
|
+
lines.push(`**Parent Brief:** ${brief.parent_brief_id}`);
|
|
216
|
+
lines.push(`**Source Run:** ${brief.source_run_id}`);
|
|
217
|
+
lines.push(`**Picked Candidate:** ${brief.source_candidate}`);
|
|
218
|
+
lines.push('');
|
|
219
|
+
lines.push(`**Workflow:** ${brief.workflow_id}`);
|
|
220
|
+
lines.push(`**Project:** ${brief.project_id}`);
|
|
221
|
+
lines.push(`**Lane:** ${brief.lane_id}`);
|
|
222
|
+
if (brief.subject_id) lines.push(`**Subject:** ${brief.subject_id}`);
|
|
223
|
+
lines.push('');
|
|
224
|
+
|
|
225
|
+
// Refine delta — the key section
|
|
226
|
+
lines.push('## Refine Delta');
|
|
227
|
+
lines.push('');
|
|
228
|
+
|
|
229
|
+
if (brief.refine_delta?.preserve?.length > 0) {
|
|
230
|
+
lines.push('### Preserve');
|
|
231
|
+
for (const p of brief.refine_delta.preserve) lines.push(`- ✓ ${p}`);
|
|
232
|
+
lines.push('');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (brief.refine_delta?.push?.length > 0) {
|
|
236
|
+
lines.push('### Push');
|
|
237
|
+
for (const p of brief.refine_delta.push) lines.push(`- → ${p}`);
|
|
238
|
+
lines.push('');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (brief.refine_delta?.suppress?.length > 0) {
|
|
242
|
+
lines.push('### Suppress');
|
|
243
|
+
for (const s of brief.refine_delta.suppress) lines.push(`- ✗ ${s}`);
|
|
244
|
+
lines.push('');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Prompt
|
|
248
|
+
lines.push('## Prompt');
|
|
249
|
+
lines.push('');
|
|
250
|
+
lines.push('```');
|
|
251
|
+
lines.push(brief.prompt);
|
|
252
|
+
lines.push('```');
|
|
253
|
+
lines.push('');
|
|
254
|
+
|
|
255
|
+
// Negative prompt
|
|
256
|
+
lines.push('## Negative Prompt');
|
|
257
|
+
lines.push('');
|
|
258
|
+
lines.push('```');
|
|
259
|
+
lines.push(brief.negative_prompt);
|
|
260
|
+
lines.push('```');
|
|
261
|
+
lines.push('');
|
|
262
|
+
|
|
263
|
+
// Canon constraints
|
|
264
|
+
if (brief.canon_constraints?.length > 0) {
|
|
265
|
+
lines.push('## Canon Constraints');
|
|
266
|
+
lines.push('');
|
|
267
|
+
for (const c of brief.canon_constraints) lines.push(`- ${c}`);
|
|
268
|
+
lines.push('');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Drift warnings
|
|
272
|
+
if (brief.drift_warnings?.length > 0) {
|
|
273
|
+
lines.push('## Drift Warnings');
|
|
274
|
+
lines.push('');
|
|
275
|
+
for (const w of brief.drift_warnings) lines.push(`- ⚠ ${w}`);
|
|
276
|
+
lines.push('');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Runtime plan
|
|
280
|
+
lines.push('## Runtime Plan');
|
|
281
|
+
lines.push('');
|
|
282
|
+
lines.push('| Parameter | Value |');
|
|
283
|
+
lines.push('|-----------|-------|');
|
|
284
|
+
const rp = brief.runtime_plan || {};
|
|
285
|
+
lines.push(`| Adapter | ${rp.adapter_target || '?'} |`);
|
|
286
|
+
lines.push(`| Width | ${rp.width || '?'} |`);
|
|
287
|
+
lines.push(`| Height | ${rp.height || '?'} |`);
|
|
288
|
+
lines.push(`| Steps | ${rp.steps || '?'} |`);
|
|
289
|
+
lines.push(`| CFG | ${rp.cfg || '?'} |`);
|
|
290
|
+
lines.push(`| Sampler | ${rp.sampler || '?'} |`);
|
|
291
|
+
lines.push(`| Seed Mode | ${rp.seed_mode || '?'} |`);
|
|
292
|
+
lines.push('');
|
|
293
|
+
|
|
294
|
+
// Expected outputs
|
|
295
|
+
lines.push('## Expected Outputs');
|
|
296
|
+
lines.push('');
|
|
297
|
+
const eo = brief.expected_outputs || {};
|
|
298
|
+
lines.push(`- **Mode:** ${eo.output_mode || '?'}`);
|
|
299
|
+
lines.push(`- **Count:** ${eo.output_count || '?'}`);
|
|
300
|
+
lines.push('');
|
|
301
|
+
|
|
302
|
+
// Metadata
|
|
303
|
+
lines.push('---');
|
|
304
|
+
lines.push('');
|
|
305
|
+
lines.push(`Compiled: ${brief.compiled_at}`);
|
|
306
|
+
lines.push(`Config fingerprint: \`${brief.config_fingerprint || '—'}\``);
|
|
307
|
+
lines.push('');
|
|
308
|
+
|
|
309
|
+
return lines.join('\n');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ─── Save ────────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Save a refined brief to the run's refine directory.
|
|
316
|
+
*/
|
|
317
|
+
export async function saveRefinedBrief(projectRoot, runId, refinedBrief) {
|
|
318
|
+
const refineDir = join(getRunsDir(projectRoot), runId, 'refine');
|
|
319
|
+
await mkdir(refineDir, { recursive: true });
|
|
320
|
+
|
|
321
|
+
const jsonPath = join(refineDir, `${refinedBrief.brief_id}.json`);
|
|
322
|
+
await writeFile(jsonPath, JSON.stringify(refinedBrief, null, 2) + '\n');
|
|
323
|
+
|
|
324
|
+
const md = renderRefinedBriefMarkdown(refinedBrief);
|
|
325
|
+
const mdPath = join(refineDir, `${refinedBrief.brief_id}.md`);
|
|
326
|
+
await writeFile(mdPath, md);
|
|
327
|
+
|
|
328
|
+
return { jsonPath, mdPath, refineDir };
|
|
329
|
+
}
|