Qualifier : corrections R2/R6 + router + modèle DB + wizard frontend
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>
This commit is contained in:
@@ -18,7 +18,7 @@ class DecisionType(str, Enum):
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration (thresholds — stored as a QualificationProtocol in DB)
|
||||
# Configuration (thresholds — stored as QualificationProtocol in DB)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -26,16 +26,15 @@ class DecisionType(str, Enum):
|
||||
class QualificationConfig:
|
||||
"""Configurable thresholds for the qualification engine.
|
||||
|
||||
These defaults will be seeded as a QualificationProtocol record so they
|
||||
can be adjusted through the admin interface without code changes.
|
||||
Seeded as a QualificationProtocol record so they can be adjusted
|
||||
through the admin interface without code changes.
|
||||
|
||||
individual_max: affected_count <= this → always individual
|
||||
small_group_max: affected_count <= this → individual recommended, collective available
|
||||
collective_wot_min: affected_count > this → collective required (WoT formula applies)
|
||||
small_group_max: affected_count <= this → individual recommended, collective available
|
||||
collective_wot_min: affected_count > this → WoT formula applicable (still recommended, not required)
|
||||
|
||||
Default modalities shown when collective is chosen (ordered by relevance).
|
||||
affected_count must be >= 2 — decisions affecting only the author
|
||||
have no place in this tool.
|
||||
"""
|
||||
individual_max: int = 1
|
||||
small_group_max: int = 5
|
||||
collective_wot_min: int = 50
|
||||
|
||||
@@ -55,7 +54,7 @@ class QualificationConfig:
|
||||
@dataclass
|
||||
class QualificationInput:
|
||||
within_mandate: bool = False
|
||||
affected_count: int | None = None
|
||||
affected_count: int | None = None # must be >= 2 when within_mandate=False
|
||||
is_structural: bool = False
|
||||
context_description: str | None = None # reserved for LLM suggestion
|
||||
|
||||
@@ -66,10 +65,11 @@ class QualificationResult:
|
||||
process: str
|
||||
recommended_modalities: list[str]
|
||||
recommend_onchain: bool
|
||||
confidence: str # "required" | "recommended" | "optional"
|
||||
onchain_reason: str | None
|
||||
confidence: str # "required" | "recommended" | "optional"
|
||||
collective_available: bool
|
||||
record_in_observatory: bool # True → decision must be logged in Observatoire
|
||||
reasons: list[str]
|
||||
onchain_reason: str | None = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -83,9 +83,9 @@ def suggest_modalities_from_context(
|
||||
) -> list[str]:
|
||||
"""Suggest voting modalities based on a natural-language context description.
|
||||
|
||||
Stub — returns empty list until local Qwen (qwen3.6) is integrated.
|
||||
When implemented, this will call the LLM API and return an ordered list
|
||||
of modality slugs from config.default_modalities.
|
||||
Stub — returns empty list until local Qwen (qwen3.6, MacStudio) is integrated.
|
||||
When implemented, will call the LLM API and return an ordered subset of
|
||||
config.default_modalities ranked by contextual relevance.
|
||||
"""
|
||||
return []
|
||||
|
||||
@@ -99,11 +99,11 @@ def qualify(inp: QualificationInput, config: QualificationConfig) -> Qualificati
|
||||
"""Qualify a decision and recommend a type, process, and modalities.
|
||||
|
||||
Rules (in priority order):
|
||||
R1/R2 within_mandate → individual + consultation_avis, no modalities
|
||||
R3 affected_count == 1 → individual + personal
|
||||
R4 affected_count ≤ small_group_max → individual recommended, collective available
|
||||
R1/R2 within_mandate → individual + consultation_avis, no vote modalities,
|
||||
decision must be recorded in Observatoire des décisions
|
||||
R4 2 ≤ affected_count ≤ small_group_max → individual recommended, collective available
|
||||
R5 small_group_max < affected_count ≤ collective_wot_min → collective recommended
|
||||
R6 affected_count > collective_wot_min → collective required (WoT)
|
||||
R6 affected_count > collective_wot_min → collective recommended (WoT formula applicable)
|
||||
R7/R8 is_structural → recommend_onchain with reason
|
||||
"""
|
||||
reasons: list[str] = []
|
||||
@@ -116,27 +116,14 @@ def qualify(inp: QualificationInput, config: QualificationConfig) -> Qualificati
|
||||
process="consultation_avis",
|
||||
recommended_modalities=[],
|
||||
recommend_onchain=_onchain(inp, reasons),
|
||||
onchain_reason=_onchain_reason(inp),
|
||||
confidence="required",
|
||||
collective_available=False,
|
||||
record_in_observatory=True,
|
||||
reasons=reasons,
|
||||
onchain_reason=_onchain_reason(inp),
|
||||
)
|
||||
|
||||
count = inp.affected_count if inp.affected_count is not None else 1
|
||||
|
||||
# ── R3: single person ───────────────────────────────────────────────────
|
||||
if count <= config.individual_max:
|
||||
reasons.append("Une seule personne concernée.")
|
||||
return QualificationResult(
|
||||
decision_type=DecisionType.INDIVIDUAL,
|
||||
process="personal",
|
||||
recommended_modalities=[],
|
||||
recommend_onchain=_onchain(inp, reasons),
|
||||
confidence="required",
|
||||
collective_available=False,
|
||||
reasons=reasons,
|
||||
onchain_reason=_onchain_reason(inp),
|
||||
)
|
||||
count = inp.affected_count if inp.affected_count is not None else 2
|
||||
|
||||
# ── R4: small group → individual recommended, collective available ───────
|
||||
if count <= config.small_group_max:
|
||||
@@ -144,32 +131,30 @@ def qualify(inp: QualificationInput, config: QualificationConfig) -> Qualificati
|
||||
f"{count} personnes concernées : décision individuelle recommandée, "
|
||||
"vote collectif possible."
|
||||
)
|
||||
modalities = _collect_modalities(inp, config)
|
||||
return QualificationResult(
|
||||
decision_type=DecisionType.INDIVIDUAL,
|
||||
process="personal",
|
||||
recommended_modalities=[],
|
||||
recommend_onchain=_onchain(inp, reasons),
|
||||
onchain_reason=_onchain_reason(inp),
|
||||
confidence="recommended",
|
||||
collective_available=True,
|
||||
record_in_observatory=False,
|
||||
reasons=reasons,
|
||||
onchain_reason=_onchain_reason(inp),
|
||||
)
|
||||
|
||||
# ── R5/R6: medium or large group → collective ────────────────────────────
|
||||
modalities = _collect_modalities(inp, config)
|
||||
|
||||
if count <= config.collective_wot_min:
|
||||
reasons.append(
|
||||
f"{count} personnes concernées : vote collectif recommandé."
|
||||
)
|
||||
reasons.append(f"{count} personnes concernées : vote collectif recommandé.")
|
||||
confidence = "recommended"
|
||||
else:
|
||||
reasons.append(
|
||||
f"{count} personnes concernées : vote collectif obligatoire "
|
||||
"(formule WoT applicable)."
|
||||
f"{count} personnes concernées : vote collectif recommandé "
|
||||
"(formule WoT applicable à cette échelle)."
|
||||
)
|
||||
confidence = "required"
|
||||
confidence = "recommended"
|
||||
if "vote_wot" not in modalities:
|
||||
modalities = ["vote_wot"] + modalities
|
||||
|
||||
@@ -178,10 +163,11 @@ def qualify(inp: QualificationInput, config: QualificationConfig) -> Qualificati
|
||||
process="vote_collective",
|
||||
recommended_modalities=modalities,
|
||||
recommend_onchain=_onchain(inp, reasons),
|
||||
onchain_reason=_onchain_reason(inp),
|
||||
confidence=confidence,
|
||||
collective_available=True,
|
||||
record_in_observatory=False,
|
||||
reasons=reasons,
|
||||
onchain_reason=_onchain_reason(inp),
|
||||
)
|
||||
|
||||
|
||||
@@ -210,10 +196,7 @@ def _onchain_reason(inp: QualificationInput) -> str | None:
|
||||
)
|
||||
|
||||
|
||||
def _collect_modalities(
|
||||
inp: QualificationInput,
|
||||
config: QualificationConfig,
|
||||
) -> list[str]:
|
||||
def _collect_modalities(inp: QualificationInput, config: QualificationConfig) -> list[str]:
|
||||
"""Combine default modalities with any LLM suggestions (stub for now)."""
|
||||
llm_suggestions = []
|
||||
if inp.context_description:
|
||||
|
||||
Reference in New Issue
Block a user