Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changelog/NEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@

- **Universe canon now lives inside Universe Builder.** Characters, places, and objects are managed inline on the universe page — no separate canon page to navigate to. The old canon URL still works as a redirect, and the Series Pipeline link lands you on the same combined view. Pending edits to other universe fields are no longer lost when canon changes are saved.
- **Locking a canon entry now also blocks new reference renders.** Locked characters/places/objects already prevented AI rewrites; they now prevent new reference and clean-plate image renders too, so a locked entry's identity stays frozen across both text and visuals. Disabled buttons explain the lock in their tooltips.

## Fixed

- **Volume cover-concept generation no longer 500s on upgraded installs.** The per-season "generate volume cover concepts" LLM step was shipped with its prompt template but missing from `stage-config.json`, so existing installs hit `Stage pipeline-volume-cover-concepts not found`. Added the config entry and a migration that seeds both the template and the config on next launch — fresh installs were already fine.
7 changes: 7 additions & 0 deletions data.sample/prompts/stage-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,13 @@
"returnsJson": true,
"variables": []
},
"pipeline-volume-cover-concepts": {
"name": "Pipeline — Volume Cover Concepts",
"description": "Generate FRONT and BACK cover-art concepts for one volume (season) of a comic series. Returns a {coverConcept, backCoverConcept} JSON pair — pure scene descriptions for the image-gen renderer (no masthead, logo, or typography). Lightweight per-season call; seeds blank cover scripts only and never clobbers a user edit.",
"model": "default",
"returnsJson": true,
"variables": []
},
"pipeline-comic-panel-image-prompt": {
"name": "Pipeline — Comic Panel Image Prompt",
"description": "Refine a single comic panel's description into a richer image-gen-ready prompt. Uses caption / dialogue / sfx context + neighboring-panel continuity to elaborate without re-imagining the scene.",
Expand Down
84 changes: 84 additions & 0 deletions scripts/migrations/017-volume-cover-concepts-stage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* Seed the `pipeline-volume-cover-concepts` stage into existing installs.
*
* Commit d802eb18 ("feat(pipeline): issue back covers + volume covers +
* trade-paperback PDF") shipped the per-season cover-concept LLM step
* (`arcPlanner.generateVolumeCoverConcepts` → `runStagedLLM('pipeline-volume-cover-concepts', …)`)
* and added `data.sample/prompts/stages/pipeline-volume-cover-concepts.md`
* but forgot two things existing installs need:
*
* 1. A `stage-config.json` entry — `setup-data.js` only merges
* `JSON_MERGE_TARGETS` on fresh setup, so existing installs that
* upgrade-and-restart never get the new entry and `prompts.getStage()`
* throws "Stage pipeline-volume-cover-concepts not found".
* 2. The `.md` template — `ensureSampleContent` copies *missing* prompt
* files on next run so a fresh install gets it, but an upgrade that
* skips re-running setup-data leaves it absent.
*
* This migration fixes both for existing installs. Modeled on the same
* idempotent pattern as `015-importer-stage-prompts.js`.
*/

import { access, copyFile, mkdir, readFile, writeFile, constants } from 'fs/promises';
import { dirname, join } from 'path';
Comment thread
atomantic marked this conversation as resolved.

const FILENAME = 'pipeline-volume-cover-concepts.md';
const STAGE_KEY = 'pipeline-volume-cover-concepts';

export default {
async up({ rootDir }) {
const stagesDir = join(rootDir, 'data', 'prompts', 'stages');
await mkdir(stagesDir, { recursive: true });

const dataPath = join(stagesDir, FILENAME);
const samplePath = join(rootDir, 'data.sample', 'prompts', 'stages', FILENAME);

const exists = await access(dataPath, constants.F_OK).then(() => true, () => false);
if (exists) {
console.log(`📝 volume-cover-concepts prompt: already present`);
} else {
const sampleExists = await access(samplePath, constants.F_OK).then(() => true, () => false);
if (!sampleExists) {
console.warn(`⚠️ volume-cover-concepts: sample missing for ${FILENAME} — skipping copy`);
} else {
try {
await copyFile(samplePath, dataPath);
console.log(`✅ seeded ${FILENAME}`);
} catch (err) {
console.warn(`⚠️ volume-cover-concepts: copy failed for ${FILENAME}: ${err.message}`);
}
}
}

const installedConfigPath = join(rootDir, 'data', 'prompts', 'stage-config.json');
const sampleConfigPath = join(rootDir, 'data.sample', 'prompts', 'stage-config.json');
const sampleConfigExists = await access(sampleConfigPath, constants.F_OK).then(() => true, () => false);
if (!sampleConfigExists) {
console.warn('⚠️ volume-cover-concepts: data.sample stage-config.json missing — cannot resolve entry; skipping config write');
return;
}
try {
const sample = JSON.parse(await readFile(sampleConfigPath, 'utf8'));
const installedExists = await access(installedConfigPath, constants.F_OK).then(() => true, () => false);
const installed = installedExists
? JSON.parse(await readFile(installedConfigPath, 'utf8'))
: { stages: {} };
installed.stages = installed.stages || {};
if (installed.stages[STAGE_KEY]) {
console.log(`📝 volume-cover-concepts stage-config: already present`);
return;
}
if (!sample?.stages?.[STAGE_KEY]) {
console.warn(`⚠️ volume-cover-concepts: sample stage-config missing ${STAGE_KEY} — skipping`);
return;
}
installed.stages[STAGE_KEY] = sample.stages[STAGE_KEY];
await mkdir(dirname(installedConfigPath), { recursive: true });
await writeFile(installedConfigPath, JSON.stringify(installed, null, 2) + '\n', 'utf8');
const action = installedExists ? 'merged' : 'created';
console.log(`📝 volume-cover-concepts stage-config (${action}): 1 added`);
} catch (err) {
console.warn(`⚠️ volume-cover-concepts: stage-config merge failed: ${err.message}`);
}
},
};