CUST-WP-0061-T06: retire suggestions to read-only legacy
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 19s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 1m4s

Founder-reviewed decision (WorkOrchestrationArchitectureDraft.md v0.2
section 8 item 6): the fresh intake work-record entity replaces
suggestions, not a rename-bridge. All 5 mutation endpoints (create, vet,
decline, promote, bump-relevance) now 410 with a pointer to POST
/intakes/ and the migration doc; GET/list stay live for the historical
record (10 rows migrated to file-backed intake records in the-custodian,
see that repo's intake-legacy-suggestions-migration.md and CUST-IN-0001
through CUST-IN-0010).

Removed dead code the retirement makes unreachable: Task/TaskPriority/
TaskStatus/normalize_task_status imports (only used by the deleted
promote body), the suggestion_relevance.bump_relevance import, and the
_ALLOWED_*_FROM stage-guard sets + _reject_stage helper (only used by
the deleted vet/decline/promote bodies). WSJF ranking (compute_wsjf,
cost_of_delay, suggestion_sort_key) stays -- still exercised by the
surviving GET /suggestions/?rank=wsjf read path.

MCP tool docstrings (create_suggestion, vet_suggestion,
decline_suggestion, promote_suggestion_to_task,
bump_suggestion_relevance) updated to point at the replacement
(create_intake/route_intake/close_intake) rather than silently 410ing
with no guidance.

