Corrections moteur (TDD) : - R2 : within_mandate → record_in_observatory=True (Observatoire des décisions) - R6 : >50 personnes → collective recommandé, pas obligatoire (confidence=recommended) - R3 supprimée : affected_count=1 hors périmètre de l'outil - R9-R12 renommés G1-G4 (garde-fous internes) - 23 tests, 213/213 verts Étape 1 — Router /api/v1/qualify : - POST / → qualify() avec config depuis DB ou defaults - GET /protocol → protocole actif - POST /protocol → créer/remplacer (auth requise) Étape 2 — Modèle QualificationProtocol : - Table qualification_protocols (seuils configurables via admin) - Migration Alembic + seed du protocole par défaut Étape 3 — Wizard frontend decisions/new.vue : - Étape 1 : formulaire de qualification (mandat, affected_count, structurant, contexte) - Étape 2 : résultat (type, raisons, modalités, observatoire, on-chain) - Étape 3 : formulaire de décision (titre, description, protocole si collectif) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
111 lines
3.7 KiB
Python
111 lines
3.7 KiB
Python
"""Qualify router: decision qualification engine endpoint."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import asdict
|
|
|
|
from fastapi import APIRouter, Depends
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.database import get_db
|
|
from app.engine.qualifier import QualificationConfig, QualificationInput, qualify
|
|
from app.models.qualification import QualificationProtocol
|
|
from app.schemas.qualification import (
|
|
QualificationProtocolCreate,
|
|
QualificationProtocolOut,
|
|
QualifyRequest,
|
|
QualifyResponse,
|
|
)
|
|
from app.services.auth_service import get_current_identity
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
async def _load_config(db: AsyncSession) -> QualificationConfig:
|
|
"""Load the active QualificationProtocol from DB, or fall back to defaults."""
|
|
result = await db.execute(
|
|
select(QualificationProtocol)
|
|
.where(QualificationProtocol.is_active == True) # noqa: E712
|
|
.order_by(QualificationProtocol.created_at.desc())
|
|
.limit(1)
|
|
)
|
|
proto = result.scalar_one_or_none()
|
|
if proto is None:
|
|
return QualificationConfig()
|
|
return QualificationConfig(
|
|
small_group_max=proto.small_group_max,
|
|
collective_wot_min=proto.collective_wot_min,
|
|
default_modalities=proto.default_modalities,
|
|
)
|
|
|
|
|
|
@router.post("/", response_model=QualifyResponse)
|
|
async def qualify_decision(
|
|
payload: QualifyRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> QualifyResponse:
|
|
"""Qualify a decision: determine type, process, and modalities.
|
|
|
|
No authentication required — this is an advisory endpoint that helps
|
|
users understand which decision pathway fits their situation.
|
|
"""
|
|
config = await _load_config(db)
|
|
inp = QualificationInput(
|
|
within_mandate=payload.within_mandate,
|
|
affected_count=payload.affected_count,
|
|
is_structural=payload.is_structural,
|
|
context_description=payload.context_description,
|
|
)
|
|
result = qualify(inp, config)
|
|
return QualifyResponse(**asdict(result))
|
|
|
|
|
|
@router.get("/protocol", response_model=QualificationProtocolOut | None)
|
|
async def get_active_protocol(
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> QualificationProtocolOut | None:
|
|
"""Return the currently active qualification protocol (thresholds)."""
|
|
result = await db.execute(
|
|
select(QualificationProtocol)
|
|
.where(QualificationProtocol.is_active == True) # noqa: E712
|
|
.order_by(QualificationProtocol.created_at.desc())
|
|
.limit(1)
|
|
)
|
|
proto = result.scalar_one_or_none()
|
|
if proto is None:
|
|
return None
|
|
return QualificationProtocolOut.model_validate(proto)
|
|
|
|
|
|
@router.post("/protocol", response_model=QualificationProtocolOut, status_code=201)
|
|
async def create_protocol(
|
|
payload: QualificationProtocolCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
_identity=Depends(get_current_identity),
|
|
) -> QualificationProtocolOut:
|
|
"""Create a new qualification protocol (requires auth).
|
|
|
|
Deactivates the current active protocol before saving the new one.
|
|
"""
|
|
# Deactivate current
|
|
current = await db.execute(
|
|
select(QualificationProtocol).where(QualificationProtocol.is_active == True) # noqa: E712
|
|
)
|
|
for proto in current.scalars().all():
|
|
proto.is_active = False
|
|
|
|
import json
|
|
proto = QualificationProtocol(
|
|
name=payload.name,
|
|
description=payload.description,
|
|
small_group_max=payload.small_group_max,
|
|
collective_wot_min=payload.collective_wot_min,
|
|
default_modalities_json=json.dumps(payload.default_modalities),
|
|
is_active=True,
|
|
)
|
|
db.add(proto)
|
|
await db.commit()
|
|
await db.refresh(proto)
|
|
return QualificationProtocolOut.model_validate(proto)
|