feat(STATE-WP-0079): retire the suggestion-backlog surface (slice E1)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 37s

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:
tegwick 2026-08-20 08:11:14 +02:00
parent 98af485cd5
commit fb363b37d3
13 changed files with 148 additions and 447 deletions

View file

@ -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.

View file

@ -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 |

View file

@ -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>`);
```

View file

@ -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>