From 78e89427ba91e63f4d731b2e641b91e6fd36c56f Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 5 Feb 2026 14:37:12 +0530 Subject: [PATCH] feat: add lead policy helpers --- backend/app/services/lead_policy.py | 27 ++++++++++++++++ backend/tests/conftest.py | 6 ++++ backend/tests/test_lead_policy.py | 50 +++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 backend/app/services/lead_policy.py create mode 100644 backend/tests/conftest.py create mode 100644 backend/tests/test_lead_policy.py diff --git a/backend/app/services/lead_policy.py b/backend/app/services/lead_policy.py new file mode 100644 index 00000000..28885aff --- /dev/null +++ b/backend/app/services/lead_policy.py @@ -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() diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py new file mode 100644 index 00000000..2a855d9f --- /dev/null +++ b/backend/tests/conftest.py @@ -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)) diff --git a/backend/tests/test_lead_policy.py b/backend/tests/test_lead_policy.py new file mode 100644 index 00000000..8bc771a4 --- /dev/null +++ b/backend/tests/test_lead_policy.py @@ -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()