cctx-cli 0.1.0__py3-none-any.whl
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.
- cctx/__init__.py +3 -0
- cctx/cli.py +375 -0
- cctx/diagnostician/__init__.py +81 -0
- cctx/diagnostician/aggregate.py +40 -0
- cctx/diagnostician/inflection.py +19 -0
- cctx/diagnostician/patterns/__init__.py +1 -0
- cctx/diagnostician/patterns/retry_loop.py +145 -0
- cctx/diagnostician/patterns/scope_creep.py +87 -0
- cctx/diagnostician/patterns/stale_context.py +147 -0
- cctx/discovery.py +185 -0
- cctx/exporters/__init__.py +0 -0
- cctx/exporters/csv.py +64 -0
- cctx/exporters/jsonl.py +64 -0
- cctx/harvest.py +173 -0
- cctx/models.py +269 -0
- cctx/parsers/__init__.py +1 -0
- cctx/parsers/claude_code.py +690 -0
- cctx/pricing.py +18 -0
- cctx/recommender/__init__.py +0 -0
- cctx/recommender/claude_md.py +131 -0
- cctx/recommender/evidence.py +46 -0
- cctx/renderers/__init__.py +0 -0
- cctx/renderers/report.py +58 -0
- cctx/renderers/templates/autopsy.html.j2 +249 -0
- cctx/renderers/terminal.py +251 -0
- cctx/renderers/trace_tui.py +291 -0
- cctx/tokenizer.py +77 -0
- cctx_cli-0.1.0.dist-info/METADATA +159 -0
- cctx_cli-0.1.0.dist-info/RECORD +31 -0
- cctx_cli-0.1.0.dist-info/WHEEL +4 -0
- cctx_cli-0.1.0.dist-info/entry_points.txt +2 -0
|
File without changes
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Patch generator — turns Findings into copy-pasteable CLAUDE.md diffs.
|
|
2
|
+
|
|
3
|
+
generate(diagnosis) -> Diagnosis (single-session path)
|
|
4
|
+
generate_from_evidence(evidence) -> list[Patch] (cross-session path)
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import dataclasses
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from cctx.models import FindingKind, Patch
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from cctx.models import Diagnosis, Finding, KindEvidence
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Patch templates (append-style unified diffs, v0)
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
_RETRY_LOOP_DIFF = """\
|
|
21
|
+
+## Retry discipline
|
|
22
|
+
+
|
|
23
|
+
+If the same command or file operation fails twice with the same error, stop and
|
|
24
|
+
+diagnose before retrying. Read the relevant file, check the full error message,
|
|
25
|
+
+confirm paths exist. Try a meaningfully different approach — never repeat the
|
|
26
|
+
+exact failing call a third time."""
|
|
27
|
+
|
|
28
|
+
_SCOPE_CREEP_DIFF = """\
|
|
29
|
+
+## Scope discipline
|
|
30
|
+
+
|
|
31
|
+
+Finish the stated task before picking up anything else. If you notice an adjacent
|
|
32
|
+
+issue while working, note it as a TODO comment but do not fix it unless explicitly
|
|
33
|
+
+asked. One task at a time."""
|
|
34
|
+
|
|
35
|
+
_STALE_CONTEXT_DIFF = """\
|
|
36
|
+
+## Context hygiene
|
|
37
|
+
+
|
|
38
|
+
+Large tool outputs (grep results, file reads over ~2K tokens) go stale quickly.
|
|
39
|
+
+After a result has served its purpose, do not carry it through 5+ additional turns
|
|
40
|
+
+without re-referencing it. Prefer re-running the tool over dragging stale context
|
|
41
|
+
+forward — the compaction system handles removal."""
|
|
42
|
+
|
|
43
|
+
_TEMPLATES: dict[FindingKind, tuple[str, str, str]] = {
|
|
44
|
+
# kind → (description, diff_body, target_file)
|
|
45
|
+
FindingKind.RETRY_LOOP: ("Add retry discipline rule", _RETRY_LOOP_DIFF, "CLAUDE.md"),
|
|
46
|
+
FindingKind.SCOPE_CREEP: ("Add scope discipline rule", _SCOPE_CREEP_DIFF, "CLAUDE.md"),
|
|
47
|
+
FindingKind.STALE_CONTEXT: ("Add context hygiene rule", _STALE_CONTEXT_DIFF, "CLAUDE.md"),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def summarize(finding: Finding) -> str:
|
|
52
|
+
ev = finding.evidence
|
|
53
|
+
match finding.kind:
|
|
54
|
+
case FindingKind.RETRY_LOOP:
|
|
55
|
+
occs = ev.get("occurrences", [])
|
|
56
|
+
if occs:
|
|
57
|
+
first = occs[0]
|
|
58
|
+
loop_len = ev.get("loop_length", "?")
|
|
59
|
+
return (
|
|
60
|
+
f"{first['call']}({first['key'][:40]}) failed {loop_len}×"
|
|
61
|
+
f" between turns {first['turn']}–{occs[-1]['turn']}"
|
|
62
|
+
)
|
|
63
|
+
return finding.summary
|
|
64
|
+
case FindingKind.SCOPE_CREEP:
|
|
65
|
+
phrases = ev.get("phrases", [])
|
|
66
|
+
if phrases:
|
|
67
|
+
return f"'{phrases[0]['phrase']}' at turn {phrases[0]['turn']}"
|
|
68
|
+
return finding.summary
|
|
69
|
+
case FindingKind.STALE_CONTEXT:
|
|
70
|
+
items = ev.get("stale_items", [])
|
|
71
|
+
if items:
|
|
72
|
+
worst = max(items, key=lambda i: i["token_turns"])
|
|
73
|
+
tokens_k = worst["content_tokens"] // 1000
|
|
74
|
+
cost_str = f", ~${finding.cost_usd:.2f}" if finding.cost_usd else ""
|
|
75
|
+
return (
|
|
76
|
+
f"{tokens_k}K-token {worst['tool_name']} result stale "
|
|
77
|
+
f"{worst['turns_stale']} turns "
|
|
78
|
+
f"(~{ev.get('total_token_turns', 0):,} token-turns{cost_str})"
|
|
79
|
+
)
|
|
80
|
+
return finding.summary
|
|
81
|
+
case _:
|
|
82
|
+
return finding.summary
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _make_patch(finding: Finding) -> Patch:
|
|
86
|
+
description, diff_body, target_file = _TEMPLATES[finding.kind]
|
|
87
|
+
return Patch(
|
|
88
|
+
target_file=target_file,
|
|
89
|
+
description=description,
|
|
90
|
+
unified_diff=diff_body,
|
|
91
|
+
finding_kind=finding.kind,
|
|
92
|
+
evidence_summary=summarize(finding),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def generate(diagnosis: Diagnosis) -> Diagnosis:
|
|
97
|
+
"""Populate patches on a Diagnosis. Returns a new Diagnosis; original unchanged."""
|
|
98
|
+
patches = [_make_patch(f) for f in diagnosis.findings]
|
|
99
|
+
return dataclasses.replace(diagnosis, patches=patches)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def generate_from_evidence(
|
|
103
|
+
evidence: dict[FindingKind, KindEvidence],
|
|
104
|
+
) -> list[Patch]:
|
|
105
|
+
"""Cross-session patch generation.
|
|
106
|
+
|
|
107
|
+
Like generate(), but appends an Evidence line when session_count >= 2:
|
|
108
|
+
Evidence: appeared in N of M sessions in the past window (~$X.XX wasted).
|
|
109
|
+
"""
|
|
110
|
+
patches = []
|
|
111
|
+
for kind, ev in evidence.items():
|
|
112
|
+
if kind not in _TEMPLATES:
|
|
113
|
+
continue
|
|
114
|
+
description, diff_body, target_file = _TEMPLATES[kind]
|
|
115
|
+
|
|
116
|
+
if ev.session_count >= 2:
|
|
117
|
+
evidence_line = (
|
|
118
|
+
f"\n+\n+Evidence: appeared in {ev.session_count} sessions "
|
|
119
|
+
f"(~${ev.total_waste_usd:.2f} wasted)."
|
|
120
|
+
)
|
|
121
|
+
diff_body = diff_body + evidence_line
|
|
122
|
+
|
|
123
|
+
example = ev.example_summaries[0] if ev.example_summaries else ""
|
|
124
|
+
patches.append(Patch(
|
|
125
|
+
target_file=target_file,
|
|
126
|
+
description=description,
|
|
127
|
+
unified_diff=diff_body,
|
|
128
|
+
finding_kind=kind,
|
|
129
|
+
evidence_summary=example,
|
|
130
|
+
))
|
|
131
|
+
return patches
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Cross-session evidence accumulation.
|
|
2
|
+
|
|
3
|
+
accumulate(diagnoses) -> dict[FindingKind, KindEvidence]
|
|
4
|
+
|
|
5
|
+
Counts how many sessions triggered each finding kind and sums waste cost.
|
|
6
|
+
Per the spec, session_count increments once per session per kind, regardless
|
|
7
|
+
of how many findings of that kind appear in one session.
|
|
8
|
+
Stores up to 3 example_summaries for the renderer.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from cctx.models import Diagnosis, FindingKind, KindEvidence
|
|
15
|
+
from cctx.recommender.claude_md import summarize
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from cctx.models import Finding
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _summarize_finding(finding: Finding) -> str:
|
|
22
|
+
return summarize(finding)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def accumulate(diagnoses: list[Diagnosis]) -> dict[FindingKind, KindEvidence]:
|
|
26
|
+
result: dict[FindingKind, KindEvidence] = {}
|
|
27
|
+
for diagnosis in diagnoses:
|
|
28
|
+
# Track which kinds we've already counted for this session to ensure
|
|
29
|
+
# session_count increments once per session per kind, not per finding.
|
|
30
|
+
seen_kinds: set[FindingKind] = set()
|
|
31
|
+
for finding in diagnosis.findings:
|
|
32
|
+
if finding.kind not in result:
|
|
33
|
+
result[finding.kind] = KindEvidence(
|
|
34
|
+
kind=finding.kind,
|
|
35
|
+
session_count=0,
|
|
36
|
+
total_waste_usd=0.0,
|
|
37
|
+
example_summaries=[],
|
|
38
|
+
)
|
|
39
|
+
ev = result[finding.kind]
|
|
40
|
+
if finding.kind not in seen_kinds:
|
|
41
|
+
ev.session_count += 1
|
|
42
|
+
seen_kinds.add(finding.kind)
|
|
43
|
+
ev.total_waste_usd += finding.cost_usd or 0.0
|
|
44
|
+
if len(ev.example_summaries) < 3:
|
|
45
|
+
ev.example_summaries.append(_summarize_finding(finding))
|
|
46
|
+
return result
|
|
File without changes
|
cctx/renderers/report.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""HTML report renderer for autopsy Diagnosis output.
|
|
2
|
+
|
|
3
|
+
render_html(diag, trace) -> str
|
|
4
|
+
Returns a fully self-contained HTML string with inlined CSS.
|
|
5
|
+
No external resources; no JavaScript.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from cctx.models import Diagnosis, Finding, SessionTrace
|
|
17
|
+
|
|
18
|
+
_TEMPLATES_DIR = Path(__file__).parent / "templates"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _flagged_index(findings: list[Finding]) -> dict[int, list[Finding]]:
|
|
22
|
+
index: dict[int, list[Finding]] = {}
|
|
23
|
+
for f in findings:
|
|
24
|
+
last = f.last_turn if f.last_turn is not None else f.first_turn
|
|
25
|
+
for tn in range(f.first_turn, last + 1):
|
|
26
|
+
index.setdefault(tn, []).append(f)
|
|
27
|
+
return index
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def render_html(diag: Diagnosis, trace: SessionTrace) -> str:
|
|
31
|
+
"""Render a Diagnosis as a self-contained HTML report string."""
|
|
32
|
+
env = Environment(
|
|
33
|
+
loader=FileSystemLoader(_TEMPLATES_DIR),
|
|
34
|
+
autoescape=select_autoescape(["html"]),
|
|
35
|
+
)
|
|
36
|
+
env.filters["to_json"] = lambda v: json.dumps(v, indent=2, default=str)
|
|
37
|
+
|
|
38
|
+
def _diff_highlight(diff_text: str) -> str:
|
|
39
|
+
lines = []
|
|
40
|
+
for line in diff_text.splitlines():
|
|
41
|
+
from markupsafe import Markup, escape
|
|
42
|
+
escaped = escape(line)
|
|
43
|
+
if line.startswith("+") and not line.startswith("+++"):
|
|
44
|
+
lines.append(Markup(f'<span class="add">{escaped}</span>'))
|
|
45
|
+
elif line.startswith("-") and not line.startswith("---"):
|
|
46
|
+
lines.append(Markup(f'<span class="del">{escaped}</span>'))
|
|
47
|
+
else:
|
|
48
|
+
lines.append(escaped)
|
|
49
|
+
return Markup("\n".join(lines))
|
|
50
|
+
|
|
51
|
+
env.filters["diff_highlight"] = _diff_highlight
|
|
52
|
+
|
|
53
|
+
tmpl = env.get_template("autopsy.html.j2")
|
|
54
|
+
return tmpl.render(
|
|
55
|
+
diag=diag,
|
|
56
|
+
trace=trace,
|
|
57
|
+
flagged=_flagged_index(diag.findings),
|
|
58
|
+
)
|
|
@@ -0,0 +1,249 @@
|
|
|
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>cctx autopsy — {{ diag.session_id }}</title>
|
|
7
|
+
<style>
|
|
8
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
|
|
11
|
+
font-size: 14px;
|
|
12
|
+
background: #0d1117;
|
|
13
|
+
color: #c9d1d9;
|
|
14
|
+
padding: 2rem 1rem;
|
|
15
|
+
}
|
|
16
|
+
main { max-width: 900px; margin: 0 auto; }
|
|
17
|
+
h1 { font-size: 1.4rem; color: #58a6ff; margin-bottom: .25rem; }
|
|
18
|
+
h2 { font-size: 1rem; color: #8b949e; text-transform: uppercase; letter-spacing: .08em; margin: 2rem 0 .75rem; }
|
|
19
|
+
code { font-family: monospace; font-size: .85em; background: #161b22; padding: .1em .35em; border-radius: 3px; }
|
|
20
|
+
a { color: #58a6ff; }
|
|
21
|
+
|
|
22
|
+
/* --- Verdict card --- */
|
|
23
|
+
.verdict-card {
|
|
24
|
+
background: #161b22;
|
|
25
|
+
border: 1px solid #30363d;
|
|
26
|
+
border-radius: 8px;
|
|
27
|
+
padding: 1.5rem;
|
|
28
|
+
margin-bottom: 2rem;
|
|
29
|
+
}
|
|
30
|
+
.verdict-card .meta { color: #8b949e; font-size: .8rem; margin-top: 1rem; }
|
|
31
|
+
.verdict { font-size: 1.1rem; font-weight: 600; margin: .5rem 0 1rem; }
|
|
32
|
+
.verdict.clean { color: #3fb950; }
|
|
33
|
+
.verdict.bad { color: #f85149; }
|
|
34
|
+
dl.costs { display: grid; grid-template-columns: auto 1fr; gap: .25rem .75rem; }
|
|
35
|
+
dl.costs dt { color: #8b949e; }
|
|
36
|
+
dl.costs dd { font-variant-numeric: tabular-nums; }
|
|
37
|
+
|
|
38
|
+
/* --- Badges --- */
|
|
39
|
+
.badge {
|
|
40
|
+
display: inline-block;
|
|
41
|
+
font-size: .7rem;
|
|
42
|
+
font-weight: 700;
|
|
43
|
+
padding: .15em .5em;
|
|
44
|
+
border-radius: 3px;
|
|
45
|
+
margin-right: .3rem;
|
|
46
|
+
vertical-align: middle;
|
|
47
|
+
}
|
|
48
|
+
.badge.kind-retry_loop { background: #f85149; color: #fff; }
|
|
49
|
+
.badge.kind-scope_creep { background: #d29922; color: #fff; }
|
|
50
|
+
.badge.kind-stale_context { background: #388bfd; color: #fff; }
|
|
51
|
+
.badge.sev-high { background: #6e1507; color: #f85149; border: 1px solid #f85149; }
|
|
52
|
+
.badge.sev-medium { background: #4d3500; color: #d29922; border: 1px solid #d29922; }
|
|
53
|
+
.badge.sev-low { background: #033a16; color: #3fb950; border: 1px solid #3fb950; }
|
|
54
|
+
|
|
55
|
+
/* --- Findings / patches --- */
|
|
56
|
+
details {
|
|
57
|
+
border: 1px solid #30363d;
|
|
58
|
+
border-radius: 6px;
|
|
59
|
+
margin-bottom: .6rem;
|
|
60
|
+
}
|
|
61
|
+
details[open] { border-color: #58a6ff44; }
|
|
62
|
+
summary {
|
|
63
|
+
list-style: none;
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
padding: .75rem 1rem;
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
flex-wrap: wrap;
|
|
69
|
+
gap: .35rem;
|
|
70
|
+
user-select: none;
|
|
71
|
+
}
|
|
72
|
+
summary::-webkit-details-marker { display: none; }
|
|
73
|
+
summary::before {
|
|
74
|
+
content: "▶";
|
|
75
|
+
font-size: .6rem;
|
|
76
|
+
color: #8b949e;
|
|
77
|
+
margin-right: .4rem;
|
|
78
|
+
transition: transform .15s;
|
|
79
|
+
}
|
|
80
|
+
details[open] summary::before { transform: rotate(90deg); }
|
|
81
|
+
.summary-text { flex: 1; color: #c9d1d9; }
|
|
82
|
+
.turns, .cost, .target { font-size: .75rem; color: #8b949e; }
|
|
83
|
+
|
|
84
|
+
.finding-body, .patch-body {
|
|
85
|
+
padding: .75rem 1rem 1rem;
|
|
86
|
+
border-top: 1px solid #21262d;
|
|
87
|
+
}
|
|
88
|
+
.finding-body p { color: #8b949e; margin-bottom: .5rem; }
|
|
89
|
+
|
|
90
|
+
pre.evidence, pre.diff {
|
|
91
|
+
background: #0d1117;
|
|
92
|
+
border: 1px solid #21262d;
|
|
93
|
+
border-radius: 4px;
|
|
94
|
+
padding: .75rem;
|
|
95
|
+
overflow-x: auto;
|
|
96
|
+
font-size: .8rem;
|
|
97
|
+
line-height: 1.5;
|
|
98
|
+
white-space: pre;
|
|
99
|
+
}
|
|
100
|
+
pre.diff .add { color: #3fb950; }
|
|
101
|
+
pre.diff .del { color: #f85149; }
|
|
102
|
+
|
|
103
|
+
.evidence-summary { color: #8b949e; font-size: .8rem; margin-bottom: .5rem; }
|
|
104
|
+
|
|
105
|
+
/* diff line coloring via spans */
|
|
106
|
+
|
|
107
|
+
/* --- Timeline --- */
|
|
108
|
+
.timeline {
|
|
109
|
+
display: flex;
|
|
110
|
+
flex-wrap: wrap;
|
|
111
|
+
gap: 3px;
|
|
112
|
+
margin-bottom: .5rem;
|
|
113
|
+
}
|
|
114
|
+
.turn-bar {
|
|
115
|
+
width: 28px;
|
|
116
|
+
height: 28px;
|
|
117
|
+
border-radius: 3px;
|
|
118
|
+
display: flex;
|
|
119
|
+
align-items: center;
|
|
120
|
+
justify-content: center;
|
|
121
|
+
font-size: .65rem;
|
|
122
|
+
font-weight: 600;
|
|
123
|
+
cursor: default;
|
|
124
|
+
border: 1px solid transparent;
|
|
125
|
+
background: #161b22;
|
|
126
|
+
color: #8b949e;
|
|
127
|
+
}
|
|
128
|
+
.turn-bar.role-user { background: #0d2940; color: #58a6ff; border-color: #1f4068; }
|
|
129
|
+
.turn-bar.role-assistant { background: #0d2018; color: #3fb950; border-color: #1a3a28; }
|
|
130
|
+
.turn-bar.role-tool_result { background: #1c1c2e; color: #8b949e; border-color: #2d2d4a; }
|
|
131
|
+
.turn-bar.flagged { background: #4d0000; color: #f85149; border-color: #7a0000; font-weight: 900; }
|
|
132
|
+
.timeline-legend {
|
|
133
|
+
display: flex;
|
|
134
|
+
gap: 1rem;
|
|
135
|
+
font-size: .75rem;
|
|
136
|
+
}
|
|
137
|
+
.legend-item { display: flex; align-items: center; gap: .3rem; color: #8b949e; }
|
|
138
|
+
.legend-item::before {
|
|
139
|
+
content: "";
|
|
140
|
+
display: inline-block;
|
|
141
|
+
width: 12px;
|
|
142
|
+
height: 12px;
|
|
143
|
+
border-radius: 2px;
|
|
144
|
+
border: 1px solid;
|
|
145
|
+
}
|
|
146
|
+
.legend-item.user::before { background: #0d2940; border-color: #1f4068; }
|
|
147
|
+
.legend-item.assistant::before { background: #0d2018; border-color: #1a3a28; }
|
|
148
|
+
.legend-item.tool_result::before { background: #1c1c2e; border-color: #2d2d4a; }
|
|
149
|
+
.legend-item.flagged::before { background: #4d0000; border-color: #7a0000; }
|
|
150
|
+
|
|
151
|
+
/* --- Footer --- */
|
|
152
|
+
footer {
|
|
153
|
+
margin-top: 3rem;
|
|
154
|
+
padding-top: 1rem;
|
|
155
|
+
border-top: 1px solid #21262d;
|
|
156
|
+
color: #484f58;
|
|
157
|
+
font-size: .75rem;
|
|
158
|
+
text-align: center;
|
|
159
|
+
}
|
|
160
|
+
</style>
|
|
161
|
+
</head>
|
|
162
|
+
<body>
|
|
163
|
+
<main>
|
|
164
|
+
|
|
165
|
+
<!-- Verdict card -->
|
|
166
|
+
<div class="verdict-card">
|
|
167
|
+
<h1>cctx autopsy</h1>
|
|
168
|
+
<p><code>{{ diag.session_id }}</code></p>
|
|
169
|
+
{% if diag.findings %}
|
|
170
|
+
<p class="verdict bad">{{ diag.findings|length }} {{ "finding" if diag.findings|length == 1 else "findings" }} · ${{ "%.2f"|format(diag.waste_cost_usd) }} waste</p>
|
|
171
|
+
{% else %}
|
|
172
|
+
<p class="verdict clean">Clean session</p>
|
|
173
|
+
{% endif %}
|
|
174
|
+
<dl class="costs">
|
|
175
|
+
<dt>Total cost</dt><dd>${{ "%.2f"|format(diag.total_cost_usd) }}</dd>
|
|
176
|
+
<dt>Waste attributed</dt><dd>${{ "%.2f"|format(diag.waste_cost_usd) }}</dd>
|
|
177
|
+
{% if diag.inflection_turn is not none %}<dt>Inflection turn</dt><dd>{{ diag.inflection_turn }}</dd>{% endif %}
|
|
178
|
+
</dl>
|
|
179
|
+
<p class="meta">Analysed {{ diag.analysed_at.strftime("%Y-%m-%d %H:%M UTC") }}</p>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<!-- Findings -->
|
|
183
|
+
{% if diag.findings %}
|
|
184
|
+
<section>
|
|
185
|
+
<h2>Findings</h2>
|
|
186
|
+
{% for f in diag.findings %}
|
|
187
|
+
<details class="finding">
|
|
188
|
+
<summary>
|
|
189
|
+
<span class="badge kind-{{ f.kind.value }}">{{ f.kind.value.replace("_", " ").upper() }}</span>
|
|
190
|
+
<span class="badge sev-{{ f.severity.value }}">{{ f.severity.value.upper() }}</span>
|
|
191
|
+
<span class="summary-text">{{ f.summary }}</span>
|
|
192
|
+
<span class="turns">turns {{ f.first_turn }}{% if f.last_turn is not none and f.last_turn != f.first_turn %}–{{ f.last_turn }}{% endif %}</span>
|
|
193
|
+
{% if f.cost_usd is not none %}<span class="cost">${{ "%.4f"|format(f.cost_usd) }}</span>{% endif %}
|
|
194
|
+
</summary>
|
|
195
|
+
<div class="finding-body">
|
|
196
|
+
<p><strong>Confidence:</strong> {{ f.confidence.value }}</p>
|
|
197
|
+
{% if f.evidence %}
|
|
198
|
+
<pre class="evidence">{{ f.evidence | to_json }}</pre>
|
|
199
|
+
{% endif %}
|
|
200
|
+
</div>
|
|
201
|
+
</details>
|
|
202
|
+
{% endfor %}
|
|
203
|
+
</section>
|
|
204
|
+
{% endif %}
|
|
205
|
+
|
|
206
|
+
<!-- Patches -->
|
|
207
|
+
{% if diag.patches %}
|
|
208
|
+
<section>
|
|
209
|
+
<h2>Recommended CLAUDE.md patches</h2>
|
|
210
|
+
{% for p in diag.patches %}
|
|
211
|
+
<details class="patch">
|
|
212
|
+
<summary>
|
|
213
|
+
<span class="badge kind-{{ p.finding_kind.value }}">{{ p.finding_kind.value.replace("_", " ").upper() }}</span>
|
|
214
|
+
<span class="summary-text">{{ p.description }}</span>
|
|
215
|
+
<span class="target">{{ p.target_file }}</span>
|
|
216
|
+
</summary>
|
|
217
|
+
<div class="patch-body">
|
|
218
|
+
<p class="evidence-summary">{{ p.evidence_summary }}</p>
|
|
219
|
+
<pre class="diff">{{ p.unified_diff | diff_highlight }}</pre>
|
|
220
|
+
</div>
|
|
221
|
+
</details>
|
|
222
|
+
{% endfor %}
|
|
223
|
+
</section>
|
|
224
|
+
{% endif %}
|
|
225
|
+
|
|
226
|
+
<!-- Turn timeline -->
|
|
227
|
+
<section>
|
|
228
|
+
<h2>Turn timeline</h2>
|
|
229
|
+
<div class="timeline">
|
|
230
|
+
{% for turn in trace.turns %}
|
|
231
|
+
{% set is_flagged = turn.turn_number in flagged %}
|
|
232
|
+
<div class="turn-bar role-{{ turn.role }}{% if is_flagged %} flagged{% endif %}"
|
|
233
|
+
title="Turn {{ turn.turn_number }}: {{ turn.role }}{% if is_flagged %} — {{ flagged[turn.turn_number]|map(attribute='kind')|map(attribute='value')|join(', ') }}{% endif %}">{{ turn.turn_number }}</div>
|
|
234
|
+
{% endfor %}
|
|
235
|
+
</div>
|
|
236
|
+
<div class="timeline-legend">
|
|
237
|
+
<span class="legend-item user">user</span>
|
|
238
|
+
<span class="legend-item assistant">assistant</span>
|
|
239
|
+
<span class="legend-item tool_result">tool result</span>
|
|
240
|
+
<span class="legend-item flagged">flagged</span>
|
|
241
|
+
</div>
|
|
242
|
+
</section>
|
|
243
|
+
|
|
244
|
+
</main>
|
|
245
|
+
<footer>
|
|
246
|
+
<p>Generated by <a href="https://github.com/jacquardlabs/cctx">cctx</a> · {{ diag.analysed_at.strftime("%Y-%m-%d") }}</p>
|
|
247
|
+
</footer>
|
|
248
|
+
</body>
|
|
249
|
+
</html>
|