Merge branch 'preview-add-verdict'

This commit is contained in:
Vinta Chen
2026-10-01 20:37:55 +08:00
2 changed files with 13 additions and 10 deletions
+4 -4
View File
@@ -1,24 +1,24 @@
---
name: preview-verdicts
description: Generate the interactive keep/drop verdict preview (HTML page with per-row feedback controls) whenever a prune sweep, batch entry edit, or restructure needs maintainer review before touching README.md — and process the feedback JSON the maintainer pastes back.
description: Generate the interactive keep/drop/add verdict preview (HTML page with per-row feedback controls) whenever a prune sweep, batch entry edit, or restructure needs maintainer review before touching README.md — and process the feedback JSON the maintainer pastes back.
---
# Verdict preview
Maintainer review happens through an interactive HTML page: one row per entry with your seeded verdict and reason, a Keep/Drop toggle and a reason field for the maintainer, and a **Copy feedback** button that exports only changed or commented rows as JSON. Generate the page, wait for the pasted JSON, then apply it. Entry changes land in README.md only after the review — and only on an explicit go.
Maintainer review happens through an interactive HTML page: one row per entry with your seeded verdict and reason, Keep/Drop buttons (Add/Drop on proposed new entries) and a reason field for the maintainer, and a **Copy feedback** button that exports only changed or commented rows as JSON. Generate the page, wait for the pasted JSON, then apply it. Entry changes land in README.md only after the review — and only on an explicit go.
## Generate the preview
1. Build the `DATA` array. A group is `[section, subcategory, rows]`; a row is `[entry, url, downloads, verdict, reason]`.
- `subcategory` may carry a note after ` — ` (rendered muted): use it for proposed splits, re-homes, or anything the maintainer should weigh for the whole group.
- `downloads` is PyPI last-month as a comma-formatted string; use `—` when no signal exists (e.g. agent skill packs), `stdlib` for standard-library modules, `fetch failed` when the lookup failed. State the fetch date in the sub-header.
- `verdict` is `keep` or `drop`, seeded from the current adjudication or dry-run.
- `verdict` is `keep`, `drop`, or `add`, seeded from the current adjudication or dry-run. `add` marks a proposed new entry; its row gets Add/Drop buttons, where Drop means not added. Show a replacement as two rows: the old entry `drop`, the new entry `add`.
- `reason` is plain language the maintainer reads cold — no invented shorthand. When fresh evidence contradicts the seeded verdict (a big download count on a drop, a dead repo on a keep), say so in that row's reason instead of silently changing the seed.
2. Copy `template.html` (sibling of this file) and replace the placeholders: `__TITLE__` (page title), `__SUB__` (sub-header: scope, seed provenance, fetch date, and the standing instruction to flip/comment then Copy feedback), `__KEY__` (localStorage key), `__DATA__` (the array). `__KEY__` must be unique per review — slug plus date, e.g. `awesome-python-science-2026-09-01` — because saved state under a reused key bleeds a previous review's flips into rows with the same section and entry name.
3. Write the page to `tmp/awesome-python-<slug>-preview.html` in the repo root (git-ignored; create the directory if needed), `open` it, and tell the maintainer the path and the return path: flip or comment rows (they highlight yellow), press **Copy feedback**, paste the JSON into the chat. Done when the page is open and the return path is stated.
## Process the pasted feedback
Each JSON row is `{section, subcategory, entry, my_verdict, your_verdict, reason}`. The maintainer's verdict is final — apply it, never re-argue it. An empty reason means the verdict stands unexplained; that is enough.
Each JSON row is `{section, subcategory, entry, my_verdict, your_verdict, reason}`. On a row seeded `add`, `your_verdict: "drop"` means do not add it. The maintainer's verdict is final — apply it, never re-argue it. An empty reason means the verdict stands unexplained; that is enough.
Before executing, surface anything the flips imply that the maintainer has not decided: a use case pushed past its cap, an entry left homeless by a proposed split, a request that is already satisfied (a no-op). Ask, then execute on their go. Done when every pasted row is either applied or surfaced back — none silently dropped.
@@ -7,7 +7,7 @@
<style>
:root {
--ink: #1c1917; --paper: #faf9f7; --muted: #78716c; --line: #e7e5e4;
--keep: #1d4ed8; --keep-bg: #eff4ff; --drop: #b42318; --drop-bg: #fdf1ef;
--keep: #1d4ed8; --keep-bg: #eff4ff; --add: #6d28d9; --add-bg: #f3efff; --drop: #b42318; --drop-bg: #fdf1ef;
--changed: #fffbeb; --changed-line: #f5c518;
}
* { box-sizing: border-box; }
@@ -32,12 +32,14 @@
.badge { display: inline-block; font-size: .78rem; font-weight: 700; letter-spacing: .03em; padding: .12em .5em; border-radius: 4px; }
.badge.keep { color: var(--keep); background: var(--keep-bg); }
.badge.drop { color: var(--drop); background: var(--drop-bg); }
.badge.add { color: var(--add); background: var(--add-bg); }
td.reason { color: #44403c; font-size: .95rem; max-width: 34rem; }
td.yours { white-space: nowrap; }
.seg { display: inline-flex; border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
.seg button { border: 0; background: #fff; padding: .3rem .8rem; font: inherit; font-size: .88rem; cursor: pointer; color: var(--muted); }
.seg button + button { border-left: 1px solid var(--line); }
.seg button.on.k { background: var(--keep); color: #fff; font-weight: 700; }
.seg button.on.a { background: var(--add); color: #fff; font-weight: 700; }
.seg button.on.d { background: var(--drop); color: #fff; font-weight: 700; }
td.fb input { width: 100%; min-width: 14rem; font: inherit; font-size: .92rem; padding: .3rem .5rem; border: 1px solid var(--line); border-radius: 6px; background: #fff; }
td.fb input:focus { outline: 2px solid var(--keep); outline-offset: 0; }
@@ -89,7 +91,9 @@ function render() {
<td class="mine"><span class="badge ${mine}">${mine.toUpperCase()}</span></td>
<td class="reason">${reason}</td>
<td class="yours"><span class="seg">
<button class="k ${yours === "keep" ? "on" : ""}" onclick="setV('${esc(id)}','keep')">Keep</button>
${mine === "add"
? `<button class="a ${yours === "add" ? "on" : ""}" onclick="setV('${esc(id)}','add')">Add</button>`
: `<button class="k ${yours === "keep" ? "on" : ""}" onclick="setV('${esc(id)}','keep')">Keep</button>`}
<button class="d ${yours === "drop" ? "on" : ""}" onclick="setV('${esc(id)}','drop')">Drop</button>
</span></td>
<td class="fb"><input value="${(s.feedback || "").replace(/"/g, "&quot;")}" oninput="setF('${esc(id)}', this.value)" placeholder="why…"></td>
@@ -133,15 +137,14 @@ function collect() {
}
function updateCounts() {
let keeps = 0, total = 0, changed = 0;
let keeps = 0, adds = 0, drops = 0, changed = 0;
for (const [section, , rows] of DATA) for (const [name, , , mine] of rows) {
total++;
const s = state[rowId(section, name)] || {};
const yours = s.verdict || mine;
if (yours === "keep") keeps++;
if (yours === "keep") keeps++; else if (yours === "add") adds++; else drops++;
if (yours !== mine) changed++;
}
document.getElementById("counts").innerHTML = `<b>${keeps}</b> keep · <b>${total - keeps}</b> drop of <b>${total}</b> &nbsp;|&nbsp; <b>${changed}</b> verdicts flipped`;
document.getElementById("counts").innerHTML = `<b>${keeps}</b> keep · <b>${drops}</b> drop · <b>${adds}</b> add &nbsp;|&nbsp; <b>${changed}</b> verdicts flipped`;
}
function copyFeedback() {