generated from coulomb/repo-seed
Finish CUST-WP-0011 and implement STATE-WP-0061 suggestion backlog.
Add persisted Suggestion entities with WSJF ranking, relevance bumps, REST/MCP write surfaces, /suggestions dashboard page, and daily triage digest integration. Document the cluster operating model, archive the completed migration workplan, and seed WARDEN-WP-0012 routing scenarios.
This commit is contained in:
118
api/services/suggestion_relevance.py
Normal file
118
api/services/suggestion_relevance.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from api.models.suggestion import (
|
||||
OPEN_SUGGESTION_STAGES,
|
||||
Suggestion,
|
||||
SuggestionRelevanceBump,
|
||||
SuggestionStage,
|
||||
)
|
||||
|
||||
DEBOUNCE_SECONDS = 3600
|
||||
|
||||
|
||||
async def bump_relevance(
|
||||
session: AsyncSession,
|
||||
suggestion: Suggestion,
|
||||
*,
|
||||
source: str,
|
||||
source_key: str,
|
||||
reason: str | None = None,
|
||||
) -> bool:
|
||||
"""Increment relevance when a suggestion is needed-but-unmet.
|
||||
|
||||
Returns True when the counter was incremented, False when debounced.
|
||||
"""
|
||||
if suggestion.stage not in OPEN_SUGGESTION_STAGES:
|
||||
return False
|
||||
|
||||
cutoff = datetime.now(tz=timezone.utc) - timedelta(seconds=DEBOUNCE_SECONDS)
|
||||
existing = (
|
||||
await session.execute(
|
||||
select(SuggestionRelevanceBump.id)
|
||||
.where(SuggestionRelevanceBump.suggestion_id == suggestion.id)
|
||||
.where(SuggestionRelevanceBump.source == source)
|
||||
.where(SuggestionRelevanceBump.source_key == source_key)
|
||||
.where(SuggestionRelevanceBump.created_at >= cutoff)
|
||||
.limit(1)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if existing is not None:
|
||||
return False
|
||||
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
suggestion.relevance += 1
|
||||
suggestion.relevance_events += 1
|
||||
suggestion.last_requested_at = now
|
||||
session.add(
|
||||
SuggestionRelevanceBump(
|
||||
suggestion_id=suggestion.id,
|
||||
source=source,
|
||||
source_key=source_key,
|
||||
reason=reason,
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def bump_matching_for_capability_request(
|
||||
session: AsyncSession,
|
||||
*,
|
||||
request_id: uuid.UUID,
|
||||
title: str,
|
||||
description: str,
|
||||
capability_type: str,
|
||||
catalog_entry_id: uuid.UUID | None = None,
|
||||
) -> int:
|
||||
"""Bump open suggestions that match an unfulfilled capability need."""
|
||||
rows = (
|
||||
await session.execute(
|
||||
select(Suggestion).where(Suggestion.stage.in_(OPEN_SUGGESTION_STAGES))
|
||||
)
|
||||
).scalars().all()
|
||||
|
||||
haystack = f"{title} {description} {capability_type}".lower()
|
||||
bumped = 0
|
||||
for suggestion in rows:
|
||||
matched = False
|
||||
if suggestion.origin_ref:
|
||||
ref = suggestion.origin_ref.lower()
|
||||
if ref in haystack or any(token in haystack for token in ref.split("-") if len(token) > 4):
|
||||
matched = True
|
||||
if not matched and suggestion.title.lower() in haystack:
|
||||
matched = True
|
||||
if not matched:
|
||||
continue
|
||||
if await bump_relevance(
|
||||
session,
|
||||
suggestion,
|
||||
source="capability_request",
|
||||
source_key=str(request_id),
|
||||
reason=f"Capability request matched: {title}",
|
||||
):
|
||||
bumped += 1
|
||||
return bumped
|
||||
|
||||
|
||||
async def bump_suggestions_for_next_steps(
|
||||
session: AsyncSession,
|
||||
suggestions: list[Suggestion],
|
||||
) -> int:
|
||||
"""Bump only suggestions surfaced in a next-steps response."""
|
||||
bucket = datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H")
|
||||
bumped = 0
|
||||
for suggestion in suggestions:
|
||||
if await bump_relevance(
|
||||
session,
|
||||
suggestion,
|
||||
source="next_steps",
|
||||
source_key=bucket,
|
||||
reason="Surfaced during get_next_steps lookup",
|
||||
):
|
||||
bumped += 1
|
||||
return bumped
|
||||
17
api/services/suggestion_wsjf.py
Normal file
17
api/services/suggestion_wsjf.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from api.models.suggestion import Suggestion
|
||||
|
||||
|
||||
def cost_of_delay(suggestion: Suggestion) -> float:
|
||||
return suggestion.base_value + (suggestion.relevance_weight * suggestion.relevance)
|
||||
|
||||
|
||||
def compute_wsjf(suggestion: Suggestion) -> float:
|
||||
size = suggestion.job_size if suggestion.job_size > 0 else 3.0
|
||||
return round(cost_of_delay(suggestion) / size, 2)
|
||||
|
||||
|
||||
def suggestion_sort_key(suggestion: Suggestion) -> tuple[float, int, str]:
|
||||
"""Higher WSJF first; tie-break on relevance then title."""
|
||||
return (-compute_wsjf(suggestion), -suggestion.relevance, suggestion.title.lower())
|
||||
@@ -45,6 +45,11 @@ WRITE_ROUTE_RULES: tuple[WriteRouteRule, ...] = (
|
||||
WriteRouteRule("POST", r"/decisions/[^/]+/resolve", "replace", "resolve decision"),
|
||||
WriteRouteRule("PATCH", r"/workplans/[^/]+", "replace", "update workplan"),
|
||||
WriteRouteRule("PATCH", r"/workstreams/[^/]+", "replace", "update legacy workstream alias"),
|
||||
WriteRouteRule("POST", r"/suggestions", "append", "create suggestion"),
|
||||
WriteRouteRule("POST", r"/suggestions/[^/]+/vet", "replace", "vet suggestion"),
|
||||
WriteRouteRule("POST", r"/suggestions/[^/]+/decline", "replace", "decline suggestion"),
|
||||
WriteRouteRule("POST", r"/suggestions/[^/]+/promote", "replace", "promote suggestion to task"),
|
||||
WriteRouteRule("POST", r"/suggestions/[^/]+/bump-relevance", "append", "bump suggestion relevance"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user