feat(STATE-WP-0079): retire the suggestion-backlog surface (slice E1)
History is archived fleet-side, so the read surface has no remaining job. - api/routers/suggestions.py: whole prefix 410s with a pointer to intake and to the archive; the router drops from 176 lines to a stub - mcp_server: the 6 suggestion tools removed outright rather than stubbed -- a retired tool that still appears in the tool list costs every agent session context on every call, which is the opposite of retiring it - write_idempotency: 5 /suggestions rules dropped - dashboard: suggestions.md deleted, nav entry removed, reference.md and wsjf-triage.md updated; docs/suggestions.md rewritten as archive pointer - tests: two tests pinned the old read-live behaviour and now pin the retirement contract instead Tables stay: they are retire/archive in SHR-INV-0001 and are captured by the final dump at T06. Untouched, and worth knowing during cutover: ui-feedback.md / todo.md 'suggestions' are Shift+click dashboard feedback backed by technical_debt, a different entity that shares the word. E3 (dashboard-meta) is that page; its owner is state-hub-until-cutover so it retires at the T06 window, not now. Full suite 612 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
98af485cd5
commit
fb363b37d3
13 changed files with 148 additions and 447 deletions
|
|
@ -1,177 +1,45 @@
|
|||
import uuid
|
||||
"""Retired: the suggestion backlog.
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
Slice E1 of the State Hub retirement (`STATE-WP-0079-T05`). The capability was
|
||||
superseded by the **intake** work-record entity; mutations were retired
|
||||
2026-07-21 under `CUST-WP-0061-T06`, and the reads were kept live only so the
|
||||
historical record stayed reachable.
|
||||
|
||||
from api.database import get_session
|
||||
from api.models.domain import Domain
|
||||
from api.models.suggestion import (
|
||||
OPEN_SUGGESTION_STAGES,
|
||||
Suggestion,
|
||||
SuggestionNote,
|
||||
SuggestionStage,
|
||||
)
|
||||
from api.schemas.suggestion import (
|
||||
SuggestionBumpRelevance,
|
||||
SuggestionCreate,
|
||||
SuggestionDecline,
|
||||
SuggestionNoteRead,
|
||||
SuggestionPromote,
|
||||
SuggestionRead,
|
||||
SuggestionVet,
|
||||
)
|
||||
from api.services.suggestion_wsjf import compute_wsjf, cost_of_delay, suggestion_sort_key
|
||||
That history is now archived at
|
||||
`the-custodian/docs/archived-suggestion-backlog.md` — all 10 suggestions, 10
|
||||
notes and 5 relevance bumps, every one closed as `declined` during the intake
|
||||
migration and none promoted. With a durable record outside this repo, the read
|
||||
surface has no remaining job, so the whole router answers 410.
|
||||
|
||||
The `suggestions`, `suggestion_notes` and `suggestion_relevance_bumps` tables
|
||||
are deliberately left in place: they are `retire`/`archive` in `SHR-INV-0001`
|
||||
and are captured by the final dump at `STATE-WP-0079-T06`. Dropping them here
|
||||
would remove data ahead of the dump for no gain.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
router = APIRouter(prefix="/suggestions", tags=["suggestions"])
|
||||
|
||||
_DETAIL = (
|
||||
"suggestions are retired (STATE-WP-0079-T05, slice E1). "
|
||||
"Use POST /intakes/ for new discovery work. The historical backlog is "
|
||||
"archived at the-custodian/docs/archived-suggestion-backlog.md — see also "
|
||||
"the-custodian/intake-legacy-suggestions-migration.md and "
|
||||
"canon/standards/work-record-types_v0.1.md."
|
||||
)
|
||||
|
||||
|
||||
def _retired() -> HTTPException:
|
||||
"""CUST-WP-0061-T06 (2026-07-21): suggestions are read-only legacy.
|
||||
Reads (GET) stay live for the historical record; every mutation is
|
||||
retired. Use the intake work-record entity instead
|
||||
(canon/standards/work-record-types_v0.1.md)."""
|
||||
return HTTPException(
|
||||
status_code=410,
|
||||
detail=(
|
||||
"suggestions are retired (read-only legacy, CUST-WP-0061-T06). "
|
||||
"Use POST /intakes/ instead — see canon/standards/"
|
||||
"work-record-types_v0.1.md and the-custodian/"
|
||||
"intake-legacy-suggestions-migration.md."
|
||||
),
|
||||
)
|
||||
|
||||
async def _resolve_domain_id(slug: str, session: AsyncSession) -> uuid.UUID:
|
||||
row = await session.execute(
|
||||
select(Domain.id).where(Domain.slug == slug, Domain.status == "active")
|
||||
)
|
||||
domain_id = row.scalar_one_or_none()
|
||||
if domain_id is None:
|
||||
valid = [r[0] for r in (await session.execute(
|
||||
select(Domain.slug).where(Domain.status == "active")
|
||||
)).all()]
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=f"Unknown domain '{slug}'. Valid domains: {sorted(valid)}",
|
||||
)
|
||||
return domain_id
|
||||
return HTTPException(status_code=410, detail=_DETAIL)
|
||||
|
||||
|
||||
def _enrich_read(suggestion: Suggestion) -> SuggestionRead:
|
||||
data = SuggestionRead.model_validate(suggestion)
|
||||
data.cost_of_delay = cost_of_delay(suggestion)
|
||||
data.wsjf = compute_wsjf(suggestion)
|
||||
return data
|
||||
|
||||
|
||||
async def _get_suggestion_or_404(
|
||||
suggestion_id: uuid.UUID,
|
||||
session: AsyncSession,
|
||||
) -> Suggestion:
|
||||
suggestion = await session.get(Suggestion, suggestion_id)
|
||||
if suggestion is None:
|
||||
raise HTTPException(status_code=404, detail="Suggestion not found")
|
||||
return suggestion
|
||||
|
||||
|
||||
@router.get("/", response_model=list[SuggestionRead])
|
||||
async def list_suggestions(
|
||||
domain: str | None = None,
|
||||
stage: SuggestionStage | None = None,
|
||||
include_terminal: bool = Query(False),
|
||||
rank: str | None = Query(None),
|
||||
limit: int = Query(100, ge=1, le=500),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> list[SuggestionRead]:
|
||||
q = select(Suggestion)
|
||||
if domain:
|
||||
domain_id = await _resolve_domain_id(domain, session)
|
||||
q = q.where(Suggestion.domain_id == domain_id)
|
||||
if stage:
|
||||
q = q.where(Suggestion.stage == stage)
|
||||
elif not include_terminal:
|
||||
q = q.where(Suggestion.stage.in_(OPEN_SUGGESTION_STAGES))
|
||||
result = await session.execute(q)
|
||||
suggestions = list(result.scalars().all())
|
||||
if rank == "wsjf":
|
||||
suggestions.sort(key=suggestion_sort_key)
|
||||
else:
|
||||
suggestions.sort(key=lambda s: s.created_at)
|
||||
return [_enrich_read(s) for s in suggestions[:limit]]
|
||||
|
||||
|
||||
@router.post("/", response_model=SuggestionRead, status_code=status.HTTP_201_CREATED)
|
||||
async def create_suggestion(
|
||||
body: SuggestionCreate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> SuggestionRead:
|
||||
"""Retired (CUST-WP-0061-T06, 2026-07-21): suggestions are read-only
|
||||
legacy. Use `POST /intakes/` (kind: intake per
|
||||
canon/standards/work-record-types_v0.1.md) instead — a fresh entity,
|
||||
not a rename of this table. The 10 records that were still open at
|
||||
retirement were migrated to file-backed intake records; see
|
||||
the-custodian/intake-legacy-suggestions-migration.md."""
|
||||
@router.api_route(
|
||||
"/{path:path}",
|
||||
methods=["GET", "POST", "PATCH", "PUT", "DELETE"],
|
||||
include_in_schema=False,
|
||||
)
|
||||
@router.api_route("/", methods=["GET", "POST", "PATCH", "PUT", "DELETE"])
|
||||
async def suggestions_retired(path: str = "") -> None:
|
||||
"""Every suggestion route is retired; see module docstring."""
|
||||
raise _retired()
|
||||
|
||||
|
||||
@router.get("/{suggestion_id}", response_model=SuggestionRead)
|
||||
async def get_suggestion(
|
||||
suggestion_id: uuid.UUID,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> SuggestionRead:
|
||||
suggestion = await _get_suggestion_or_404(suggestion_id, session)
|
||||
return _enrich_read(suggestion)
|
||||
|
||||
|
||||
@router.post("/{suggestion_id}/vet", response_model=SuggestionRead)
|
||||
async def vet_suggestion(
|
||||
suggestion_id: uuid.UUID,
|
||||
body: SuggestionVet,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> SuggestionRead:
|
||||
"""Retired (CUST-WP-0061-T06). See _retired()."""
|
||||
raise _retired()
|
||||
|
||||
|
||||
@router.post("/{suggestion_id}/decline", response_model=SuggestionRead)
|
||||
async def decline_suggestion(
|
||||
suggestion_id: uuid.UUID,
|
||||
body: SuggestionDecline,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> SuggestionRead:
|
||||
"""Retired (CUST-WP-0061-T06). See _retired()."""
|
||||
raise _retired()
|
||||
|
||||
|
||||
@router.post("/{suggestion_id}/promote", response_model=SuggestionRead)
|
||||
async def promote_suggestion_to_task(
|
||||
suggestion_id: uuid.UUID,
|
||||
body: SuggestionPromote,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> SuggestionRead:
|
||||
"""Retired (CUST-WP-0061-T06). See _retired()."""
|
||||
raise _retired()
|
||||
|
||||
|
||||
@router.post("/{suggestion_id}/bump-relevance", response_model=SuggestionRead)
|
||||
async def bump_suggestion_relevance(
|
||||
suggestion_id: uuid.UUID,
|
||||
body: SuggestionBumpRelevance,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> SuggestionRead:
|
||||
"""Retired (CUST-WP-0061-T06). See _retired()."""
|
||||
raise _retired()
|
||||
|
||||
|
||||
@router.get("/{suggestion_id}/notes", response_model=list[SuggestionNoteRead])
|
||||
async def list_suggestion_notes(
|
||||
suggestion_id: uuid.UUID,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> list[SuggestionNote]:
|
||||
await _get_suggestion_or_404(suggestion_id, session)
|
||||
result = await session.execute(
|
||||
select(SuggestionNote)
|
||||
.where(SuggestionNote.suggestion_id == suggestion_id)
|
||||
.order_by(SuggestionNote.created_at)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
|
@ -45,11 +45,6 @@ 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"),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue