Artifex/control_plane/authoring/management/commands/story_state.py

110 lines
4.6 KiB
Python
Raw Normal View History

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,
)
)