diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index e016863..c868714 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -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 | diff --git a/api/routers/suggestions.py b/api/routers/suggestions.py index b8b3f54..ed5df8c 100644 --- a/api/routers/suggestions.py +++ b/api/routers/suggestions.py @@ -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()) \ No newline at end of file diff --git a/api/services/write_idempotency.py b/api/services/write_idempotency.py index fb0c646..a7c06b5 100644 --- a/api/services/write_idempotency.py +++ b/api/services/write_idempotency.py @@ -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"), ) diff --git a/dashboard/observablehq.config.js b/dashboard/observablehq.config.js index 9aebb7a..b749881 100644 --- a/dashboard/observablehq.config.js +++ b/dashboard/observablehq.config.js @@ -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" }, diff --git a/dashboard/src/docs/suggestions.md b/dashboard/src/docs/suggestions.md index d007176..ff3b08e 100644 --- a/dashboard/src/docs/suggestions.md +++ b/dashboard/src/docs/suggestions.md @@ -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:`) 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. diff --git a/dashboard/src/reference.md b/dashboard/src/reference.md index 9e2641b..dd96ce2 100644 --- a/dashboard/src/reference.md +++ b/dashboard/src/reference.md @@ -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 | diff --git a/dashboard/src/suggestions.md b/dashboard/src/suggestions.md deleted file mode 100644 index 3da219f..0000000 --- a/dashboard/src/suggestions.md +++ /dev/null @@ -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`
- - ${_ok ? `Live · ${_ts?.toLocaleTimeString()}` : html`API offline`} -
`; -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`

Read-only legacy. New gated needs are intake work records - (GET /intakes/, MCP create_intake) — see - Intakes and Suggestions reference. - This page lists historical GET /suggestions/ rows only; mutations return HTTP 410.

`); -display(html`

Former ranking: WSJF = (base_value + relevance_weight × relevance) / job_size. - Daily WSJF triage is workplan-oriented advisory review.

`); -``` - -```js -const stageBadge = (stage) => { - const colors = { - suggestion: "#6b7280", - requirement: "#2563eb", - promoted: "#059669", - declined: "#9ca3af", - }; - return html`${stage}`; -}; - -const rows = suggestions.map((s) => html` - ${stageBadge(s.stage)} - ${s.title}${s.origin_ref ? html`
${s.origin_ref}` : ""} - ${s.domain_slug || "—"} - ${s.relevance} - ${s.wsjf?.toFixed?.(1) ?? s.wsjf} - ${s.last_requested_at ? new Date(s.last_requested_at).toLocaleString() : "—"} -`); - -display(html` - - - - ${rows.length ? rows : html``} -
StageTitleDomainRelevanceWSJFLast requested
No open suggestions yet.
`); -``` \ No newline at end of file diff --git a/dashboard/src/wsjf-triage.md b/dashboard/src/wsjf-triage.md index c593923..bf991ca 100644 --- a/dashboard/src/wsjf-triage.md +++ b/dashboard/src/wsjf-triage.md @@ -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`

Daily State Hub triage from activity-core. Recommendations are advisory; the operator and workplan owners decide what to act on. Ranked suggestion backlog feeds the digest.

`); +display(html`

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.

`); display(html`
Last updated ${latestReport ? fmtDateTime(latestReport.created_at) : "No daily_triage events yet"} diff --git a/docs/evidence/legacy-meter-weekly-review-20260820.json b/docs/evidence/legacy-meter-weekly-review-20260820.json index 7446dfd..1316882 100644 --- a/docs/evidence/legacy-meter-weekly-review-20260820.json +++ b/docs/evidence/legacy-meter-weekly-review-20260820.json @@ -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", diff --git a/docs/retirement-cutover-slice-plan.md b/docs/retirement-cutover-slice-plan.md index 6657496..281360b 100644 --- a/docs/retirement-cutover-slice-plan.md +++ b/docs/retirement-cutover-slice-plan.md @@ -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) diff --git a/mcp_server/server.py b/mcp_server/server.py index 2ef687e..5cedae9 100644 --- a/mcp_server/server.py +++ b/mcp_server/server.py @@ -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) diff --git a/tests/test_suggestions.py b/tests/test_suggestions.py index bb6f735..801fba3 100644 --- a/tests/test_suggestions.py +++ b/tests/test_suggestions.py @@ -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 diff --git a/workplans/STATE-WP-0079-retirement-strangler.md b/workplans/STATE-WP-0079-retirement-strangler.md index c30002d..0019f29 100644 --- a/workplans/STATE-WP-0079-retirement-strangler.md +++ b/workplans/STATE-WP-0079-retirement-strangler.md @@ -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.