文件预览

continuity.py

查看 lobster-novel 技能包中的文件内容。

文件内容

core/continuity.py

#!/usr/bin/env python3
"""
lobster-novel: continuity tracker
"""
import json
from pathlib import Path
from dataclasses import dataclass, field, asdict
from datetime import datetime
from typing import List, Dict, Optional

@dataclass
class ChapterState:
    """State snapshot after a chapter"""
    chapter: int
    summary: str = ""
    changed_characters: Dict[str, str] = field(default_factory=dict)  # name->new_state
    new_facts: List[str] = field(default_factory=list)
    resolved_hooks: List[str] = field(default_factory=list)
    new_hooks: List[str] = field(default_factory=list)
    continuity_risks: List[str] = field(default_factory=list)
    word_count: int = 0

class ContinuityTracker:
    def __init__(self, project_dir: Path):
        self.dir = Path(project_dir) / "continuity"
        self.dir.mkdir(parents=True, exist_ok=True)
        self.state_file = self.dir / "state.jsonl"

    def append(self, state: ChapterState):
        """Append a chapter state record"""
        with open(self.state_file, "a", encoding="utf-8") as f:
            f.write(json.dumps(asdict(state), ensure_ascii=False) + "\n")

    def get_latest(self, n: int = 5) -> List[ChapterState]:
        """Get the last N chapter states"""
        if not self.state_file.exists():
            return []
        with open(self.state_file, encoding="utf-8") as f:
            lines = [l.strip() for l in f if l.strip()]
        return [ChapterState(**json.loads(l)) for l in lines[-n:]]

    def get_summary_for(self, chapter: int) -> str:
        """Get context summary up to chapter N"""
        states = self.get_latest(5)
        lines = []
        for s in states:
            if s.chapter > chapter:
                break
            lines.append(f"ch{s.chapter}: {s.summary}")
            if s.changed_characters:
                lines.append(f"  chars: {', '.join(f'{k}={v}' for k,v in s.changed_characters.items())}")
            if s.new_hooks:
                lines.append(f"  new hooks: {', '.join(s.new_hooks)}")
        return "\n".join(lines)

    def get_hook_status(self) -> str:
        """Get all active hooks (resolved hooks excluded)."""
        if not self.state_file.exists():
            return "no hooks"
        with open(self.state_file, encoding="utf-8") as f:
            states = [ChapterState(**json.loads(l)) for l in f if l.strip()]

        # First pass: collect all resolved hooks
        resolved = set()
        for s in states:
            for h in s.resolved_hooks:
                resolved.add(h)

        # Second pass: active = new_hooks not in resolved
        active = []
        seen = set()
        for s in states:
            for h in s.new_hooks:
                if h not in resolved and h not in seen:
                    active.append((s.chapter, h))
                    seen.add(h)

        if not active:
            return "no active hooks"
        return "\n".join(f"  ch{p[0]}: {p[1]}" for p in active[-10:])