cc-pushback 0.3.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {cc_pushback-0.3.0 → cc_pushback-0.5.0}/PKG-INFO +3 -3
  2. cc_pushback-0.5.0/cc_pushback/assets/base.css +66 -0
  3. cc_pushback-0.5.0/cc_pushback/assets/dashboard.css +58 -0
  4. cc_pushback-0.5.0/cc_pushback/assets/index.html +44 -0
  5. cc_pushback-0.5.0/cc_pushback/assets/js/cards.js +38 -0
  6. cc_pushback-0.5.0/cc_pushback/assets/js/detail.js +20 -0
  7. cc_pushback-0.5.0/cc_pushback/assets/js/dom.js +27 -0
  8. cc_pushback-0.5.0/cc_pushback/assets/js/filters.js +116 -0
  9. cc_pushback-0.5.0/cc_pushback/assets/js/lineage.js +84 -0
  10. cc_pushback-0.5.0/cc_pushback/assets/js/main.js +64 -0
  11. cc_pushback-0.5.0/cc_pushback/assets/js/state.js +10 -0
  12. cc_pushback-0.5.0/cc_pushback/assets/js/stats.js +37 -0
  13. cc_pushback-0.5.0/cc_pushback/claude.py +56 -0
  14. {cc_pushback-0.3.0 → cc_pushback-0.5.0}/cc_pushback/cli.py +27 -28
  15. cc_pushback-0.5.0/cc_pushback/dashboard.py +261 -0
  16. {cc_pushback-0.3.0 → cc_pushback-0.5.0}/cc_pushback/detectors.py +25 -7
  17. cc_pushback-0.5.0/cc_pushback/enrich.py +178 -0
  18. {cc_pushback-0.3.0 → cc_pushback-0.5.0}/cc_pushback/formats.py +14 -1
  19. {cc_pushback-0.3.0 → cc_pushback-0.5.0}/cc_pushback/report.py +36 -240
  20. {cc_pushback-0.3.0 → cc_pushback-0.5.0}/cc_pushback/scan.py +19 -4
  21. cc_pushback-0.5.0/cc_pushback/sidecar.py +216 -0
  22. {cc_pushback-0.3.0 → cc_pushback-0.5.0}/cc_pushback/store.py +46 -178
  23. {cc_pushback-0.3.0 → cc_pushback-0.5.0}/pyproject.toml +3 -3
  24. cc_pushback-0.3.0/cc_pushback/claude.py +0 -47
  25. cc_pushback-0.3.0/cc_pushback/dashboard.py +0 -338
  26. cc_pushback-0.3.0/cc_pushback/enrich.py +0 -306
  27. {cc_pushback-0.3.0 → cc_pushback-0.5.0}/LICENSE +0 -0
  28. {cc_pushback-0.3.0 → cc_pushback-0.5.0}/README.md +0 -0
  29. {cc_pushback-0.3.0 → cc_pushback-0.5.0}/cc_pushback/__init__.py +0 -0
  30. {cc_pushback-0.3.0 → cc_pushback-0.5.0}/cc_pushback/__main__.py +0 -0
  31. {cc_pushback-0.3.0 → cc_pushback-0.5.0}/cc_pushback/evaluate.py +0 -0
  32. {cc_pushback-0.3.0 → cc_pushback-0.5.0}/cc_pushback/golden_triage.json +0 -0
  33. {cc_pushback-0.3.0 → cc_pushback-0.5.0}/cc_pushback/models.py +0 -0
  34. {cc_pushback-0.3.0 → cc_pushback-0.5.0}/cc_pushback/py.typed +0 -0
  35. {cc_pushback-0.3.0 → cc_pushback-0.5.0}/cc_pushback/refine.py +0 -0
  36. {cc_pushback-0.3.0 → cc_pushback-0.5.0}/cc_pushback/serve.py +0 -0
  37. {cc_pushback-0.3.0 → cc_pushback-0.5.0}/cc_pushback/spec.py +0 -0
  38. {cc_pushback-0.3.0 → cc_pushback-0.5.0}/cc_pushback/triage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cc-pushback
3
- Version: 0.3.0
3
+ Version: 0.5.0
4
4
  Summary: Learn your pushback style from past Claude Code feedback and code reviews, and replicate it with a language model.
5
5
  Keywords:
6
6
  Author: Yasyf Mohamedali
@@ -14,11 +14,11 @@ Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Programming Language :: Python :: 3 :: Only
15
15
  Classifier: Typing :: Typed
16
16
  Requires-Dist: anyio>=4.4
17
- Requires-Dist: cc-transcript>=3.0,<4
17
+ Requires-Dist: cc-transcript>=5,<6
18
18
  Requires-Dist: click>=8
19
19
  Requires-Dist: fastapi>=0.115
20
20
  Requires-Dist: pydantic>=2
21
- Requires-Dist: spawnllm>=0.1.3
21
+ Requires-Dist: spawnllm>=0.4.0
22
22
  Requires-Dist: uvicorn>=0.30
23
23
  Requires-Dist: pytest>=8.0 ; extra == 'dev'
24
24
  Requires-Dist: ty>=0.0.44 ; extra == 'dev'
