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:
2026-07-06 10:52:49 +02:00
parent cac9a6b1e0
commit f2e042a278
31 changed files with 1537 additions and 66 deletions

View 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

View 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())

View File

@@ -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"),
)