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
|
|
@ -48,8 +48,8 @@
|
|||
| workplan | STATE-WP-0076 | finished | — | workplans/STATE-WP-0076-definition-of-ready-and-comprehension.md |
|
||||
| workplan | STATE-WP-0077 | finished | — | workplans/STATE-WP-0077-dox-assessment-recording-and-soft-visibility.md |
|
||||
| workplan | STATE-WP-0078 | finished | — | workplans/STATE-WP-0078-ops-run-read-projection.md |
|
||||
| workplan | STATE-WP-0079 | proposed | — | workplans/STATE-WP-0079-retirement-strangler.md |
|
||||
| workplan | STATE-WP-0080 | active | — | workplans/STATE-WP-0080-register-project-flavor-awareness.md |
|
||||
| workplan | STATE-WP-0079 | active | — | workplans/STATE-WP-0079-retirement-strangler.md |
|
||||
| workplan | STATE-WP-0080 | finished | — | workplans/STATE-WP-0080-register-project-flavor-awareness.md |
|
||||
| task | ADHOC-2026-06-04-T01 | done | — | workplans/ADHOC-2026-06-04.md |
|
||||
| task | ADHOC-2026-07-01-T01 | done | — | workplans/ADHOC-2026-07-01.md |
|
||||
| task | ADHOC-2026-07-01-T02 | done | — | workplans/ADHOC-2026-07-01.md |
|
||||
|
|
@ -274,8 +274,8 @@
|
|||
| task | STATE-WP-0078-T01 | done | — | workplans/STATE-WP-0078-ops-run-read-projection.md |
|
||||
| task | STATE-WP-0078-T02 | done | — | workplans/STATE-WP-0078-ops-run-read-projection.md |
|
||||
| task | STATE-WP-0078-T03 | done | — | workplans/STATE-WP-0078-ops-run-read-projection.md |
|
||||
| task | STATE-WP-0079-T01 | todo | — | workplans/STATE-WP-0079-retirement-strangler.md |
|
||||
| task | STATE-WP-0079-T02 | todo | — | workplans/STATE-WP-0079-retirement-strangler.md |
|
||||
| task | STATE-WP-0079-T01 | done | — | workplans/STATE-WP-0079-retirement-strangler.md |
|
||||
| task | STATE-WP-0079-T02 | done | — | workplans/STATE-WP-0079-retirement-strangler.md |
|
||||
| task | STATE-WP-0079-T03 | done | — | workplans/STATE-WP-0079-retirement-strangler.md |
|
||||
| task | STATE-WP-0079-T04 | todo | — | workplans/STATE-WP-0079-retirement-strangler.md |
|
||||
| task | STATE-WP-0079-T05 | todo | — | workplans/STATE-WP-0079-retirement-strangler.md |
|
||||
|
|
@ -283,7 +283,7 @@
|
|||
| task | STATE-WP-0080-T01 | done | — | workplans/STATE-WP-0080-register-project-flavor-awareness.md |
|
||||
| task | STATE-WP-0080-T02 | done | — | workplans/STATE-WP-0080-register-project-flavor-awareness.md |
|
||||
| task | STATE-WP-0080-T03 | done | — | workplans/STATE-WP-0080-register-project-flavor-awareness.md |
|
||||
| task | STATE-WP-0080-T04 | wait | — | workplans/STATE-WP-0080-register-project-flavor-awareness.md |
|
||||
| task | STATE-WP-0080-T04 | cancel | — | workplans/STATE-WP-0080-register-project-flavor-awareness.md |
|
||||
| task | STATE-WP-0080-T05 | cancel | — | workplans/STATE-WP-0080-register-project-flavor-awareness.md |
|
||||
| task | STATE-WP-0080-T06 | cancel | — | workplans/STATE-WP-0080-register-project-flavor-awareness.md |
|
||||
| task | STATE-WP-0080-T07 | cancel | — | workplans/STATE-WP-0080-register-project-flavor-awareness.md |
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,6 @@ export default {
|
|||
{ name: "Interventions", path: "/interventions" },
|
||||
{ name: "Tasks", path: "/tasks" },
|
||||
{ name: "UI Feedback", path: "/ui-feedback" },
|
||||
{ name: "Suggestions (legacy)", path: "/suggestions" },
|
||||
{ name: "WSJF Triage", path: "/wsjf-triage" },
|
||||
],
|
||||
},
|
||||
|
|
@ -120,7 +119,7 @@ export default {
|
|||
{ name: "SBOM", path: "/docs/sbom" },
|
||||
{ name: "SCOPE.md", path: "/docs/scope" },
|
||||
{ name: "Service Catalog", path: "/docs/services" },
|
||||
{ name: "Suggestions (legacy)", path: "/docs/suggestions" },
|
||||
{ name: "Suggestions (archived)", path: "/docs/suggestions" },
|
||||
{ name: "Tasks", path: "/docs/tasks" },
|
||||
{ name: "TPSC", path: "/docs/tpsc" },
|
||||
{ name: "TPSC — GDPR Maturity", path: "/docs/gdpr-maturity" },
|
||||
|
|
|
|||
|
|
@ -1,80 +1,61 @@
|
|||
---
|
||||
title: Suggestions (legacy) — Reference
|
||||
title: Suggestions (archived) — Reference
|
||||
---
|
||||
|
||||
# Suggestions (legacy) — Reference
|
||||
# Suggestions (archived) — Reference
|
||||
|
||||
The **suggestion** entity is **read-only legacy**. It predates the fleet
|
||||
**work-record** model. Gated needs, findings, and unvetted sparks are recorded
|
||||
as **intake** work records (`kind: intake`), not as suggestions.
|
||||
The **suggestion** entity is **retired**. It predates the fleet **work-record**
|
||||
model. Gated needs, findings, and unvetted sparks are recorded as **intake**
|
||||
work records (`kind: intake`).
|
||||
|
||||
| Use | Path |
|
||||
|-----|------|
|
||||
| **Current** | [Intakes](/docs/intakes) · `POST /intakes/` · MCP `create_intake` |
|
||||
| **Canon** | `the-custodian/canon/standards/work-record-types_v0.1.md` |
|
||||
| **Legacy table** | `GET /suggestions/` (historical rows may remain) |
|
||||
| **Archived record** | `the-custodian/docs/archived-suggestion-backlog.md` |
|
||||
|
||||
Umbrella: [Work Records](/docs/work-records).
|
||||
|
||||
---
|
||||
|
||||
## What still exists
|
||||
## What happened
|
||||
|
||||
| Surface | Behaviour today |
|
||||
|---------|-----------------|
|
||||
| `GET /suggestions/` | Lists historical suggestion rows (if any). Ranking query params still exist on the read path. |
|
||||
| `GET /suggestions/{id}` | Read one historical row. |
|
||||
| Mutation routes (`POST /suggestions/`, vet, decline, promote, bump-relevance) | **HTTP 410 Gone** — body points at `/intakes/` and the work-record standard |
|
||||
| MCP `create_suggestion`, `vet_suggestion`, `decline_suggestion`, `promote_suggestion_to_task`, `bump_suggestion_relevance` | Call the retired mutation endpoints; they return 410 |
|
||||
| Dashboard **Suggestions** page | Still polls `GET /suggestions/?rank=wsjf`; useful only for residual history |
|
||||
| `GET /state/summary` → `ranked_suggestions` | Still computed from open suggestion rows; after migration the open set is typically empty |
|
||||
The suggestion backlog was a demand-weighted queue: agents that hit an unmet
|
||||
need raised a suggestion, repeat encounters bumped its relevance, and WSJF
|
||||
scoring ordered what to build. Intake covers the same discovery step inside the
|
||||
work-record framework rather than beside it.
|
||||
|
||||
Records were not deleted at retirement. Open backlog at cut-over (2026-07-21,
|
||||
CUST-WP-0061-T06) was re-authored as file-backed intakes (with
|
||||
`origin: legacy-suggestion:<uuid>`) and the hub suggestion rows were closed in
|
||||
place so history remained inspectable.
|
||||
| Date | Step |
|
||||
|------|------|
|
||||
| 2026-07-21 | Mutations retired (`CUST-WP-0061-T06`) — every write returns HTTP 410. Reads kept live so the history stayed reachable. |
|
||||
| 2026-08-20 | History archived to `the-custodian`; read routes, MCP tools, and this dashboard page retired (`STATE-WP-0079-T05`, slice E1). |
|
||||
|
||||
---
|
||||
Every route under `/suggestions` now returns **HTTP 410** with a pointer to
|
||||
intake and to the archive.
|
||||
|
||||
## Former stage model (historical)
|
||||
## The archived record
|
||||
|
||||
While mutations were live, stages were:
|
||||
All 10 suggestions, 10 notes, and 5 relevance bumps are preserved in
|
||||
`the-custodian/docs/archived-suggestion-backlog.md`, with each entry's origin,
|
||||
WSJF inputs, and migration target.
|
||||
|
||||
| Stage | Meaning |
|
||||
|-------|---------|
|
||||
| `suggestion` | Recorded need, not yet vetted |
|
||||
| `requirement` | Vetted with structured fields and notes |
|
||||
| `promoted` | Became a real `Task` (`promoted_task_id` set) |
|
||||
| `declined` | Rejected; terminal |
|
||||
Every entry was closed as `declined` during the intake migration and **none was
|
||||
promoted** — the backlog was drained deliberately, not abandoned. The per-entry
|
||||
notes name each one's successor intake (`CUST-IN-0001`, `CUST-IN-0002`, …).
|
||||
|
||||
WSJF ranking used `base_value`, `relevance`, `job_size`, and
|
||||
`relevance_weight`. That ranking applied only to the suggestion table; it is
|
||||
not the intake ranking model.
|
||||
The record lives in `the-custodian` rather than here because State Hub is being
|
||||
archived under `STATE-WP-0079`; a record kept inside the component it documents
|
||||
disappears with it.
|
||||
|
||||
---
|
||||
## What is not retired
|
||||
|
||||
## Mapping to intake
|
||||
**Dashboard UI feedback** — the Shift+click suggestions on
|
||||
[Todo](/docs/todo) and the UI Feedback page — is a *different* entity backed by
|
||||
`technical_debt`, and is unaffected. The shared word is a coincidence of naming.
|
||||
|
||||
| Suggestion concept | Intake equivalent |
|
||||
|--------------------|-------------------|
|
||||
| Create gated need | `POST /intakes/` / `create_intake` |
|
||||
| Vet | Move toward `vetted` / notes on the intake |
|
||||
| Promote to task | `route_intake` then `statehub promote-intake … --to task` (or workplan / decision / engagement) |
|
||||
| Decline | `close_intake(outcome="declined")` |
|
||||
| Absorbed by existing work | `close_intake(outcome="absorbed")` |
|
||||
| Domain-scoped row | Intake scoped by `topic_id` and/or `workplan_id` and/or `repo_id` |
|
||||
## Data
|
||||
|
||||
Do not open new work through the suggestion API.
|
||||
|
||||
---
|
||||
|
||||
## Related pages
|
||||
|
||||
- [Intakes](/docs/intakes) — current entity and lifecycle
|
||||
- [Work Records](/docs/work-records) — kind registry
|
||||
- [WSJF Triage](/docs/wsjf-triage) — daily advisory triage (workplan-oriented)
|
||||
|
||||
---
|
||||
|
||||
*Suggestion mutations are retired by design (CUST-WP-0061). Intake is a fresh
|
||||
entity, not a rename of the suggestions table.*
|
||||
The `suggestions`, `suggestion_notes`, and `suggestion_relevance_bumps` tables
|
||||
are intentionally still present. They are `retire`/`archive` in `SHR-INV-0001`
|
||||
and are captured by the final dump at `STATE-WP-0079-T06`; dropping them ahead
|
||||
of that dump would remove data for no gain.
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ convention used in the Custodian State Hub.
|
|||
| [Progress Log](/docs/progress-log) | Event types, append-only policy, session protocol |
|
||||
| [Repos](/docs/repos) | Repo registry, SBOM coverage map, ingestion commands |
|
||||
| [SBOM](/docs/sbom) | Lockfile ingestion, licence report, copyleft detection |
|
||||
| [Suggestions (legacy)](/docs/suggestions) | Retired gated-need table; pointer to intakes |
|
||||
| [Suggestions (archived)](/docs/suggestions) | Retired backlog; archived record and pointer to intakes |
|
||||
| [Tasks](/docs/tasks) | Task statuses, priorities, filter bar, status distribution chart |
|
||||
| [Technical Debt](/docs/debt) | Debt types, severities, statuses, registration |
|
||||
| [Todo](/docs/todo) | Internal/Ecosystem/Third-party classification, data sources |
|
||||
|
|
|
|||
|
|
@ -1,83 +0,0 @@
|
|||
---
|
||||
title: Suggestions (legacy)
|
||||
---
|
||||
|
||||
```js
|
||||
import {apiFetch, pollDelay, waitForVisible} from "./components/config.js";
|
||||
const POLL = 30_000;
|
||||
```
|
||||
|
||||
```js
|
||||
const sugState = (async function*() {
|
||||
let failures = 0;
|
||||
while (true) {
|
||||
let data = [], ok = false;
|
||||
try {
|
||||
const r = await apiFetch("/suggestions/?rank=wsjf&limit=100");
|
||||
ok = r.ok;
|
||||
data = ok ? await r.json() : [];
|
||||
} catch {}
|
||||
failures = ok ? 0 : failures + 1;
|
||||
yield {data, ok, ts: new Date()};
|
||||
await waitForVisible(pollDelay({ok, base: POLL, failures}));
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
```js
|
||||
const suggestions = sugState.data ?? [];
|
||||
const _ok = sugState.ok ?? false;
|
||||
const _ts = sugState.ts;
|
||||
```
|
||||
|
||||
# Suggestions (legacy)
|
||||
|
||||
```js
|
||||
import {injectTocTop} from "./components/toc-sidebar.js";
|
||||
import {withDocHelp} from "./components/doc-overlay.js";
|
||||
|
||||
const _liveEl = html`<div class="live-indicator">
|
||||
<span style="color:${_ok ? 'var(--theme-foreground-focus)' : 'red'}">●</span>
|
||||
${_ok ? `Live · ${_ts?.toLocaleTimeString()}` : html`<span style="color:red">API offline</span>`}
|
||||
</div>`;
|
||||
withDocHelp(_liveEl, "/docs/live-data");
|
||||
injectTocTop("live-indicator", _liveEl);
|
||||
|
||||
const _h1 = document.querySelector("#observablehq-main h1");
|
||||
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/suggestions"); }
|
||||
|
||||
display(html`<p class="dim"><strong>Read-only legacy.</strong> New gated needs are <em>intake</em> work records
|
||||
(<code>GET /intakes/</code>, MCP <code>create_intake</code>) — see
|
||||
<a href="/docs/intakes">Intakes</a> and <a href="/docs/suggestions">Suggestions reference</a>.
|
||||
This page lists historical <code>GET /suggestions/</code> rows only; mutations return HTTP 410.</p>`);
|
||||
display(html`<p class="dim">Former ranking: WSJF = (base_value + relevance_weight × relevance) / job_size.
|
||||
<a href="/wsjf-triage">Daily WSJF triage</a> is workplan-oriented advisory review.</p>`);
|
||||
```
|
||||
|
||||
```js
|
||||
const stageBadge = (stage) => {
|
||||
const colors = {
|
||||
suggestion: "#6b7280",
|
||||
requirement: "#2563eb",
|
||||
promoted: "#059669",
|
||||
declined: "#9ca3af",
|
||||
};
|
||||
return html`<span style="font-size:0.75rem;padding:0.1rem 0.45rem;border-radius:4px;background:${colors[stage] ?? "#ccc'};color:#fff">${stage}</span>`;
|
||||
};
|
||||
|
||||
const rows = suggestions.map((s) => html`<tr>
|
||||
<td>${stageBadge(s.stage)}</td>
|
||||
<td><strong>${s.title}</strong>${s.origin_ref ? html`<br/><code style="font-size:0.75rem">${s.origin_ref}</code>` : ""}</td>
|
||||
<td>${s.domain_slug || "—"}</td>
|
||||
<td style="text-align:right">${s.relevance}</td>
|
||||
<td style="text-align:right">${s.wsjf?.toFixed?.(1) ?? s.wsjf}</td>
|
||||
<td style="font-size:0.8rem">${s.last_requested_at ? new Date(s.last_requested_at).toLocaleString() : "—"}</td>
|
||||
</tr>`);
|
||||
|
||||
display(html`<table class="dashboard-table" style="width:100%;font-size:0.88rem">
|
||||
<thead><tr>
|
||||
<th>Stage</th><th>Title</th><th>Domain</th><th>Relevance</th><th>WSJF</th><th>Last requested</th>
|
||||
</tr></thead>
|
||||
<tbody>${rows.length ? rows : html`<tr><td colspan="6" class="dim">No open suggestions yet.</td></tr>`}</tbody>
|
||||
</table>`);
|
||||
```
|
||||
|
|
@ -219,7 +219,7 @@ injectTocTop("live-indicator", _liveEl);
|
|||
const _h1 = document.querySelector("#observablehq-main h1");
|
||||
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/wsjf-triage"); }
|
||||
|
||||
display(html`<p class="triage-subtitle">Daily State Hub triage from activity-core. Recommendations are advisory; the operator and workplan owners decide what to act on. Ranked <a href="/suggestions">suggestion backlog</a> feeds the digest.</p>`);
|
||||
display(html`<p class="triage-subtitle">Daily State Hub triage from activity-core. Recommendations are advisory; the operator and workplan owners decide what to act on. The ranked suggestion backlog that once fed this digest is retired (slice E1); the digest now stands on workplan and task state alone.</p>`);
|
||||
display(html`<div class="triage-latest">
|
||||
<span>Last updated</span>
|
||||
<strong>${latestReport ? fmtDateTime(latestReport.created_at) : "No daily_triage events yet"}</strong>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"captured_at": "2026-08-20T05:27:26.224218+00:00",
|
||||
"captured_at": "2026-08-20T06:00:07.693350+00:00",
|
||||
"api_base": "http://127.0.0.1:8000",
|
||||
"workplan": "STATE-WP-0070",
|
||||
"retired_interfaces": [],
|
||||
"weekly_review": {
|
||||
"generated_at": "2026-08-20T05:27:26.217390Z",
|
||||
"window_start": "2026-08-13T05:27:26.168151Z",
|
||||
"window_end": "2026-08-20T05:27:26.168151Z",
|
||||
"generated_at": "2026-08-20T06:00:07.687814Z",
|
||||
"window_start": "2026-08-13T06:00:07.633734Z",
|
||||
"window_end": "2026-08-20T06:00:07.633734Z",
|
||||
"cadence": "weekly",
|
||||
"activity_core_handoff": {
|
||||
"activity_id": "statehub-legacy-interface-review",
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ D3 is two MCP tools with no dependants; take it opportunistically.
|
|||
| --- | --- | --- | --- |
|
||||
| E1 | `suggestion-backlog` → archive | 17 | history archived 2026-08-20; read surface + dashboard remain |
|
||||
| E2 | `work-records-workplan-legacy` → archive | 13 | legacy-meter — candidate rule, see below |
|
||||
| E3 | `dashboard-meta` | 1 | none |
|
||||
| E3 | `dashboard-meta` (`/ui-feedback`) | 1 | T06 freeze window — owner is `state-hub-until-cutover` |
|
||||
| E4 | `legacy-meter` itself | 9 | **last** — it is the instrument for E2 |
|
||||
|
||||
E4 retires only after every other legacy surface is closed; retiring the meter
|
||||
|
|
@ -197,12 +197,25 @@ live so the historical record stayed reachable.
|
|||
migration target. Every entry was `declined` in the intake migration and none
|
||||
was promoted, so nothing forward-looking is lost by removing the surface.
|
||||
|
||||
**Remaining for E1:** retire the 8 read routes and 6 MCP tools, then drop the 3
|
||||
tables at the T06 dump. This has a dependency the inventory's 17-item count does
|
||||
not show — `dashboard/src/suggestions.md` is a live page, and `index.md` and
|
||||
`wsjf-triage.md` reference it. The dashboard must be updated in the same change
|
||||
or the retirement breaks it. Sequence E1's removal with A4/B5 (the UI slices)
|
||||
rather than ahead of them.
|
||||
**Executed 2026-08-20.** The 8 read routes now 410 with a pointer to intake and
|
||||
to the archive, the 6 MCP tools are removed (not stubbed — a retired tool that
|
||||
still appears in the tool list costs every agent session context on every call),
|
||||
the 5 `/suggestions` write-idempotency rules are gone, and the dashboard page is
|
||||
deleted with its nav entry, `reference.md` row, and `wsjf-triage.md` link
|
||||
updated. `dashboard/src/docs/suggestions.md` is rewritten as the archive
|
||||
pointer. Tables stay for the T06 dump.
|
||||
|
||||
One naming trap worth recording: **`ui-feedback.md` and `todo.md` "suggestions"
|
||||
are a different entity** — Shift+click dashboard feedback backed by
|
||||
`technical_debt`, not the suggestion backlog. They are untouched. Anything
|
||||
grepping for `suggestion` during cutover will hit them.
|
||||
|
||||
### E3 status (2026-08-20)
|
||||
|
||||
`dashboard-meta` is the `/ui-feedback` page — the feedback scratch surface just
|
||||
mentioned. Its owner is `state-hub-until-cutover` and it is still in active use,
|
||||
so it retires **at the T06 freeze window**, not before. Removing it now would
|
||||
delete a working feedback loop during the retirement it serves. No action.
|
||||
|
||||
### E2 evidence — defect found and partly fixed (2026-08-20)
|
||||
|
||||
|
|
|
|||
|
|
@ -1222,108 +1222,20 @@ def get_next_steps() -> str:
|
|||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Demand-weighted suggestion backlog (STATE-WP-0061)
|
||||
# Suggestion backlog — RETIRED (STATE-WP-0079-T05, slice E1)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@mcp.tool()
|
||||
def list_suggestions(
|
||||
domain: str | None = None,
|
||||
stage: str | None = None,
|
||||
rank: str | None = None,
|
||||
limit: int = 50,
|
||||
) -> str:
|
||||
"""List persisted suggestions, optionally ranked by WSJF."""
|
||||
return json.dumps(
|
||||
_get("/suggestions", {
|
||||
"domain": domain,
|
||||
"stage": stage,
|
||||
"rank": rank,
|
||||
"limit": limit,
|
||||
}),
|
||||
indent=2,
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def create_suggestion(
|
||||
domain: str,
|
||||
title: str,
|
||||
description: str | None = None,
|
||||
origin_ref: str | None = None,
|
||||
workplan_id: str | None = None,
|
||||
base_value: float = 3.0,
|
||||
job_size: float = 3.0,
|
||||
) -> str:
|
||||
"""Retired (CUST-WP-0061-T06, 2026-07-21): suggestions are read-only
|
||||
legacy. Use create_intake instead (kind: intake per canon/standards/
|
||||
work-record-types_v0.1.md). This call now 410s server-side; kept only
|
||||
so old callers get a clear error instead of a missing tool."""
|
||||
return json.dumps(_post("/suggestions", {
|
||||
"domain": domain,
|
||||
"title": title,
|
||||
"description": description,
|
||||
"origin_ref": origin_ref,
|
||||
"workplan_id": workplan_id,
|
||||
"base_value": base_value,
|
||||
"job_size": job_size,
|
||||
}), indent=2)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def vet_suggestion(suggestion_id: str, note: str, author: str | None = None) -> str:
|
||||
"""Retired (CUST-WP-0061-T06, 2026-07-21): suggestions are read-only
|
||||
legacy. There is no vet/route equivalent needed for intake — use
|
||||
route_intake directly. This call now 410s server-side."""
|
||||
return json.dumps(_post(f"/suggestions/{suggestion_id}/vet", {
|
||||
"note": note,
|
||||
"author": author,
|
||||
}), indent=2)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def decline_suggestion(suggestion_id: str, note: str, author: str | None = None) -> str:
|
||||
"""Retired (CUST-WP-0061-T06, 2026-07-21): suggestions are read-only
|
||||
legacy. Use close_intake(outcome="declined") instead. This call now
|
||||
410s server-side."""
|
||||
return json.dumps(_post(f"/suggestions/{suggestion_id}/decline", {
|
||||
"note": note,
|
||||
"author": author,
|
||||
}), indent=2)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def promote_suggestion_to_task(
|
||||
suggestion_id: str,
|
||||
note: str | None = None,
|
||||
task_title: str | None = None,
|
||||
author: str | None = None,
|
||||
) -> str:
|
||||
"""Retired (CUST-WP-0061-T06, 2026-07-21): suggestions are read-only
|
||||
legacy. Use route_intake then the promote-intake CLI
|
||||
(scripts/promote_intake.py --to task) instead. This call now 410s
|
||||
server-side."""
|
||||
return json.dumps(_post(f"/suggestions/{suggestion_id}/promote", {
|
||||
"note": note,
|
||||
"task_title": task_title,
|
||||
"author": author,
|
||||
}), indent=2)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def bump_suggestion_relevance(
|
||||
suggestion_id: str,
|
||||
reason: str | None = None,
|
||||
author: str | None = None,
|
||||
) -> str:
|
||||
"""Retired (CUST-WP-0061-T06, 2026-07-21): suggestions are read-only
|
||||
legacy — relevance/WSJF scoring has no intake equivalent yet (ordering
|
||||
is lane+priority+age per the founder-reviewed architecture draft).
|
||||
This call now 410s server-side."""
|
||||
return json.dumps(_post(f"/suggestions/{suggestion_id}/bump-relevance", {
|
||||
"reason": reason,
|
||||
"author": author,
|
||||
}), indent=2)
|
||||
|
||||
# The six suggestion tools (list/create/vet/decline/promote/bump) were removed
|
||||
# on 2026-08-20. Mutations had 410'd since CUST-WP-0061-T06; the reads existed
|
||||
# only to keep the historical backlog reachable, and that history is now
|
||||
# archived at the-custodian/docs/archived-suggestion-backlog.md.
|
||||
#
|
||||
# Successor: the intake work-record entity — create_intake / route_intake /
|
||||
# close_intake, and scripts/promote_intake.py --to task. See
|
||||
# canon/standards/work-record-types_v0.1.md.
|
||||
#
|
||||
# Kept as a comment rather than as 410 stubs: 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.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dependency graph tools (S1.4)
|
||||
|
|
|
|||
|
|
@ -83,7 +83,11 @@ class TestSuggestionsRetired:
|
|||
r = await client.post(f"/suggestions/{sid}/bump-relevance", json={})
|
||||
assert r.status_code == 410
|
||||
|
||||
async def test_get_and_list_still_work_for_historical_rows(self, client, test_engine):
|
||||
async def test_reads_are_retired_too(self, client, test_engine):
|
||||
"""Slice E1 (STATE-WP-0079-T05, 2026-08-20): reads were kept live only
|
||||
to keep the historical backlog reachable. That history is archived at
|
||||
the-custodian/docs/archived-suggestion-backlog.md, so the read surface
|
||||
is retired as well."""
|
||||
await _create_domain(client, "custodian", "Custodian")
|
||||
sid = await _seed_suggestion(
|
||||
test_engine, domain_slug="custodian",
|
||||
|
|
@ -91,16 +95,17 @@ class TestSuggestionsRetired:
|
|||
origin="WARDEN-WP-0012", origin_ref="example",
|
||||
)
|
||||
|
||||
r = await client.get(f"/suggestions/{sid}")
|
||||
assert r.status_code == 200
|
||||
assert r.json()["title"] == "Historical gated-need signal"
|
||||
for path in (f"/suggestions/{sid}", "/suggestions/", f"/suggestions/{sid}/notes"):
|
||||
r = await client.get(path)
|
||||
assert r.status_code == 410, path
|
||||
|
||||
r2 = await client.get("/suggestions/")
|
||||
assert r2.status_code == 200
|
||||
assert any(s["id"] == sid for s in r2.json())
|
||||
|
||||
async def test_unknown_id_still_404s_not_410(self, client):
|
||||
"""Read paths keep their normal semantics; only mutations are
|
||||
blanket-retired."""
|
||||
async def test_unknown_id_also_410s(self, client):
|
||||
"""The whole prefix is retired, so there is nothing left to 404 on."""
|
||||
r = await client.get(f"/suggestions/{uuid.uuid4()}")
|
||||
assert r.status_code == 404
|
||||
assert r.status_code == 410
|
||||
|
||||
async def test_retirement_detail_points_at_intake_and_archive(self, client):
|
||||
r = await client.get("/suggestions/")
|
||||
detail = r.json()["detail"]
|
||||
assert "POST /intakes/" in detail
|
||||
assert "archived-suggestion-backlog.md" in detail
|
||||
|
|
|
|||
|
|
@ -184,10 +184,21 @@ notes, 5 bumps, all `declined` in the 2026-07-21 intake migration, none
|
|||
promoted. Mutations already 410 since `CUST-WP-0061-T06`; reads were open only
|
||||
to keep that history reachable, which the archive now replaces.
|
||||
|
||||
E1's remaining removal (8 read routes, 6 MCP tools, 3 tables) is **coupled to
|
||||
the dashboard** — `dashboard/src/suggestions.md` is live and referenced from
|
||||
`index.md` and `wsjf-triage.md`. The inventory's 17-item count does not show
|
||||
this. Sequence with the UI slices A4/B5, not ahead of them.
|
||||
**E1 removal executed the same day:** the 8 read routes 410 with a pointer to
|
||||
intake and the archive, the 6 MCP tools are removed outright, the 5
|
||||
`/suggestions` write-idempotency rules are dropped, and the dashboard page is
|
||||
deleted with nav, `reference.md` and `wsjf-triage.md` updated;
|
||||
`dashboard/src/docs/suggestions.md` is now the archive pointer. Tables remain
|
||||
for the T06 dump. Two tests that pinned the old read-live behaviour were
|
||||
rewritten to pin the retirement contract.
|
||||
|
||||
Naming trap for cutover: `ui-feedback.md` / `todo.md` "suggestions" are
|
||||
Shift+click dashboard feedback backed by `technical_debt`, **not** this
|
||||
capability. Untouched.
|
||||
|
||||
**E3 (`dashboard-meta`) is `/ui-feedback`** — that same feedback surface. Owner
|
||||
is `state-hub-until-cutover` and it is still in use, so it retires at the T06
|
||||
freeze window, not now.
|
||||
|
||||
Then `dashboard-meta` (E3, 1); `legacy-meter` itself (E4, 9) retires last,
|
||||
being the instrument.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue