feat(scoring): composite recency decay and severity weights persisted in DB [FASE-5.1]
This commit is contained in:
@@ -1,14 +1,8 @@
|
||||
"""Scoring configuration persistence service.
|
||||
|
||||
Reads and writes scoring weights from the ``scoring_config`` table.
|
||||
Falls back to environment-variable defaults (from ``Settings``) when
|
||||
no row has been persisted yet.
|
||||
|
||||
This module is framework-agnostic: no FastAPI imports.
|
||||
"""
|
||||
"""Scoring configuration persistence service."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -18,29 +12,39 @@ from app.domain.value_objects.scoring_weights import ScoringWeights
|
||||
from app.models.scoring_config import ScoringConfig
|
||||
|
||||
|
||||
def get_scoring_weights(db: Session) -> ScoringWeights:
|
||||
"""Return the active scoring weights.
|
||||
def _row_recency(row: ScoringConfig) -> float:
|
||||
return float(getattr(row, "weight_recency", None) or getattr(row, "weight_freshness", 10.0))
|
||||
|
||||
Reads the single ``scoring_config`` row. If the table is empty
|
||||
(first run or migration just applied), falls back to the values
|
||||
from the environment / ``Settings``.
|
||||
"""
|
||||
|
||||
def _row_severity(row: ScoringConfig) -> float:
|
||||
return float(
|
||||
getattr(row, "weight_severity", None)
|
||||
or getattr(row, "weight_platform_diversity", 10.0)
|
||||
)
|
||||
|
||||
|
||||
def get_scoring_weights(db: Session) -> ScoringWeights:
|
||||
"""Return the active scoring weights from the database or env defaults."""
|
||||
row = db.query(ScoringConfig).first()
|
||||
if row is not None:
|
||||
return ScoringWeights(
|
||||
tests=row.weight_tests,
|
||||
detection_rules=row.weight_detection_rules,
|
||||
d3fend=row.weight_d3fend,
|
||||
freshness=row.weight_freshness,
|
||||
platform_diversity=row.weight_platform_diversity,
|
||||
recency=_row_recency(row),
|
||||
severity=_row_severity(row),
|
||||
)
|
||||
|
||||
return ScoringWeights(
|
||||
tests=float(settings.SCORING_WEIGHT_TESTS),
|
||||
detection_rules=float(settings.SCORING_WEIGHT_DETECTION_RULES),
|
||||
d3fend=float(settings.SCORING_WEIGHT_D3FEND),
|
||||
freshness=float(settings.SCORING_WEIGHT_FRESHNESS),
|
||||
platform_diversity=float(settings.SCORING_WEIGHT_PLATFORM_DIVERSITY),
|
||||
recency=float(
|
||||
getattr(settings, "SCORING_WEIGHT_RECENCY", settings.SCORING_WEIGHT_FRESHNESS)
|
||||
),
|
||||
severity=float(
|
||||
getattr(settings, "SCORING_WEIGHT_SEVERITY", settings.SCORING_WEIGHT_PLATFORM_DIVERSITY)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -50,25 +54,26 @@ def update_scoring_weights(
|
||||
tests: float | None = None,
|
||||
detection_rules: float | None = None,
|
||||
d3fend: float | None = None,
|
||||
recency: float | None = None,
|
||||
severity: float | None = None,
|
||||
freshness: float | None = None,
|
||||
platform_diversity: float | None = None,
|
||||
updated_by: uuid.UUID | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Upsert scoring weights into the database.
|
||||
"""Upsert scoring weights. Does not commit."""
|
||||
if freshness is not None and recency is None:
|
||||
recency = freshness
|
||||
if platform_diversity is not None and severity is None:
|
||||
severity = platform_diversity
|
||||
|
||||
Only provided fields are overwritten; ``None`` values keep the
|
||||
current (or default) value. Validates via ``ScoringWeights``
|
||||
before persisting.
|
||||
|
||||
Returns a dict with ``weights`` and ``total``.
|
||||
"""
|
||||
current = get_scoring_weights(db)
|
||||
|
||||
new = ScoringWeights(
|
||||
tests=tests if tests is not None else current.tests,
|
||||
detection_rules=detection_rules if detection_rules is not None else current.detection_rules,
|
||||
d3fend=d3fend if d3fend is not None else current.d3fend,
|
||||
freshness=freshness if freshness is not None else current.freshness,
|
||||
platform_diversity=platform_diversity if platform_diversity is not None else current.platform_diversity,
|
||||
recency=recency if recency is not None else current.recency,
|
||||
severity=severity if severity is not None else current.severity,
|
||||
)
|
||||
|
||||
row = db.query(ScoringConfig).first()
|
||||
@@ -79,10 +84,17 @@ def update_scoring_weights(
|
||||
row.weight_tests = new.tests
|
||||
row.weight_detection_rules = new.detection_rules
|
||||
row.weight_d3fend = new.d3fend
|
||||
row.weight_freshness = new.freshness
|
||||
row.weight_platform_diversity = new.platform_diversity
|
||||
if hasattr(row, "weight_recency"):
|
||||
row.weight_recency = new.recency
|
||||
elif hasattr(row, "weight_freshness"):
|
||||
row.weight_freshness = new.recency
|
||||
if hasattr(row, "weight_severity"):
|
||||
row.weight_severity = new.severity
|
||||
elif hasattr(row, "weight_platform_diversity"):
|
||||
row.weight_platform_diversity = new.severity
|
||||
if updated_by is not None and hasattr(row, "updated_by"):
|
||||
row.updated_by = updated_by
|
||||
|
||||
# Does not commit; caller (router) uses UnitOfWork.
|
||||
return _weights_dict(new)
|
||||
|
||||
|
||||
@@ -96,10 +108,15 @@ def _weights_dict(w: ScoringWeights) -> dict[str, Any]:
|
||||
"tests": w.tests,
|
||||
"detection_rules": w.detection_rules,
|
||||
"d3fend": w.d3fend,
|
||||
"freshness": w.freshness,
|
||||
"platform_diversity": w.platform_diversity,
|
||||
"recency": w.recency,
|
||||
"severity": w.severity,
|
||||
# Legacy keys for older clients
|
||||
"freshness": w.recency,
|
||||
"platform_diversity": w.severity,
|
||||
}
|
||||
return {
|
||||
"weights": weights,
|
||||
"total": sum(weights.values()),
|
||||
"total": sum(
|
||||
[w.tests, w.detection_rules, w.d3fend, w.recency, w.severity]
|
||||
),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user