"""Named, versioned journal generation guidelines. Not model temperature. Four independent dimensions are selected by ID. Instruction text lives in generation_guidelines, seeded from JSON. This module selects, validates and composes. It does not own prompt wording and has no numeric 0–100 path. """ from __future__ import annotations import json import re import uuid from dataclasses import dataclass from pathlib import Path from db import get_db, row_to_dict from journal_policy import PolicyError from placeholders import CONTEXT_PATTERN PURPOSE_JOURNAL = "journal_generate" USER_SLOTS = ("transformation", "detail", "voice", "narrative") ALLOWED_SLOTS = USER_SLOTS LEGACY_SOURCE_MODE_SLOT = "source_mode" SELECTION_KEYS = ( "transformation_id", "detail_id", "voice_id", "narrative_id", ) SLOT_TO_ID_KEY = { "transformation": "transformation_id", "detail": "detail_id", "voice": "voice_id", "narrative": "narrative_id", } ID_KEY_TO_SLOT = {value: key for key, value in SLOT_TO_ID_KEY.items()} SLOT_INSTRUCTION_KEYS = { "transformation": "transformation_instructions", "detail": "detail_instructions", "voice": "voice_instructions", "narrative": "narrative_instructions", } REQUIRED_JOURNAL_PLACEHOLDERS = ( "transformation_instructions", "detail_instructions", "voice_instructions", "narrative_instructions", "writing_profile", "style_examples", "reconstruction", "existing_text", ) RETIRED_JOURNAL_PLACEHOLDERS = ( "source_mode_instructions", "editorial_mode", "editorial_instructions", ) STATUS_DRAFT = "draft" STATUS_ACTIVE = "active" STATUS_ARCHIVED = "archived" STATUSES = (STATUS_DRAFT, STATUS_ACTIVE, STATUS_ARCHIVED) MAX_INSTRUCTION_CHARS = 1000 MAX_LABEL_CHARS = 80 MAX_SUMMARY_CHARS = 160 KEY_RE = re.compile(r"^[a-z][a-z0-9_]{0,40}$") SEED_PATH = Path(__file__).resolve().parent / "config" / "generation_instructions.seed.json" PUBLIC_FIELDS = ( "id", "purpose", "slot", "guideline_key", "label", "summary", "sort_order", "status", "revision", "cloned_from", "is_default", "is_system_seed", "seed_revision", "used_at", "created", "updated", ) class GenerationPolicyError(PolicyError): def __init__( self, message: str, *, code: str = "invalid_generation_selection", status_code: int = 400, ): super().__init__(code, message, status_code) class CatalogError(GenerationPolicyError): def __init__(self, message: str, *, code: str = "generation_policy_invalid", status_code: int = 409): super().__init__(message, code=code, status_code=status_code) @dataclass(frozen=True) class CompiledPolicy: ids: dict[str, str] keys: dict[str, str] labels: dict[str, str] summaries: dict[str, str] revisions: dict[str, int] instructions: dict[str, str] seed_revision: str def load_seed_document() -> dict: return json.loads(SEED_PATH.read_text(encoding="utf-8")) def _as_bool(raw) -> bool: return bool(int(raw)) if not isinstance(raw, bool) else raw def _limits() -> tuple[int, int, int]: seed = load_seed_document() return ( int(seed.get("max_instruction_chars") or MAX_INSTRUCTION_CHARS), int(seed.get("max_label_chars") or MAX_LABEL_CHARS), int(seed.get("max_summary_chars") or MAX_SUMMARY_CHARS), ) def _clean_text(raw, field: str, *, max_chars: int, allow_empty: bool = False) -> str: text = (raw or "").strip() if not text and not allow_empty: raise CatalogError(f"{field} darf nicht leer sein.", code="invalid_generation_guideline", status_code=400) if len(text) > max_chars: raise CatalogError(f"{field} darf höchstens {max_chars} Zeichen haben.", code="invalid_generation_guideline", status_code=400) return text def _clean_key(raw) -> str: key = (raw or "").strip() if not KEY_RE.match(key): raise CatalogError( "guideline_key muss aus Kleinbuchstaben, Ziffern und Unterstrich bestehen.", code="invalid_generation_guideline", status_code=400, ) return key def _row(item: dict | None, *, include_instruction: bool = False) -> dict | None: if not item: return None payload = {field: item.get(field) for field in PUBLIC_FIELDS} payload["is_default"] = _as_bool(item.get("is_default", 0)) payload["is_system_seed"] = _as_bool(item.get("is_system_seed", 0)) payload["revision"] = int(item.get("revision") or 1) payload["sort_order"] = int(item.get("sort_order") or 0) payload["guideline_key"] = item.get("guideline_key") or "" if include_instruction: payload["instruction"] = item.get("instruction") or "" payload["seed_id"] = item.get("seed_id") or "" return payload def _fetch(guideline_id: str) -> dict | None: with get_db() as conn: return row_to_dict( conn.execute("SELECT * FROM generation_guidelines WHERE id = ?", (guideline_id,)).fetchone() ) def get_guideline(guideline_id: str, *, include_instruction: bool = True) -> dict: row = _fetch(guideline_id) if not row: raise CatalogError("Ausprägung nicht gefunden.", code="guideline_missing", status_code=404) return _row(row, include_instruction=include_instruction) def list_guidelines( purpose: str = PURPOSE_JOURNAL, *, slot: str | None = None, statuses: tuple[str, ...] | None = None, include_instruction: bool = False, ) -> list[dict]: query = "SELECT * FROM generation_guidelines WHERE purpose = ?" params: list = [purpose] if slot: query += " AND slot = ?" params.append(slot) if statuses: query += " AND status IN (" + ",".join("?" for _ in statuses) + ")" params.extend(statuses) query += " ORDER BY slot, sort_order, revision, label" with get_db() as conn: rows = [row_to_dict(row) for row in conn.execute(query, params).fetchall()] return [_row(row, include_instruction=include_instruction) for row in rows] def overview_payload(purpose: str = PURPOSE_JOURNAL) -> dict: seed = load_seed_document() items = list_guidelines(purpose) slots = {slot: [item for item in items if item["slot"] == slot] for slot in ALLOWED_SLOTS} return { "purpose": purpose, "seed_revision": seed.get("seed_revision") or "", "max_instruction_chars": int(seed.get("max_instruction_chars") or MAX_INSTRUCTION_CHARS), "max_label_chars": int(seed.get("max_label_chars") or MAX_LABEL_CHARS), "max_summary_chars": int(seed.get("max_summary_chars") or MAX_SUMMARY_CHARS), "slots": slots, } def active_user_options(purpose: str = PURPOSE_JOURNAL) -> dict[str, list[dict]]: items = list_guidelines(purpose, statuses=(STATUS_ACTIVE,)) return { slot: [ { "id": item["id"], "key": item["guideline_key"], "label": item["label"], "summary": item["summary"], "revision": item["revision"], "is_default": item["is_default"], } for item in items if item["slot"] == slot ] for slot in USER_SLOTS } def default_selection_ids(purpose: str = PURPOSE_JOURNAL) -> dict[str, str]: items = list_guidelines(purpose, statuses=(STATUS_ACTIVE,)) chosen = {} for slot in USER_SLOTS: slot_items = [item for item in items if item["slot"] == slot] if not slot_items: raise CatalogError(f"{slot} hat keine aktive Ausprägung.") preferred = next((item for item in slot_items if item["is_default"]), slot_items[0]) chosen[SLOT_TO_ID_KEY[slot]] = preferred["id"] return chosen def _require_active_for_run(row: dict, slot: str) -> dict: if not row: raise GenerationPolicyError(f"{slot} ist unbekannt.") if row.get("slot") != slot: raise GenerationPolicyError(f"{slot} verweist auf die falsche Dimension.") if row.get("status") != STATUS_ACTIVE: raise GenerationPolicyError( f"{slot} ist nicht für neue Läufe verfügbar.", code="invalid_generation_selection", ) if not (row.get("instruction") or "").strip(): raise CatalogError(f"{slot} hat keine Anweisung.") return row def validate_selection(raw, *, purpose: str = PURPOSE_JOURNAL) -> dict[str, dict]: if not isinstance(raw, dict): raise GenerationPolicyError("generation_selection muss die vier Ausprägungs-IDs enthalten.") unknown = sorted(set(raw) - set(SELECTION_KEYS)) if unknown: raise GenerationPolicyError("Unbekannte Auswahl: " + ", ".join(unknown)) missing = [key for key in SELECTION_KEYS if not (raw.get(key) or "").strip()] if missing: raise GenerationPolicyError("generation_selection ist unvollständig: " + ", ".join(missing)) chosen = {} for key in SELECTION_KEYS: slot = ID_KEY_TO_SLOT[key] row = _fetch(str(raw[key]).strip()) chosen[slot] = _require_active_for_run(row, slot) if (row.get("purpose") or purpose) != purpose: raise GenerationPolicyError(f"{slot} gehört nicht zu diesem Zweck.") return chosen def compile_selection( selection: dict[str, str], *, purpose: str = PURPOSE_JOURNAL, ) -> CompiledPolicy: chosen = validate_selection(selection, purpose=purpose) instructions = { SLOT_INSTRUCTION_KEYS[slot]: chosen[slot]["instruction"] for slot in USER_SLOTS } revision = "" for row in chosen.values(): revision = row.get("seed_revision") or revision return CompiledPolicy( ids={slot: chosen[slot]["id"] for slot in USER_SLOTS}, keys={slot: chosen[slot]["guideline_key"] for slot in USER_SLOTS}, labels={slot: chosen[slot]["label"] for slot in USER_SLOTS}, summaries={slot: chosen[slot].get("summary") or "" for slot in USER_SLOTS}, revisions={slot: int(chosen[slot].get("revision") or 1) for slot in USER_SLOTS}, instructions=instructions, seed_revision=revision, ) def policy_trace( compiled: CompiledPolicy, *, source: str, remembered: bool, ) -> dict: return { "source": source, "remembered": remembered, "ids": dict(compiled.ids), "keys": dict(compiled.keys), "labels": dict(compiled.labels), "revisions": dict(compiled.revisions), "seed_revision": compiled.seed_revision, } def draft_snapshot(compiled: CompiledPolicy, *, prompt: dict | None = None, model: str = "") -> dict: """Persistable run snapshot. No prompt bodies or instruction texts.""" return { "transformation": { "id": compiled.ids["transformation"], "key": compiled.keys["transformation"], "label": compiled.labels["transformation"], "revision": compiled.revisions["transformation"], }, "detail": { "id": compiled.ids["detail"], "key": compiled.keys["detail"], "label": compiled.labels["detail"], "revision": compiled.revisions["detail"], }, "voice": { "id": compiled.ids["voice"], "key": compiled.keys["voice"], "label": compiled.labels["voice"], "revision": compiled.revisions["voice"], }, "narrative": { "id": compiled.ids["narrative"], "key": compiled.keys["narrative"], "label": compiled.labels["narrative"], "revision": compiled.revisions["narrative"], }, "prompt_slug": (prompt or {}).get("slug") or "", "prompt_revision": (prompt or {}).get("seed_revision") or "", "model": model or "", } def snapshot_summary(snapshot: dict | None) -> str: data = snapshot or {} labels = [ (data.get("transformation") or {}).get("label") or "", (data.get("detail") or {}).get("label") or "", (data.get("voice") or {}).get("label") or "", (data.get("narrative") or {}).get("label") or "", ] labels = [item for item in labels if item] if not labels: return "" line = " · ".join(item[:1].upper() + item[1:] if item else item for item in labels) return f"Erzeugt mit:\n{line}" def assert_journal_prompt_contract(template: str) -> None: keys = CONTEXT_PATTERN.findall(template or "") retired = [key for key in RETIRED_JOURNAL_PLACEHOLDERS if key in keys] if retired: shown = ", ".join(f"{{{{{key}}}}}" for key in retired) raise CatalogError( "Dieser Journal-Prompt verwendet den veralteten Platzhalter " + shown + ". Die Unterscheidung zwischen Fließtext- und Stichpunktmodus ist entfallen. " "Bitte den Prompt unter Admin → Prompts aktualisieren.", code="prompt_contract_incompatible", ) missing = [key for key in REQUIRED_JOURNAL_PLACEHOLDERS if key not in keys] if missing: shown = ", ".join(f"{{{{{key}}}}}" for key in missing) raise CatalogError( "Dieser Journal-Prompt erfüllt den aktuellen Vertrag nicht. Es fehlen: " + shown + ". Bitte den Prompt unter Admin → Prompts aktualisieren.", code="prompt_contract_incompatible", ) def assert_template_resolved(rendered: str) -> None: leftover = CONTEXT_PATTERN.findall(rendered or "") if leftover: raise CatalogError( "Promptplatzhalter konnten nicht aufgelöst werden: " + ", ".join(f"{{{{{key}}}}}" for key in leftover), code="unresolved_placeholder", ) def load_selection(profile_id: str) -> dict[str, str] | None: with get_db() as conn: row = row_to_dict( conn.execute( """ SELECT transformation_id, detail_id, voice_id, narrative_id, updated FROM journal_generation_selection WHERE profile_id = ? """, (profile_id,), ).fetchone() ) if not row: return None values = {key: row[key] for key in SELECTION_KEYS} values["updated"] = row.get("updated") or "" return values def save_selection(profile_id: str, selection: dict[str, str]) -> dict[str, str]: chosen = validate_selection(selection) checked = {SLOT_TO_ID_KEY[slot]: chosen[slot]["id"] for slot in USER_SLOTS} with get_db() as conn: conn.execute( """ INSERT INTO journal_generation_selection ( profile_id, transformation_id, detail_id, voice_id, narrative_id, updated ) VALUES (?, ?, ?, ?, ?, datetime('now')) ON CONFLICT(profile_id) DO UPDATE SET transformation_id = excluded.transformation_id, detail_id = excluded.detail_id, voice_id = excluded.voice_id, narrative_id = excluded.narrative_id, updated = datetime('now') """, ( profile_id, checked["transformation_id"], checked["detail_id"], checked["voice_id"], checked["narrative_id"], ), ) stored = load_selection(profile_id) or {**checked, "updated": ""} return stored def get_or_create_selection(profile_id: str) -> dict[str, str]: existing = load_selection(profile_id) if existing is not None: return existing return save_selection(profile_id, default_selection_ids()) def settings_payload(profile_id: str) -> dict: stored = get_or_create_selection(profile_id) selection = {key: stored[key] for key in SELECTION_KEYS} return { "selection": selection, "updated": stored.get("updated") or "", "options": active_user_options(), "defaults": default_selection_ids(), } def resolve_run_selection( profile_id: str, snapshot: dict | None, remember: bool, ) -> tuple[dict[str, str], dict]: if snapshot is None: stored = get_or_create_selection(profile_id) values = {key: stored[key] for key in SELECTION_KEYS} validate_selection(values) return values, {"source": "profile", "remembered": False} values = {key: str(snapshot.get(key) or "").strip() for key in SELECTION_KEYS} validate_selection(values) remembered = False if remember: save_selection(profile_id, values) remembered = True return values, {"source": "request", "remembered": remembered} def mark_guidelines_used(ids: list[str]) -> None: clean = [item for item in ids if item] if not clean: return with get_db() as conn: conn.executemany( """ UPDATE generation_guidelines SET used_at = COALESCE(used_at, datetime('now')) WHERE id = ? """, [(item,) for item in clean], ) def _write_fields(raw: dict) -> dict: max_instruction, max_label, max_summary = _limits() return { "guideline_key": _clean_key(raw.get("guideline_key") or raw.get("key")), "label": _clean_text(raw.get("label"), "label", max_chars=max_label), "summary": _clean_text(raw.get("summary"), "summary", max_chars=max_summary, allow_empty=True), "instruction": _clean_text(raw.get("instruction"), "instruction", max_chars=max_instruction), "sort_order": int(raw.get("sort_order") or 0), } def create_guideline(purpose: str, slot: str, body: dict) -> dict: if slot not in ALLOWED_SLOTS: raise CatalogError("Unbekannter Slot.", code="invalid_generation_guideline", status_code=400) fields = _write_fields(body) guideline_id = str(uuid.uuid4()) with get_db() as conn: count = conn.execute( "SELECT COALESCE(MAX(sort_order), -1) AS n FROM generation_guidelines WHERE purpose = ? AND slot = ?", (purpose, slot), ).fetchone()["n"] conn.execute( """ INSERT INTO generation_guidelines ( id, purpose, slot, guideline_key, label, summary, instruction, sort_order, status, revision, cloned_from, is_default, is_system_seed, seed_id, seed_revision, created, updated ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, NULL, 0, 0, '', '', datetime('now'), datetime('now')) """, ( guideline_id, purpose, slot, fields["guideline_key"], fields["label"], fields["summary"], fields["instruction"], fields["sort_order"] if body.get("sort_order") is not None else int(count) + 1, STATUS_DRAFT, ), ) return get_guideline(guideline_id) def clone_guideline(guideline_id: str) -> dict: current = _fetch(guideline_id) if not current: raise CatalogError("Ausprägung nicht gefunden.", code="guideline_missing", status_code=404) if current.get("slot") == LEGACY_SOURCE_MODE_SLOT: raise CatalogError( "Der Quellenmodus ist kein aktiver Slot mehr.", code="invalid_generation_guideline", status_code=400, ) new_id = str(uuid.uuid4()) with get_db() as conn: conn.execute( """ INSERT INTO generation_guidelines ( id, purpose, slot, guideline_key, label, summary, instruction, sort_order, status, revision, cloned_from, is_default, is_system_seed, seed_id, seed_revision, created, updated ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, '', ?, datetime('now'), datetime('now')) """, ( new_id, current["purpose"], current["slot"], current["guideline_key"], current["label"], current["summary"], current["instruction"], int(current.get("sort_order") or 0), STATUS_DRAFT, int(current.get("revision") or 1) + 1, current["id"], current.get("seed_revision") or "", ), ) return get_guideline(new_id) def update_guideline(guideline_id: str, body: dict) -> dict: current = _fetch(guideline_id) if not current: raise CatalogError("Ausprägung nicht gefunden.", code="guideline_missing", status_code=404) if current.get("status") != STATUS_DRAFT: raise CatalogError( "Aktive oder archivierte Ausprägungen sind unveränderlich. Bitte klonen.", code="guideline_immutable", status_code=409, ) fields = _write_fields({**current, **body, "guideline_key": body.get("guideline_key") or current["guideline_key"]}) with get_db() as conn: conn.execute( """ UPDATE generation_guidelines SET guideline_key = ?, label = ?, summary = ?, instruction = ?, sort_order = ?, updated = datetime('now') WHERE id = ? """, ( fields["guideline_key"], fields["label"], fields["summary"], fields["instruction"], fields["sort_order"], guideline_id, ), ) return get_guideline(guideline_id) def publish_guideline(guideline_id: str) -> dict: current = _fetch(guideline_id) if not current: raise CatalogError("Ausprägung nicht gefunden.", code="guideline_missing", status_code=404) if current.get("status") == STATUS_ARCHIVED: raise CatalogError("Archivierte Ausprägungen können nicht veröffentlicht werden.", code="guideline_archived", status_code=409) max_instruction, max_label, max_summary = _limits() _clean_text(current.get("label"), "label", max_chars=max_label) _clean_text(current.get("summary"), "summary", max_chars=max_summary, allow_empty=True) _clean_text(current.get("instruction"), "instruction", max_chars=max_instruction) with get_db() as conn: conn.execute( "UPDATE generation_guidelines SET status = ?, updated = datetime('now') WHERE id = ?", (STATUS_ACTIVE, guideline_id), ) return get_guideline(guideline_id, include_instruction=False) def archive_guideline(guideline_id: str) -> dict: current = _fetch(guideline_id) if not current: raise CatalogError("Ausprägung nicht gefunden.", code="guideline_missing", status_code=404) if current.get("status") == STATUS_DRAFT: raise CatalogError("Drafts werden gelöscht, nicht archiviert.", code="invalid_generation_guideline", status_code=400) with get_db() as conn: remaining = conn.execute( """ SELECT COUNT(*) AS n FROM generation_guidelines WHERE purpose = ? AND slot = ? AND status = ? AND id != ? """, (current["purpose"], current["slot"], STATUS_ACTIVE, guideline_id), ).fetchone()["n"] if int(remaining or 0) < 1: raise CatalogError( "Die letzte aktive Ausprägung dieser Dimension kann nicht archiviert werden.", code="guideline_last_active", status_code=409, ) with get_db() as conn: conn.execute( """ UPDATE generation_guidelines SET status = ?, is_default = 0, updated = datetime('now') WHERE id = ? """, (STATUS_ARCHIVED, guideline_id), ) return get_guideline(guideline_id, include_instruction=False) def set_default_guideline(guideline_id: str) -> dict: current = _fetch(guideline_id) if not current: raise CatalogError("Ausprägung nicht gefunden.", code="guideline_missing", status_code=404) if current.get("status") != STATUS_ACTIVE: raise CatalogError("Nur aktive Ausprägungen können Standard sein.", code="invalid_generation_guideline", status_code=400) if current["slot"] not in USER_SLOTS: raise CatalogError("Nur die vier Gestaltungsdimensionen haben einen Nutzerstandard.", code="invalid_generation_guideline", status_code=400) with get_db() as conn: conn.execute( """ UPDATE generation_guidelines SET is_default = CASE WHEN id = ? THEN 1 ELSE 0 END, updated = datetime('now') WHERE purpose = ? AND slot = ? """, (guideline_id, current["purpose"], current["slot"]), ) return get_guideline(guideline_id, include_instruction=False) def delete_guideline(guideline_id: str) -> None: current = _fetch(guideline_id) if not current: raise CatalogError("Ausprägung nicht gefunden.", code="guideline_missing", status_code=404) if current.get("status") != STATUS_DRAFT: raise CatalogError("Nur unverwendete Drafts können gelöscht werden.", code="guideline_immutable", status_code=409) if current.get("used_at"): raise CatalogError("Verwendete Drafts können nicht gelöscht werden.", code="guideline_in_use", status_code=409) with get_db() as conn: conn.execute("DELETE FROM generation_guidelines WHERE id = ?", (guideline_id,)) def reset_seed_drafts(purpose: str = PURPOSE_JOURNAL) -> dict: """Create new drafts from the current seed. Never overwrite published variants.""" seed = load_seed_document() created = [] slots = seed.get("slots") or {} for slot in ALLOWED_SLOTS: for index, item in enumerate((slots.get(slot) or {}).get("variants") or []): created.append( create_guideline( purpose, slot, { "guideline_key": item.get("guideline_key") or item.get("variant_key"), "label": item.get("label") or "", "summary": item.get("summary") or "", "instruction": item.get("instruction") or "", "sort_order": index, }, ) ) overview = overview_payload(purpose) overview["reset_drafts"] = created return overview def preview_selection(selection: dict[str, str], *, purpose: str = PURPOSE_JOURNAL) -> dict: from engine import load_active_prompt, preview_prompt compiled = compile_selection(selection, purpose=purpose) prompt = load_active_prompt("mvp.journal_generate") assert_journal_prompt_contract(prompt.get("template") or "") rendered = preview_prompt( prompt, { **compiled.instructions, "writing_profile": "(nicht enthalten)", "style_examples": "(nicht enthalten)", "reconstruction": "(nicht enthalten)", "existing_text": "", }, ) assert_template_resolved(rendered.get("rendered") or "") return { **compiled.instructions, "rendered": rendered.get("rendered") or "", "prompt_slug": prompt.get("slug") or "", "prompt_revision": prompt.get("seed_revision") or "", "selection": { "ids": dict(compiled.ids), "keys": dict(compiled.keys), "labels": dict(compiled.labels), "revisions": dict(compiled.revisions), }, "seed_revision": compiled.seed_revision, } def _seed_rows(seed: dict, purpose: str) -> list[tuple]: revision = seed.get("seed_revision") or "" rows = [] slots = seed.get("slots") or {} for slot in ALLOWED_SLOTS: variants = (slots.get(slot) or {}).get("variants") or [] for index, item in enumerate(variants): rows.append( ( item.get("id") or str(uuid.uuid4()), purpose, slot, item.get("guideline_key") or item.get("variant_key"), item.get("label") or "", item.get("summary") or "", item.get("instruction") or "", index, STATUS_ACTIVE, 1, 1 if item.get("is_default") else 0, 1, item.get("id") or "", revision, ) ) return rows def seed_generation_instructions(conn) -> None: """Insert missing seed guidelines. Never overwrite published or admin-created rows.""" seed = load_seed_document() purpose = seed.get("purpose") or PURPOSE_JOURNAL conn.execute( """ UPDATE generation_guidelines SET status = ?, is_default = 0, updated = datetime('now') WHERE purpose = ? AND slot = ? AND status != ? """, (STATUS_ARCHIVED, purpose, LEGACY_SOURCE_MODE_SLOT, STATUS_ARCHIVED), ) existing = { row["id"] for row in conn.execute("SELECT id FROM generation_guidelines WHERE purpose = ?", (purpose,)).fetchall() } existing_seed_ids = { row["seed_id"] for row in conn.execute( "SELECT seed_id FROM generation_guidelines WHERE purpose = ? AND seed_id != ''", (purpose,), ).fetchall() if row["seed_id"] } for row in _seed_rows(seed, purpose): row_id = row[0] seed_id = row[12] if row_id in existing or seed_id in existing_seed_ids: continue conn.execute( """ INSERT INTO generation_guidelines ( id, purpose, slot, guideline_key, label, summary, instruction, sort_order, status, revision, cloned_from, is_default, is_system_seed, seed_id, seed_revision, created, updated ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, datetime('now'), datetime('now')) """, row, ) def backfill_missing_settings(conn) -> None: seed_generation_instructions(conn) defaults = {} for slot, key in SLOT_TO_ID_KEY.items(): row = conn.execute( """ SELECT id FROM generation_guidelines WHERE purpose = ? AND slot = ? AND status = ? AND is_default = 1 ORDER BY sort_order LIMIT 1 """, (PURPOSE_JOURNAL, slot, STATUS_ACTIVE), ).fetchone() if not row: row = conn.execute( """ SELECT id FROM generation_guidelines WHERE purpose = ? AND slot = ? AND status = ? ORDER BY sort_order LIMIT 1 """, (PURPOSE_JOURNAL, slot, STATUS_ACTIVE), ).fetchone() if row: defaults[key] = row["id"] if len(defaults) != 4: return conn.execute( """ INSERT OR IGNORE INTO journal_generation_selection ( profile_id, transformation_id, detail_id, voice_id, narrative_id, updated ) SELECT id, ?, ?, ?, ?, datetime('now') FROM profiles """, ( defaults["transformation_id"], defaults["detail_id"], defaults["voice_id"], defaults["narrative_id"], ), )