tests/test_suggestions.py rewritten: verifies all 5 mutations 410,
GET/list still work for historical rows (seeded directly via the DB
session since creation is retired -- there's no other way to get
historical data into the table anymore), 404 still behaves normally on
unknown ids. Live-verified against the running dev API: POST 410s,
GET with include_terminal=true still returns all 10 migrated-and-declined
historical rows. No regressions: full repo suite green (563 tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-07-21 01:51:59 +02:00
parent b564ac7046
commit 6c4fc64ef3
3 changed files with 136 additions and 257 deletions

View file

@ -12,7 +12,6 @@ from api.models.suggestion import (
SuggestionNote,
SuggestionStage,
)
from api.models.task import Task, TaskPriority, TaskStatus
from api.schemas.suggestion import (
SuggestionBumpRelevance,
SuggestionCreate,
@ -22,16 +21,25 @@ from api.schemas.suggestion import (
SuggestionRead,
SuggestionVet,
)
from api.services.suggestion_relevance import bump_relevance
from api.services.suggestion_wsjf import compute_wsjf, cost_of_delay, suggestion_sort_key
from api.task_status import normalize_task_status
router = APIRouter(prefix="/suggestions", tags=["suggestions"])
_ALLOWED_VET_FROM = {SuggestionStage.suggestion}
_ALLOWED_DECLINE_FROM = {SuggestionStage.suggestion, SuggestionStage.requirement}
_ALLOWED_PROMOTE_FROM = {SuggestionStage.requirement}
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(
@ -66,14 +74,6 @@ async def _get_suggestion_or_404(
return suggestion
def _reject_stage(suggestion: Suggestion, allowed: set[SuggestionStage], action: str) -> None:
if suggestion.stage not in allowed:
raise HTTPException(
status_code=409,
detail=f"Cannot {action} suggestion in stage '{suggestion.stage.value}'",
)
@router.get("/", response_model=list[SuggestionRead])
async def list_suggestions(
domain: str | None = None,
@ -105,23 +105,13 @@ async def create_suggestion(
body: SuggestionCreate,
session: AsyncSession = Depends(get_session),
) -> SuggestionRead:
domain_id = await _resolve_domain_id(body.domain, session)
suggestion = Suggestion(
domain_id=domain_id,
topic_id=body.topic_id,
workplan_id=body.workplan_id,
title=body.title,
description=body.description,
origin=body.origin,
origin_ref=body.origin_ref,
base_value=body.base_value,
job_size=body.job_size,
relevance_weight=body.relevance_weight,
)
session.add(suggestion)
await session.commit()
await session.refresh(suggestion)
return _enrich_read(suggestion)
"""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."""
raise _retired()
@router.get("/{suggestion_id}", response_model=SuggestionRead)
@ -139,28 +129,8 @@ async def vet_suggestion(
body: SuggestionVet,
session: AsyncSession = Depends(get_session),
) -> SuggestionRead:
suggestion = await _get_suggestion_or_404(suggestion_id, session)
_reject_stage(suggestion, _ALLOWED_VET_FROM, "vet")
suggestion.stage = SuggestionStage.requirement
if body.base_value is not None:
suggestion.base_value = body.base_value
if body.job_size is not None:
suggestion.job_size = body.job_size
if body.relevance_weight is not None:
suggestion.relevance_weight = body.relevance_weight
if body.workplan_id is not None:
suggestion.workplan_id = body.workplan_id
session.add(
SuggestionNote(
suggestion_id=suggestion.id,
stage=SuggestionStage.requirement.value,
author=body.author,
content=body.note,
)
)
await session.commit()
await session.refresh(suggestion)
return _enrich_read(suggestion)
"""Retired (CUST-WP-0061-T06). See _retired()."""
raise _retired()
@router.post("/{suggestion_id}/decline", response_model=SuggestionRead)
@ -169,20 +139,8 @@ async def decline_suggestion(
body: SuggestionDecline,
session: AsyncSession = Depends(get_session),
) -> SuggestionRead:
suggestion = await _get_suggestion_or_404(suggestion_id, session)
_reject_stage(suggestion, _ALLOWED_DECLINE_FROM, "decline")
suggestion.stage = SuggestionStage.declined
session.add(
SuggestionNote(
suggestion_id=suggestion.id,
stage=SuggestionStage.declined.value,
author=body.author,
content=body.note,
)
)
await session.commit()
await session.refresh(suggestion)
return _enrich_read(suggestion)
"""Retired (CUST-WP-0061-T06). See _retired()."""
raise _retired()
@router.post("/{suggestion_id}/promote", response_model=SuggestionRead)
@ -191,38 +149,8 @@ async def promote_suggestion_to_task(
body: SuggestionPromote,
session: AsyncSession = Depends(get_session),
) -> SuggestionRead:
suggestion = await _get_suggestion_or_404(suggestion_id, session)
_reject_stage(suggestion, _ALLOWED_PROMOTE_FROM, "promote")
if suggestion.workplan_id is None:
raise HTTPException(
status_code=409,
detail="Suggestion must have workplan_id before promotion",
)
task = Task(
workplan_id=suggestion.workplan_id,
title=body.task_title or suggestion.title,
description=body.task_description or suggestion.description,
status=TaskStatus(normalize_task_status(body.task_status)),
priority=TaskPriority(body.task_priority),
)
session.add(task)
await session.flush()
suggestion.stage = SuggestionStage.promoted
suggestion.promoted_task_id = task.id
if body.note:
session.add(
SuggestionNote(
suggestion_id=suggestion.id,
stage=SuggestionStage.promoted.value,
author=body.author,
content=body.note,
)
)
await session.commit()
await session.refresh(suggestion)
return _enrich_read(suggestion)
"""Retired (CUST-WP-0061-T06). See _retired()."""
raise _retired()
@router.post("/{suggestion_id}/bump-relevance", response_model=SuggestionRead)
@ -231,18 +159,8 @@ async def bump_suggestion_relevance(
body: SuggestionBumpRelevance,
session: AsyncSession = Depends(get_session),
) -> SuggestionRead:
suggestion = await _get_suggestion_or_404(suggestion_id, session)
source_key = body.author or "explicit"
await bump_relevance(
session,
suggestion,
source="explicit",
source_key=source_key,
reason=body.reason,
)
await session.commit()
await session.refresh(suggestion)
return _enrich_read(suggestion)
"""Retired (CUST-WP-0061-T06). See _retired()."""
raise _retired()
@router.get("/{suggestion_id}/notes", response_model=list[SuggestionNoteRead])