Finish CUST-WP-0011 and implement STATE-WP-0061 suggestion backlog.

Add persisted Suggestion entities with WSJF ranking, relevance bumps,
REST/MCP write surfaces, /suggestions dashboard page, and daily triage
digest integration. Document the cluster operating model, archive the
completed migration workplan, and seed WARDEN-WP-0012 routing scenarios.
This commit is contained in:
2026-07-06 10:52:49 +02:00
parent cac9a6b1e0
commit f2e042a278
31 changed files with 1537 additions and 66 deletions

View File

@@ -81,6 +81,7 @@ export default {
{ name: "Interventions", path: "/interventions" },
{ name: "Tasks", path: "/tasks" },
{ name: "UI Feedback", path: "/ui-feedback" },
{ name: "Suggestions", path: "/suggestions" },
{ name: "WSJF Triage", path: "/wsjf-triage" },
],
},
@@ -122,6 +123,7 @@ export default {
{ name: "Workstream Health", path: "/docs/workstream-health-index" },
{ name: "Workstream Lifecycle", path: "/docs/workstream-lifecycle" },
{ name: "Workstreams", path: "/docs/workstreams" },
{ name: "Suggestions", path: "/docs/suggestions" },
{ name: "WSJF Triage", path: "/docs/wsjf-triage" },
],
},

View File

@@ -227,5 +227,8 @@ and age in days.
---
*Capability requests are a sanctioned write use case of the State Hub alongside
`resolve_decision` and `get_next_steps`. They do not originate in workplan files —
`resolve_decision`, `get_next_steps`, and the suggestion backlog writes
(`create_suggestion`, `vet_suggestion`, `decline_suggestion`,
`promote_suggestion_to_task`, `bump_suggestion_relevance`). They do not
originate in workplan files —
they are operational coordination.*

View File

@@ -0,0 +1,49 @@
# Demand-Weighted Suggestion Backlog
The `/suggestions` page shows persisted **gated needs** that are not yet real
tasks. Each unmet lookup increments `relevance`, which raises WSJF ranking.
## Stages
| 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 |
## WSJF projection
```text
cost_of_delay = base_value + (relevance_weight × relevance)
wsjf = cost_of_delay / job_size
```
`GET /suggestions?rank=wsjf` returns open suggestions/requirements ordered by
score. Promoted and declined entries are excluded unless
`include_terminal=true`.
## Sanctioned writes
MCP and REST:
- `create_suggestion` / `POST /suggestions/`
- `vet_suggestion` / `POST /suggestions/{id}/vet`
- `decline_suggestion` / `POST /suggestions/{id}/decline`
- `promote_suggestion_to_task` / `POST /suggestions/{id}/promote`
- `bump_suggestion_relevance` / `POST /suggestions/{id}/bump-relevance`
Relevance also bumps automatically when:
- `GET /state/next_steps` surfaces open suggestions
- A `CapabilityRequest` matches an open suggestion
## Daily triage
`GET /state/summary` includes `ranked_suggestions` for the activity-core
`daily_triage_digest` resolver. See [WSJF Triage](/docs/wsjf-triage).
## Origin
Motivated by ops-warden `WARDEN-WP-0012` gated routing scenarios. Example
backfill: `scripts/seed_wp0012_suggestions.py`.

View File

@@ -0,0 +1,79 @@
---
title: Suggestions
---
```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;
```
# Demand-Weighted Suggestions
```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">Ranked by WSJF = (base_value + relevance_weight × relevance) / job_size. Gated needs accrue relevance when unmet.</p>`);
display(html`<p class="dim"><a href="/wsjf-triage">Daily WSJF triage</a> consumes this backlog in its digest.</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.</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. Ranked <a href="/suggestions">suggestion backlog</a> feeds the digest.</p>`);
display(html`<div class="triage-latest">
<span>Last updated</span>
<strong>${latestReport ? fmtDateTime(latestReport.created_at) : "No daily_triage events yet"}</strong>