feat: add lead policy helpers
This commit is contained in:
27
backend/app/services/lead_policy.py
Normal file
27
backend/app/services/lead_policy.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
from typing import Mapping
|
||||||
|
|
||||||
|
CONFIDENCE_THRESHOLD = 80
|
||||||
|
|
||||||
|
|
||||||
|
def compute_confidence(rubric_scores: Mapping[str, int]) -> int:
|
||||||
|
return int(sum(rubric_scores.values()))
|
||||||
|
|
||||||
|
|
||||||
|
def approval_required(*, confidence: int, is_external: bool, is_risky: bool) -> bool:
|
||||||
|
return is_external or is_risky or confidence < CONFIDENCE_THRESHOLD
|
||||||
|
|
||||||
|
|
||||||
|
def infer_planning(signals: Mapping[str, bool]) -> bool:
|
||||||
|
# Require at least two planning signals to avoid spam on general boards.
|
||||||
|
truthy = [key for key, value in signals.items() if value]
|
||||||
|
return len(truthy) >= 2
|
||||||
|
|
||||||
|
|
||||||
|
def task_fingerprint(title: str, description: str | None, board_id: str) -> str:
|
||||||
|
normalized_title = title.strip().lower()
|
||||||
|
normalized_desc = (description or "").strip().lower()
|
||||||
|
seed = f"{board_id}::{normalized_title}::{normalized_desc}"
|
||||||
|
return hashlib.sha256(seed.encode()).hexdigest()
|
||||||
6
backend/tests/conftest.py
Normal file
6
backend/tests/conftest.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
if str(ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
50
backend/tests/test_lead_policy.py
Normal file
50
backend/tests/test_lead_policy.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import hashlib
|
||||||
|
|
||||||
|
from app.services.lead_policy import (
|
||||||
|
approval_required,
|
||||||
|
compute_confidence,
|
||||||
|
infer_planning,
|
||||||
|
task_fingerprint,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_compute_confidence_sums_weights():
|
||||||
|
rubric = {
|
||||||
|
"clarity": 20,
|
||||||
|
"constraints": 15,
|
||||||
|
"completeness": 10,
|
||||||
|
"risk": 20,
|
||||||
|
"dependencies": 10,
|
||||||
|
"similarity": 5,
|
||||||
|
}
|
||||||
|
assert compute_confidence(rubric) == 80
|
||||||
|
|
||||||
|
|
||||||
|
def test_approval_required_for_low_confidence():
|
||||||
|
assert approval_required(confidence=79, is_external=False, is_risky=False)
|
||||||
|
assert not approval_required(confidence=85, is_external=False, is_risky=False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_approval_required_for_external_or_risky():
|
||||||
|
assert approval_required(confidence=90, is_external=True, is_risky=False)
|
||||||
|
assert approval_required(confidence=90, is_external=False, is_risky=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_infer_planning_requires_signal_threshold():
|
||||||
|
signals = {
|
||||||
|
"goal_gap": True,
|
||||||
|
"recent_ambiguity": False,
|
||||||
|
"research_only": False,
|
||||||
|
"stalled_inbox": False,
|
||||||
|
}
|
||||||
|
assert infer_planning(signals) is False
|
||||||
|
|
||||||
|
signals["recent_ambiguity"] = True
|
||||||
|
assert infer_planning(signals) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_task_fingerprint_deterministic():
|
||||||
|
fp1 = task_fingerprint("Title", "Desc", "board-1")
|
||||||
|
fp2 = task_fingerprint("Title", "Desc", "board-1")
|
||||||
|
assert fp1 == fp2
|
||||||
|
assert fp1 == hashlib.sha256("board-1::title::desc".encode()).hexdigest()
|
||||||
Reference in New Issue
Block a user