@@ -0,0 +1,66 @@
1
+ :root{--bg:#0d1117;--panel:#161b22;--border:#30363d;--fg:#e6edf3;--muted:#8b949e;--accent:#58a6ff}
2
+ *{box-sizing:border-box}
3
+ body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}
4
+ h1,h2{font-weight:600}
5
+ header.top{padding:24px;border-bottom:1px solid var(--border)}
6
+ header.top .sub{color:var(--muted)}
7
+ section{padding:16px 24px}
8
+ .stat-cards{display:flex;gap:12px;flex-wrap:wrap}
9
+ .stat{background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:12px 16px}
10
+ .stat .n{font-size:20px;font-weight:600}
11
+ .stat .l{color:var(--muted);font-size:12px}
12
+ table.dist{border-collapse:collapse;margin-top:14px}
13
+ table.dist td{padding:2px 10px 2px 0;white-space:nowrap}
14
+ .bar{display:inline-block;height:10px;background:var(--accent);border-radius:3px;vertical-align:middle}
15
+ .narrative{background:var(--panel);border:1px solid var(--border);border-left:3px solid var(--accent);
16
+ border-radius:8px;padding:14px 18px;max-width:80ch;margin-top:14px}
17
+ .card{background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:12px 16px;margin:12px 0}
18
+ .card header{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:8px}
19
+ .badge{font-size:11px;padding:2px 8px;border-radius:10px;background:#21262d;border:1px solid var(--border)}
20
+ .badge-transcript_message{color:#8b949e}.badge-review_comment{color:#7ee787}.badge-plan_review{color:#d2a8ff}
21
+ .badge-interrupt_rejection{color:#ff7b72}.badge-superset_issue{color:#ffa657}
22
+ time{color:var(--muted);font-size:12px}
23
+ .chip{font-size:11px;color:var(--muted);background:#21262d;border-radius:6px;padding:1px 6px}
24
+ .text pre{white-space:pre-wrap;word-break:break-word;margin:0;font:inherit}
25
+ details.ctx{margin-top:10px}
26
+ details.ctx summary{color:var(--accent);cursor:pointer}
27
+ .turn{border-left:2px solid var(--border);padding:4px 0 4px 10px;margin:6px 0}
28
+ .turn .role{font-size:10px;text-transform:uppercase;color:var(--muted)}
29
+ .turn .tools{font-size:10px;color:var(--accent);margin-left:6px}
30
+ .turn pre{white-space:pre-wrap;word-break:break-word;margin:2px 0 0;font:inherit;color:var(--muted)}
31
+ .turn-user pre{color:var(--fg)}
32
+ .turn-trigger{border-left-color:var(--accent)}
33
+ .turn-trigger .role::after{content:" \2190 pushed back on";color:var(--accent)}
34
+ .lineage{display:flex;flex-direction:column;gap:14px}
35
+ .stage{background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:12px 16px}
36
+ .stage h3{margin:0 0 10px;font-size:13px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}
37
+ .stage-detector{border-left:3px solid var(--muted)}
38
+ .stage-judge{border-left:3px solid var(--accent)}
39
+ .stage-auditor{border-left:3px solid #d2a8ff}
40
+ .stage-refiner{border-left:3px solid #7ee787}
41
+ .stage-golden{border-left:3px solid #ffa657}
42
+ .verdict,.pair{border:1px solid var(--border);border-radius:6px;padding:8px 10px;margin:8px 0}
43
+ .vhead{display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-bottom:6px}
44
+ .vsum,.vrat,.paction,.pcomplaint{white-space:pre-wrap;word-break:break-word;margin:2px 0;font:inherit}
45
+ .vrat,.pcomplaint{color:var(--muted)}
46
+ .pverbatim{border-left:2px solid #7ee787;margin:6px 0;padding:2px 0 2px 10px;color:var(--fg)}
47
+ .orig pre{white-space:pre-wrap;word-break:break-word;margin:0 0 8px;color:var(--muted)}
48
+ mark{background:#7ee78733;color:var(--fg);border-radius:3px}
49
+ .evidence{margin-top:8px}
50
+ .panes{display:flex;gap:8px;flex-wrap:wrap;margin:6px 0}
51
+ .pane{flex:1;min-width:220px;border:1px solid var(--border);border-radius:6px;padding:6px 8px}
52
+ .pane .plabel{font-size:10px;text-transform:uppercase;color:var(--muted);margin-bottom:4px}
53
+ .pane .del,.pane .ins{white-space:pre-wrap;word-break:break-word;padding:0 4px;border-radius:3px}
54
+ .pane .del{background:#ff7b7222}
55
+ .pane .ins{background:#7ee78722}
56
+ .pane .del::before{content:"- ";color:#ff7b72}
57
+ .pane .ins::before{content:"+ ";color:#7ee787}
58
+ .chip-git{color:#ffa657}
59
+ .flip{font-size:11px;color:#ffa657}
60
+ .agree{font-size:11px;color:#7ee787}
61
+ .disagree{font-size:11px;color:#ff7b72}
62
+ .muted{color:var(--muted)}
63
+ .badge.pass{color:#7ee787}.badge.fail{color:#ff7b72}
64
+ .cat-wrong_approach{color:#ff7b72}.cat-incorrect_change{color:#ffa657}.cat-unwanted_action{color:#f0883e}
65
+ .cat-style_violation{color:#d2a8ff}.cat-premature{color:#79c0ff}
66
+ .cat-operational_directive,.cat-status_update,.cat-new_task,.cat-question,.cat-other{color:#8b949e}
@@ -0,0 +1,58 @@
1
+ header.top{display:flex;justify-content:space-between;align-items:flex-start;gap:16px}
2
+ .head-right{display:flex;align-items:center;gap:12px}
3
+ #stat-strip{color:var(--muted);font-size:12px;text-align:right}
4
+ #stats-toggle{background:var(--panel);color:var(--fg);border:1px solid var(--border);
5
+ border-radius:8px;padding:4px 12px;cursor:pointer;font:inherit}
6
+ #stats.hidden{display:none}
7
+ .comp h2{font-size:13px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;margin:18px 0 0}
8
+ .comp table.dist td{padding:3px 12px 3px 0}
9
+ .app{display:flex;align-items:flex-start}
10
+ #filters{width:236px;flex:none;position:sticky;top:0;max-height:100vh;overflow:auto;
11
+ padding:16px;border-right:1px solid var(--border)}
12
+ .facet-group{margin-bottom:18px}
13
+ .facet-group h3{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);margin:0 0 8px}
14
+ .facet-row{display:flex;align-items:center;gap:8px;width:100%;text-align:left;background:none;
15
+ border:0;color:var(--fg);font:inherit;padding:4px 6px;border-radius:6px;cursor:pointer}
16
+ .facet-row:hover{background:var(--panel)}
17
+ .facet-row.on{background:var(--panel);box-shadow:inset 2px 0 0 var(--accent)}
18
+ .facet-row.empty{opacity:.4}
19
+ .facet-row .fcheck{width:12px;color:var(--accent)}
20
+ .facet-row .fv{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:130px}
21
+ .facet-count{margin-left:auto;color:var(--muted);font-size:11px}
22
+ main{flex:1;min-width:0;padding:0 24px 48px}
23
+ #toolbar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;position:sticky;top:0;
24
+ background:var(--bg);padding:14px 0;z-index:2;border-bottom:1px solid var(--border)}
25
+ .views{display:flex;gap:6px}
26
+ .view-btn{background:var(--panel);color:var(--fg);border:1px solid var(--border);border-radius:14px;
27
+ padding:4px 12px;cursor:pointer;font:inherit}
28
+ .view-btn.active{background:var(--accent);color:#0d1117;border-color:var(--accent)}
29
+ #search{flex:1;min-width:200px;background:var(--panel);color:var(--fg);border:1px solid var(--border);
30
+ border-radius:6px;padding:6px 10px;font:inherit}
31
+ #count{color:var(--muted)}
32
+ #active{display:flex;flex-wrap:wrap;gap:6px;padding:10px 0}
33
+ #active:empty{display:none}
34
+ .achip{background:var(--panel);border:1px solid var(--border);border-radius:12px;color:var(--fg);
35
+ font:inherit;font-size:12px;padding:2px 10px;cursor:pointer}
36
+ .achip:hover{border-color:var(--accent)}
37
+ .achip.clear{color:var(--muted)}
38
+ .card{cursor:pointer}
39
+ .card:hover{border-color:var(--accent)}
40
+ .card.sel{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent)}
41
+ .card-head{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:8px}
42
+ .complaint{color:var(--accent);margin-top:6px}
43
+ .chip-lang{color:var(--accent);border:1px solid var(--accent)}
44
+ .st-refined{color:#7ee787}.st-accepted{color:#58a6ff}.st-noise{color:#8b949e}.st-unjudged{color:#6e7681}
45
+ details.diff{margin-top:8px}
46
+ details.diff summary{color:var(--accent);cursor:pointer}
47
+ #backdrop{position:fixed;inset:0;background:#00000066;opacity:0;pointer-events:none;
48
+ transition:opacity .22s ease;z-index:10}
49
+ #backdrop.open{opacity:1;pointer-events:auto}
50
+ #detail{position:fixed;top:0;right:0;height:100vh;width:min(760px,92vw);background:var(--bg);
51
+ border-left:1px solid var(--border);transform:translateX(100%);transition:transform .22s ease;
52
+ z-index:20;overflow:auto;box-shadow:-16px 0 40px #00000066}
53
+ #detail.open{transform:translateX(0)}
54
+ #detail-bar{position:sticky;top:0;display:flex;justify-content:flex-end;padding:8px;
55
+ background:var(--bg);border-bottom:1px solid var(--border)}
56
+ #detail-close{background:var(--panel);color:var(--fg);border:1px solid var(--border);
57
+ border-radius:8px;width:30px;height:30px;cursor:pointer;font:inherit}
58
+ #detail-body{padding:16px 24px}
@@ -0,0 +1,44 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>cc-pushback dashboard</title>
7
+ <link rel="stylesheet" href="/static/base.css">
8
+ <link rel="stylesheet" href="/static/dashboard.css">
9
+ </head>
10
+ <body>
11
+ <header class="top">
12
+ <div>
13
+ <h1>cc-pushback — training pairs</h1>
14
+ <div class="sub">refined pairs &amp; their lineage</div>
15
+ </div>
16
+ <div class="head-right">
17
+ <span id="stat-strip"></span>
18
+ <button id="stats-toggle">stats ▾</button>
19
+ </div>
20
+ </header>
21
+ <section id="stats" class="hidden"></section>
22
+ <div class="app">
23
+ <aside id="filters"></aside>
24
+ <main>
25
+ <div id="toolbar">
26
+ <div class="views">
27
+ <button class="view-btn active" data-view="pairs">refined pairs</button>
28
+ <button class="view-btn" data-view="candidates">all candidates</button>
29
+ </div>
30
+ <input id="search" type="search" placeholder="search…">
31
+ <span id="count"></span>
32
+ </div>
33
+ <div id="active"></div>
34
+ <div id="list"></div>
35
+ </main>
36
+ </div>
37
+ <div id="backdrop"></div>
38
+ <aside id="detail">
39
+ <div id="detail-bar"><button id="detail-close">✕</button></div>
40
+ <div id="detail-body"><p class="muted">select a row to see its lineage</p></div>
41
+ </aside>
42
+ <script type="module" src="/static/js/main.js"></script>
43
+ </body>
44
+ </html>
@@ -0,0 +1,38 @@
1
+ import { state } from "./state.js";
2
+ import { esc, chip, badge, diffPane } from "./dom.js";
3
+
4
+ export function evidenceHtml(ev) {
5
+ if (!ev) return "";
6
+ const git = ev.source === "git" ? '<span class="chip chip-git">git</span>' : "";
7
+ const correct = ev.correct ? diffPane("correct", ev.correct) : "";
8
+ return `<details class="diff"><summary>code evidence</summary>` +
9
+ `<div class="vhead"><span class="chip">${esc(ev.file_path)}</span>${git}</div>` +
10
+ `<div class="panes">${diffPane("incorrect", ev.incorrect)}${correct}</div></details>`;
11
+ }
12
+
13
+ function pairRow(r) {
14
+ const file = r.evidence ? `<span class="chip">${esc(r.evidence.file_path)}</span>` : "";
15
+ const lang = r.language ? `<span class="chip chip-lang">${esc(r.language)}</span>` : "";
16
+ return `<article class="card" data-key="${esc(r.dedup_key)}"><header class="card-head">` +
17
+ badge("cat-" + (r.category || "other"), r.category || "—") + badge("badge-" + r.source_kind, r.source_kind) +
18
+ `${chip(r.project)}<span class="chip">pair ${r.pair_index}</span>${file}${lang}</header>` +
19
+ `<div class="text"><pre>${esc(r.action)}</pre></div>` +
20
+ `<blockquote class="pverbatim">${esc(r.complaint_verbatim)}</blockquote>` +
21
+ `<div class="complaint">↳ ${esc(r.complaint)}</div>${evidenceHtml(r.evidence)}</article>`;
22
+ }
23
+
24
+ function candRow(r) {
25
+ const cat = r.category ? badge("cat-" + r.category, r.category) : "";
26
+ const pc = r.pair_count ? `<span class="chip">${r.pair_count} pairs</span>` : "";
27
+ const flip = r.flipped ? '<span class="flip">flip</span>' : "";
28
+ const agree = r.agreement ? `<span class="${esc(r.agreement)}">${esc(r.agreement)}</span>` : "";
29
+ const gold = r.golden ? badge(r.golden, "golden " + r.golden) : "";
30
+ return `<article class="card" data-key="${esc(r.dedup_key)}"><header class="card-head">` +
31
+ badge("st-" + r.status, r.status) + cat + badge("badge-" + r.source_kind, r.source_kind) +
32
+ `${chip(r.project)}${pc}${flip}${agree}${gold}</header>` +
33
+ `<div class="text"><pre>${esc(r.text)}</pre></div></article>`;
34
+ }
35
+
36
+ export function rowHtml(r) {
37
+ return state.view === "pairs" ? pairRow(r) : candRow(r);
38
+ }
@@ -0,0 +1,20 @@
1
+ import { state } from "./state.js";
2
+ import { lineageHtml } from "./lineage.js";
3
+
4
+ export function openDetail(key) {
5
+ for (const c of state.cards) c.classList.toggle("sel", c.dataset.key === key);
6
+ document.getElementById("detail").classList.add("open");
7
+ document.getElementById("backdrop").classList.add("open");
8
+ const body = document.getElementById("detail-body");
9
+ body.innerHTML = '<p class="muted">loading…</p>';
10
+ body.scrollTop = 0;
11
+ fetch("/api/lineage/" + encodeURIComponent(key))
12
+ .then((res) => res.ok ? res.json().then(lineageHtml) : '<p class="muted">no lineage</p>')
13
+ .then((html) => { body.innerHTML = html; });
14
+ }
15
+
16
+ export function closeDetail() {
17
+ document.getElementById("detail").classList.remove("open");
18
+ document.getElementById("backdrop").classList.remove("open");
19
+ for (const c of state.cards) c.classList.remove("sel");
20
+ }
@@ -0,0 +1,27 @@
1
+ // Small HTML-building helpers shared across the renderers. esc() is the single
2
+ // escape point — every value interpolated into markup must pass through it.
3
+ export function esc(s) {
4
+ return (s == null ? "" : String(s))
5
+ .replaceAll("&", "&amp;")
6
+ .replaceAll("<", "&lt;")
7
+ .replaceAll(">", "&gt;")
8
+ .replaceAll('"', "&quot;")
9
+ .replaceAll("'", "&#x27;");
10
+ }
11
+
12
+ export function chip(t) {
13
+ return t ? `<span class="chip">${esc(t)}</span>` : "";
14
+ }
15
+
16
+ export function badge(cls, t) {
17
+ return `<span class="badge ${cls}">${esc(t)}</span>`;
18
+ }
19
+
20
+ function diffLines(cls, text) {
21
+ return text.split("\n").map((l) => `<div class="${cls}">${esc(l)}</div>`).join("");
22
+ }
23
+
24
+ export function diffPane(label, side) {
25
+ return `<div class="pane"><div class="plabel">${esc(label)}</div>` +
26
+ diffLines("del", side.old) + diffLines("ins", side.new) + "</div>";
27
+ }
@@ -0,0 +1,116 @@
1
+ import { state } from "./state.js";
2
+ import { esc, badge } from "./dom.js";
3
+
4
+ // Facet groups, ordered as they render in the sidebar. `views` gates each group
5
+ // to the view where it means something; list facets are multi-select with
6
+ // drill-down counts, toggle facets are booleans.
7
+ export const GROUPS = [
8
+ { label: "Status", views: ["candidates"], list: { key: "status", get: (r) => r.status } },
9
+ { label: "Category", views: ["pairs", "candidates"], list: { key: "category", get: (r) => r.category, badge: "cat" } },
10
+ { label: "Kind", views: ["pairs", "candidates"], list: { key: "source_kind", get: (r) => r.source_kind, badge: "kind" } },
11
+ { label: "Project", views: ["pairs", "candidates"], list: { key: "project", get: (r) => r.project } },
12
+ { label: "Language", views: ["pairs"], list: { key: "language", get: (r) => r.language } },
13
+ { label: "Evidence", views: ["pairs"], toggles: [{ key: "evidence", text: "has code", match: (r) => !!r.evidence }] },
14
+ { label: "Quality", views: ["candidates"], toggles: [
15
+ { key: "golden", text: "golden", match: (r) => !!r.golden },
16
+ { key: "flipped", text: "flipped", match: (r) => !!r.flipped },
17
+ { key: "disagree", text: "disagreements", match: (r) => r.agreement === "disagree" },
18
+ ] },
19
+ ];
20
+
21
+ export function rowText(r) {
22
+ const parts = [];
23
+ const walk = (v) => {
24
+ if (v == null) return;
25
+ if (typeof v === "object") Object.values(v).forEach(walk);
26
+ else parts.push(String(v));
27
+ };
28
+ walk(r);
29
+ return parts.join(" ").toLowerCase();
30
+ }
31
+
32
+ function groupsFor(view) {
33
+ return GROUPS.filter((g) => g.views.includes(view));
34
+ }
35
+
36
+ function matchRow(r, except) {
37
+ for (const g of groupsFor(state.view)) {
38
+ if (g.list) {
39
+ if (g.list.key !== except) {
40
+ const sel = state.picks[g.list.key];
41
+ if (sel && sel.size && !sel.has(g.list.get(r))) return false;
42
+ }
43
+ } else {
44
+ for (const t of g.toggles) {
45
+ if (t.key !== except && state.flags[t.key] && !t.match(r)) return false;
46
+ }
47
+ }
48
+ }
49
+ return !state.q || r._text.includes(state.q);
50
+ }
51
+
52
+ function listCounts(facet) {
53
+ const m = new Map();
54
+ for (const r of state.rows) {
55
+ if (matchRow(r, facet.key)) {
56
+ const v = facet.get(r);
57
+ if (v != null) m.set(v, (m.get(v) || 0) + 1);
58
+ }
59
+ }
60
+ return m;
61
+ }
62
+
63
+ function facetGroupHtml(g) {
64
+ if (g.list) {
65
+ const counts = listCounts(g.list);
66
+ const sel = state.picks[g.list.key] || new Set();
67
+ const vals = [...new Set(state.rows.map(g.list.get).filter((v) => v != null))].sort();
68
+ if (!vals.length) return "";
69
+ const body = vals.map((v) => {
70
+ const n = counts.get(v) || 0;
71
+ const on = sel.has(v);
72
+ const label = g.list.badge === "cat" ? badge("cat-" + v, v)
73
+ : g.list.badge === "kind" ? badge("badge-" + v, v) : `<span class="fv">${esc(v)}</span>`;
74
+ return `<button class="facet-row${on ? " on" : ""}${n ? "" : " empty"}" data-facet="${esc(g.list.key)}" ` +
75
+ `data-value="${esc(v)}"><span class="fcheck">${on ? "✓" : ""}</span>${label}` +
76
+ `<span class="facet-count">${n}</span></button>`;
77
+ }).join("");
78
+ return `<div class="facet-group"><h3>${esc(g.label)}</h3>${body}</div>`;
79
+ }
80
+ const body = g.toggles.map((t) => {
81
+ const n = state.rows.filter((r) => matchRow(r, t.key) && t.match(r)).length;
82
+ const on = !!state.flags[t.key];
83
+ return `<button class="facet-row${on ? " on" : ""}${n ? "" : " empty"}" data-toggle="${esc(t.key)}">` +
84
+ `<span class="fcheck">${on ? "✓" : ""}</span><span class="fv">${esc(t.text)}</span>` +
85
+ `<span class="facet-count">${n}</span></button>`;
86
+ }).join("");
87
+ return `<div class="facet-group"><h3>${esc(g.label)}</h3>${body}</div>`;
88
+ }
89
+
90
+ function renderFacets() {
91
+ document.getElementById("filters").innerHTML = groupsFor(state.view).map(facetGroupHtml).join("");
92
+ }
93
+
94
+ function chipsHtml() {
95
+ const items = [];
96
+ for (const g of groupsFor(state.view)) {
97
+ if (g.list) for (const v of state.picks[g.list.key] || []) items.push([g.list.key, v, v]);
98
+ else for (const t of g.toggles) if (state.flags[t.key]) items.push(["@" + t.key, t.key, t.text]);
99
+ }
100
+ if (!items.length) return "";
101
+ return items.map(([k, v, label]) => `<button class="achip" data-k="${esc(k)}" data-v="${esc(v)}">${esc(label)} ✕</button>`).join("") +
102
+ '<button class="achip clear" data-clear="1">clear all ✕</button>';
103
+ }
104
+
105
+ export function apply() {
106
+ state.q = document.getElementById("search").value.trim().toLowerCase();
107
+ let shown = 0;
108
+ state.rows.forEach((r, i) => {
109
+ const ok = matchRow(r);
110
+ state.cards[i].style.display = ok ? "" : "none";
111
+ if (ok) shown++;
112
+ });
113
+ document.getElementById("count").textContent = shown + " / " + state.rows.length;
114
+ renderFacets();
115
+ document.getElementById("active").innerHTML = chipsHtml();
116
+ }
@@ -0,0 +1,84 @@
1
+ import { esc, diffPane } from "./dom.js";
2
+
3
+ // Renders one candidate's pipeline trail as a five-stage rail from the JSON the
4
+ // `/api/lineage/{key}` endpoint returns. The markup and class names mirror the
5
+ // shared stylesheet (base.css) exactly.
6
+
7
+ function goldenLabel(isPushback) {
8
+ return isPushback ? "pushback" : "noise";
9
+ }
10
+
11
+ function metaChips(meta) {
12
+ return meta.map((m) => `<span class="chip">${esc(m)}</span>`).join("");
13
+ }
14
+
15
+ function turnHtml(t) {
16
+ const cls = "turn turn-" + t.role + (t.is_trigger ? " turn-trigger" : "");
17
+ const tools = t.tool_calls ? `<span class="tools">${t.tool_calls} tool calls</span>` : "";
18
+ return `<div class="${cls}"><span class="role">${esc(t.role)}</span>${tools}<pre>${esc(t.preview)}</pre></div>`;
19
+ }
20
+
21
+ function contextHtml(ctx) {
22
+ if (!ctx) return "";
23
+ return `<details class="ctx"><summary>context (${ctx.turns.length} turns)</summary>${ctx.turns.map(turnHtml).join("")}</details>`;
24
+ }
25
+
26
+ function verdictHtml(v) {
27
+ const flag = v.flipped ? '<span class="flip">flipped across versions</span>' : "";
28
+ return `<div class="verdict stage-${esc(v.role)}"><div class="vhead">` +
29
+ `<span class="badge cat-${esc(v.category)}">${esc(v.category)}</span>` +
30
+ `<span class="chip">${esc(v.role)} v${v.prompt_version} · ${esc(v.model)}</span>` +
31
+ `<span class="chip">conf ${v.confidence.toFixed(2)}</span>` +
32
+ `<span class="chip">${goldenLabel(v.is_pushback)}</span>${flag}</div>` +
33
+ `<pre class="vsum">${esc(v.what_claude_did)}</pre>` +
34
+ `<pre class="vrat">${esc(v.rationale)}</pre></div>`;
35
+ }
36
+
37
+ function auditorHtml(a) {
38
+ if (!a) return '<p class="muted">not audited</p>';
39
+ return verdictHtml(a) + `<span class="${esc(a.agreement)}">${esc(a.agreement)} with judge</span>`;
40
+ }
41
+
42
+ function evidenceHtml(ev) {
43
+ const git = ev.source === "git" ? '<span class="chip chip-git">git</span>' : "";
44
+ const correct = ev.correct ? diffPane("correct", ev.correct) : "";
45
+ return `<div class="evidence"><div class="vhead"><span class="chip">${esc(ev.file_path)}</span>${git}</div>` +
46
+ `<div class="panes">${diffPane("incorrect", ev.incorrect)}${correct}</div></div>`;
47
+ }
48
+
49
+ function highlightSpans(text, spans) {
50
+ let out = esc(text);
51
+ for (const span of spans) out = out.split(esc(span)).join(`<mark>${esc(span)}</mark>`);
52
+ return out;
53
+ }
54
+
55
+ function refinerHtml(refiner) {
56
+ if (!refiner.pairs.length) return '<p class="muted">not yet refined</p>';
57
+ const cards = refiner.pairs.map((p) =>
58
+ `<div class="pair"><div class="vhead"><span class="chip">pair ${p.pair_index}</span>` +
59
+ `<span class="chip">v${p.prompt_version} · ${esc(p.model)}</span></div>` +
60
+ `<pre class="paction">${esc(p.action)}</pre>` +
61
+ `<blockquote class="pverbatim">${esc(p.complaint_verbatim)}</blockquote>` +
62
+ `<pre class="pcomplaint">${esc(p.complaint)}</pre>` +
63
+ `${p.evidence ? evidenceHtml(p.evidence) : ""}</div>`).join("");
64
+ return `<div class="orig"><pre>${highlightSpans(refiner.original, refiner.spans)}</pre></div>${cards}`;
65
+ }
66
+
67
+ function goldenHtml(g) {
68
+ if (!g) return '<p class="muted">not in golden set</p>';
69
+ return `<span class="badge ${esc(g.verdict)}">golden ${esc(g.verdict)} · expected ${esc(goldenLabel(g.expected))}</span>`;
70
+ }
71
+
72
+ export function lineageHtml(d) {
73
+ const det = d.detector;
74
+ return `<div class="lineage">` +
75
+ `<section class="stage stage-detector"><h3>1 · detector</h3>` +
76
+ `<header class="card-head"><span class="badge badge-${esc(det.source_kind)}">${esc(det.source_kind)}</span>` +
77
+ `<time>${esc(det.occurred_at)}</time>${metaChips(det.meta)}</header>` +
78
+ `<div class="text"><pre>${esc(det.text)}</pre></div>${contextHtml(det.context)}</section>` +
79
+ `<section class="stage stage-judge"><h3>2 · judge</h3>${d.judge.map(verdictHtml).join("") || '<p class="muted">unjudged</p>'}</section>` +
80
+ `<section class="stage stage-auditor"><h3>3 · auditor</h3>${auditorHtml(d.auditor)}</section>` +
81
+ `<section class="stage stage-refiner"><h3>4 · refiner — atomic pairs</h3>${refinerHtml(d.refiner)}</section>` +
82
+ `<section class="stage stage-golden"><h3>5 · golden gate</h3>${goldenHtml(d.golden)}</section>` +
83
+ `</div>`;
84
+ }
@@ -0,0 +1,64 @@
1
+ import { state } from "./state.js";
2
+ import { rowHtml } from "./cards.js";
3
+ import { apply, rowText } from "./filters.js";
4
+ import { loadStats } from "./stats.js";
5
+ import { openDetail, closeDetail } from "./detail.js";
6
+
7
+ function render() {
8
+ const listEl = document.getElementById("list");
9
+ listEl.innerHTML = state.rows.map(rowHtml).join("") || '<p class="muted">none</p>';
10
+ state.cards = [...listEl.querySelectorAll(".card")];
11
+ for (const c of state.cards) c.addEventListener("click", () => openDetail(c.dataset.key));
12
+ for (const s of listEl.querySelectorAll("details.diff summary")) s.addEventListener("click", (e) => e.stopPropagation());
13
+ apply();
14
+ }
15
+
16
+ async function load() {
17
+ const data = await (await fetch(state.view === "pairs" ? "/api/pairs" : "/api/candidates")).json();
18
+ state.rows = state.view === "pairs" ? data.pairs : data.candidates;
19
+ state.rows.forEach((r) => { r._text = rowText(r); });
20
+ state.picks = {};
21
+ state.flags = {};
22
+ document.getElementById("search").value = "";
23
+ render();
24
+ }
25
+
26
+ for (const b of document.querySelectorAll(".view-btn")) b.addEventListener("click", () => {
27
+ state.view = b.dataset.view;
28
+ for (const x of document.querySelectorAll(".view-btn")) x.classList.toggle("active", x === b);
29
+ closeDetail();
30
+ load();
31
+ });
32
+
33
+ document.getElementById("search").addEventListener("input", apply);
34
+
35
+ document.getElementById("filters").addEventListener("click", (e) => {
36
+ const row = e.target.closest(".facet-row");
37
+ if (!row) return;
38
+ if (row.dataset.facet) {
39
+ const set = state.picks[row.dataset.facet] || (state.picks[row.dataset.facet] = new Set());
40
+ set.has(row.dataset.value) ? set.delete(row.dataset.value) : set.add(row.dataset.value);
41
+ } else {
42
+ state.flags[row.dataset.toggle] = !state.flags[row.dataset.toggle];
43
+ }
44
+ apply();
45
+ });
46
+
47
+ document.getElementById("active").addEventListener("click", (e) => {
48
+ const c = e.target.closest(".achip");
49
+ if (!c) return;
50
+ if (c.dataset.clear) { state.picks = {}; state.flags = {}; }
51
+ else if (c.dataset.k[0] === "@") state.flags[c.dataset.k.slice(1)] = false;
52
+ else state.picks[c.dataset.k].delete(c.dataset.v);
53
+ apply();
54
+ });
55
+
56
+ document.getElementById("backdrop").addEventListener("click", closeDetail);
57
+ document.getElementById("detail-close").addEventListener("click", closeDetail);
58
+ document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeDetail(); });
59
+ document.getElementById("stats-toggle").addEventListener("click", (e) => {
60
+ e.target.textContent = document.getElementById("stats").classList.toggle("hidden") ? "stats ▾" : "stats ▴";
61
+ });
62
+
63
+ loadStats();
64
+ load();
@@ -0,0 +1,10 @@
1
+ // Shared mutable client state. ES-module live bindings make this single object
2
+ // the one source of truth every module reads and mutates.
3
+ export const state = {
4
+ view: "pairs",
5
+ rows: [],
6
+ cards: [],
7
+ picks: {},
8
+ flags: {},
9
+ q: "",
10
+ };
@@ -0,0 +1,37 @@
1
+ import { esc } from "./dom.js";
2
+
3
+ function statHtml(label, val) {
4
+ return `<div class="stat"><div class="n">${esc(val)}</div><div class="l">${esc(label)}</div></div>`;
5
+ }
6
+
7
+ function compHtml(comp) {
8
+ const cats = Object.keys(comp);
9
+ if (!cats.length) return "";
10
+ const kinds = [...new Set(cats.flatMap((c) => Object.keys(comp[c])))].sort();
11
+ const max = Math.max(1, ...cats.flatMap((c) => kinds.map((k) => comp[c][k] || 0)));
12
+ const head = `<tr><td></td>${kinds.map((k) => `<td>${esc(k)}</td>`).join("")}<td>total</td></tr>`;
13
+ const body = cats.map((c) => {
14
+ const tot = kinds.reduce((a, k) => a + (comp[c][k] || 0), 0);
15
+ const cells = kinds.map((k) => {
16
+ const n = comp[c][k] || 0;
17
+ return `<td>${n ? `<span class="bar" style="width:${Math.round(40 * n / max)}px"></span> ${n}` : "·"}</td>`;
18
+ }).join("");
19
+ return `<tr><td><span class="badge cat-${esc(c)}">${esc(c)}</span></td>${cells}<td>${tot}</td></tr>`;
20
+ }).join("");
21
+ return `<div class="comp"><h2>composition · accepted by category × kind</h2>` +
22
+ `<table class="dist"><tbody>${head}${body}</tbody></table></div>`;
23
+ }
24
+
25
+ export async function loadStats() {
26
+ const s = await (await fetch("/api/stats")).json();
27
+ const p = s.pipeline, c = s.corpus;
28
+ const statCards = [["events", c.total], ["accepted", p.accepted], ["refined", p.refined], ["pending", p.pending],
29
+ ["atomic pairs", p.total_pairs], ["pairs/event", p.pairs_per_event.toFixed(2)], ["noise", p.noise_judged],
30
+ ["unjudged", p.unjudged], ["audited", p.audited], ["disagree", p.disagree], ["flips", p.flips],
31
+ ["golden", p.golden_pass + "/" + p.golden_total]];
32
+ document.getElementById("stats").innerHTML = `<div class="stat-cards">${statCards.map((x) => statHtml(x[0], x[1])).join("")}</div>` +
33
+ compHtml(p.by_category_kind) +
34
+ (s.narrative ? `<div class="narrative">${esc(s.narrative)}</div>` : "");
35
+ document.getElementById("stat-strip").textContent =
36
+ `${c.total} events · ${p.accepted} accepted · ${p.refined} refined · ${p.total_pairs} pairs`;
37
+ }
@@ -0,0 +1,56 @@
1
+ """A thin shell-out to the ``claude`` CLI for a single headless text completion.
2
+
3
+ The run is driven by the shared ``spawnllm`` library: :func:`spawnllm.run`
4
+ spawns ``claude``, retries transient envelopes, and returns captured output, and
5
+ :func:`spawnllm.parse_result_envelope` unwraps the ``{is_error, result}`` JSON.
6
+ It uses the user's existing Claude Code auth (no API key), so the package stays
7
+ offline unless ``claude`` is actually on the path. The structured path lives in
8
+ :mod:`cc_transcript.judge` (``run_structured``/``structured_judge``).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import shutil
14
+
15
+ from spawnllm import ClaudeConfig, RunSpec, parse_result_envelope, run
16
+
17
+ CLAUDE_TIMEOUT = 180
18
+
19
+
20
+ def claude_available() -> bool:
21
+ """Returns whether the ``claude`` CLI is on ``PATH``."""
22
+ return shutil.which("claude") is not None
23
+
24
+
25
+ async def run_claude(prompt: str, *, system: str, model: str) -> str:
26
+ """Runs one headless ``claude`` turn and returns its text result.
27
+
28
+ Args:
29
+ prompt: The user message to send.
30
+ system: The system prompt.
31
+ model: The model to run, for example ``claude-sonnet-4-6``.
32
+
33
+ Returns:
34
+ The assistant's text response — the ``result`` field of the JSON output.
35
+
36
+ Raises:
37
+ subprocess.SubprocessError: If ``claude`` exits non-zero, times out, or
38
+ reports an error in its JSON envelope.
39
+ """
40
+ rr = await run(
41
+ RunSpec(
42
+ prompt=prompt,
43
+ model=model,
44
+ timeout=CLAUDE_TIMEOUT,
45
+ provider_configs={
46
+ "claude": ClaudeConfig(
47
+ system_prompt=system,
48
+ max_turns=1,
49
+ tools="",
50
+ disable_slash_commands=True,
51
+ output_format="json",
52
+ )
53
+ },
54
+ )
55
+ )
56
+ return parse_result_envelope(rr.stdout.encode(), argv=[], stderr=rr.stderr.encode())