diff --git a/.changelog/NEXT.md b/.changelog/NEXT.md index 25eac05ac..b284e7258 100644 --- a/.changelog/NEXT.md +++ b/.changelog/NEXT.md @@ -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. diff --git a/data.sample/prompts/stage-config.json b/data.sample/prompts/stage-config.json index 9f4c0ffa9..23de2e690 100644 --- a/data.sample/prompts/stage-config.json +++ b/data.sample/prompts/stage-config.json @@ -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.", diff --git a/scripts/migrations/017-volume-cover-concepts-stage.js b/scripts/migrations/017-volume-cover-concepts-stage.js new file mode 100644 index 000000000..7c4f174d7 --- /dev/null +++ b/scripts/migrations/017-volume-cover-concepts-stage.js @@ -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'; + +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}`); + } + }, +};