from __future__ import annotations import json from django.core.management.base import BaseCommand, CommandError from control_plane.authoring.models import ChapterRevision, ChapterStateDocument, StateChange from control_plane.authoring.services import DjangoStoryWorkflowServices from graph.models import GraphApproval, GraphApprovalStatus from model_router.providers import providers_from_resources from model_router.router import ModelRouter class Command(BaseCommand): help = "Build, inspect, or query the immutable story state ledger." def add_arguments(self, parser) -> None: parser.add_argument("action", choices=["build", "show", "history"]) parser.add_argument("--revision") parser.add_argument("--slug") parser.add_argument("--entity") parser.add_argument("--reuse-extraction", action="store_true") def handle(self, *args, **options) -> None: if options["action"] == "history": self._history(options) return if not options["revision"]: raise CommandError("build and show require --revision") revision = ChapterRevision.objects.select_related("chapter__story").get( id=options["revision"] ) if options["action"] == "show": self._show(revision) return services = DjangoStoryWorkflowServices( ModelRouter(providers_from_resources(), persist_requests=True) ) state = { "revision_id": str(revision.id), "story_id": str(revision.chapter.story_id), "context_snapshot_id": str(revision.context_snapshot_id or ""), } if not options["reuse_extraction"]: services.extract_continuity(state) elif not ChapterStateDocument.objects.filter(revision=revision).exists(): raise CommandError("--reuse-extraction requested but no state document exists") result = services.judge_state_contract(state) payload = services.state_approval_payload(state) approval = GraphApproval.objects.filter( reason=f"STORY_CHAPTER_APPROVAL:{revision.id}", status=GraphApprovalStatus.PENDING, ).first() if approval is not None: approval.payload = {**approval.payload, **payload} approval.save(update_fields=["payload", "updated_at"]) self.stdout.write(json.dumps({**result, **payload}, ensure_ascii=False, indent=2)) def _show(self, revision: ChapterRevision) -> None: document = ChapterStateDocument.objects.get(revision=revision) self.stdout.write( json.dumps( { "id": str(document.id), "status": document.status, "verdict": document.verdict, "coverage": document.coverage, "observed_state": document.observed_state, "proposed_delta": document.proposed_delta, "json_artifact_uri": document.json_artifact_uri, "markdown_artifact_uri": document.markdown_artifact_uri, }, ensure_ascii=False, indent=2, ) ) def _history(self, options: dict) -> None: if not options.get("slug") or not options.get("entity"): raise CommandError("history requires --slug and --entity") changes = StateChange.objects.filter( story__slug=options["slug"], entity__entity_key=options["entity"], status="COMMITTED", ).select_related("revision__chapter", "related_entity") self.stdout.write( json.dumps( [ { "chapter": change.effective_chapter, "revision_id": str(change.revision_id), "sequence": change.sequence, "change_type": change.change_type, "predicate": change.predicate, "operation": change.operation, "previous_value": change.previous_value, "new_value": change.new_value, "related_entity": ( change.related_entity.entity_key if change.related_entity else None ), "evidence_quote": change.evidence_quote, "evidence_location": change.evidence_location, } for change in changes.order_by("effective_chapter", "sequence") ], ensure_ascii=False, indent=2, ) )