"""Procedure-improvement suggestion workflow. When an operator submits a round with a filled-in procedure field, the command(s) in it are extracted heuristically (see ``procedure_extraction_service``) and proposed as an update to the originating template's suggested-procedure field. A suggestion is only ever created when there's a real, novel improvement to propose — nothing is written to the template until a lead reviews and approves it. """ from __future__ import annotations import uuid from datetime import datetime from sqlalchemy.orm import Session from app.domain.errors import EntityNotFoundError, InvalidOperationError from app.models.procedure_suggestion import ProcedureSuggestion from app.models.test import Test from app.models.test_template import TestTemplate from app.models.user import User from app.services.notification_service import notify_role from app.services.procedure_extraction_service import extract_commands # Which Test field holds the operator's free-text procedure, and which # TestTemplate field it's proposed as an improvement to, per team. _SOURCE_FIELD = {"red": "procedure_text", "blue": "detect_procedure"} _TEMPLATE_FIELD = {"red": "attack_procedure", "blue": "detect_suggested_procedure"} _LEAD_ROLE = {"red": "red_lead", "blue": "blue_lead"} def create_suggestion_from_submission(db: Session, test: Test, team: str) -> ProcedureSuggestion | None: """Propose a template procedure improvement from *test*'s just-submitted round. Returns the created (unflushed-to-caller-commit) suggestion, or ``None`` when there's nothing worth proposing: the test wasn't created from a template, no command could be extracted, the extraction matches what the template already suggests, or an identical suggestion is already pending review. Does not commit; caller uses UnitOfWork. """ if not test.source_template_id: return None procedure_text = getattr(test, _SOURCE_FIELD[team]) extracted = extract_commands(procedure_text) if not extracted: return None template = db.query(TestTemplate).filter(TestTemplate.id == test.source_template_id).first() if template is None: return None current = (getattr(template, _TEMPLATE_FIELD[team]) or "").strip() if extracted == current: return None duplicate = ( db.query(ProcedureSuggestion) .filter( ProcedureSuggestion.template_id == template.id, ProcedureSuggestion.team == team, ProcedureSuggestion.suggested_text == extracted, ProcedureSuggestion.status == "pending", ) .first() ) if duplicate is not None: return None submitter_id = test.red_tech_assignee if team == "red" else test.blue_tech_assignee suggestion = ProcedureSuggestion( template_id=template.id, team=team, suggested_text=extracted, source_test_id=test.id, submitted_by=submitter_id, ) db.add(suggestion) db.flush() notify_role( db, role=_LEAD_ROLE[team], type="procedure_suggestion", title="New procedure suggestion for review", message=f'A {team} procedure improvement was proposed for template "{template.name}".', entity_type="procedure_suggestion", entity_id=suggestion.id, ) return suggestion def list_pending_suggestions(db: Session, *, team: str | None = None) -> list[ProcedureSuggestion]: """List pending suggestions, optionally filtered to one team.""" query = db.query(ProcedureSuggestion).filter(ProcedureSuggestion.status == "pending") if team is not None: query = query.filter(ProcedureSuggestion.team == team) return query.order_by(ProcedureSuggestion.created_at.asc()).all() def _get_pending_suggestion_or_raise(db: Session, suggestion_id: uuid.UUID) -> ProcedureSuggestion: suggestion = db.query(ProcedureSuggestion).filter(ProcedureSuggestion.id == suggestion_id).first() if suggestion is None: raise EntityNotFoundError("ProcedureSuggestion", str(suggestion_id)) if suggestion.status != "pending": raise InvalidOperationError("This suggestion has already been reviewed.") return suggestion def approve_suggestion(db: Session, suggestion_id: uuid.UUID, user: User) -> ProcedureSuggestion: """Approve a suggestion: write it into the template and mark reviewed. Does not commit.""" suggestion = _get_pending_suggestion_or_raise(db, suggestion_id) template = db.query(TestTemplate).filter(TestTemplate.id == suggestion.template_id).first() if template is None: raise EntityNotFoundError("TestTemplate", str(suggestion.template_id)) setattr(template, _TEMPLATE_FIELD[suggestion.team], suggestion.suggested_text) suggestion.status = "approved" suggestion.reviewed_by = user.id suggestion.reviewed_at = datetime.utcnow() db.flush() return suggestion def reject_suggestion(db: Session, suggestion_id: uuid.UUID, user: User) -> ProcedureSuggestion: """Reject a suggestion without touching the template. Does not commit.""" suggestion = _get_pending_suggestion_or_raise(db, suggestion_id) suggestion.status = "rejected" suggestion.reviewed_by = user.id suggestion.reviewed_at = datetime.utcnow() db.flush() return suggestion