cc-transcript 5.0.0__tar.gz → 6.0.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.
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/PKG-INFO +19 -54
- cc_transcript-6.0.0/README.md +111 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/filterspec.py +12 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/mining/__init__.py +34 -28
- cc_transcript-6.0.0/cc_transcript/mining/engine.py +91 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/mining/formats.py +9 -39
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/mining/signals.py +104 -109
- cc_transcript-6.0.0/cc_transcript/mining/spec.py +524 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/pyproject.toml +1 -1
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/rust/src/event.rs +3 -3
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/rust/src/lib.rs +14 -1
- cc_transcript-6.0.0/rust/src/mining.rs +1250 -0
- cc_transcript-5.0.0/README.md +0 -146
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/Cargo.lock +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/Cargo.toml +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/LICENSE +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/__init__.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/__main__.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/_parser_rs.pyi +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/activity.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/backend.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/builders.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/cli.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/context.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/corrections.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/corrections_cli.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/cost.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/decisions.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/discovery.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/disktruth.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/evidence.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/extract/__init__.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/extract/correct.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/ids.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/judge/__init__.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/judge/llm.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/judge/verdicts.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/messages.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/mining/candidates.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/mining/confidence.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/mining/filterspec.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/mining/sourcekind.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/mining/store.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/models.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/parser.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/py.typed +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/query.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/render.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/rust.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/sentiment/__init__.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/sentiment/buckets.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/sentiment/engine.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/sentiment/lexicon.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/sentiment/scorespec.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/store.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/tools.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/rust/Cargo.toml +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/rust/data/afinn-en-165.tsv +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/rust/data/domain_overrides.tsv +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/rust/src/filter.rs +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/rust/src/lexicon.rs +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/rust/src/model.rs +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/rust/src/score.rs +0 -0
- {cc_transcript-5.0.0 → cc_transcript-6.0.0}/rust/src/value.rs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cc-transcript
|
|
3
|
-
Version:
|
|
3
|
+
Version: 6.0.0
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Environment :: Console
|
|
6
6
|
Classifier: Intended Audience :: Developers
|
|
@@ -44,20 +44,19 @@ Project-URL: Repository, https://github.com/yasyf/cc-transcript
|
|
|
44
44
|
[](https://yasyf.github.io/cc-transcript/)
|
|
45
45
|
[](https://github.com/yasyf/cc-transcript/blob/main/LICENSE)
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
A non-lossy parser for Claude Code's on-disk JSONL transcripts, plus a CLI to investigate sessions.
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
`cc-transcript` parses Claude Code's JSONL transcripts into a typed superset event model — every entry type preserved, nothing dropped. The parser never silently discards sidechains, synthetic turns, tool results, or unrecognized entries; filtering is opt-in and lives in your code, not buried in the parser. It ships as a Python library, a `uvx`-runnable CLI, and a Claude Code plugin.
|
|
50
50
|
|
|
51
51
|
## Install
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
|
-
|
|
55
|
-
uvx cc-transcript --help # CLI, no install needed
|
|
54
|
+
uvx cc-transcript --help # run the CLI
|
|
56
55
|
```
|
|
57
56
|
|
|
58
57
|
## Quickstart
|
|
59
58
|
|
|
60
|
-
Discover the transcripts on disk, parse one, and
|
|
59
|
+
Discover the transcripts on disk, parse one, and walk the events:
|
|
61
60
|
|
|
62
61
|
```python
|
|
63
62
|
import anyio
|
|
@@ -75,8 +74,7 @@ for event in events:
|
|
|
75
74
|
print(f"assistant ({model}):", text[:80])
|
|
76
75
|
```
|
|
77
76
|
|
|
78
|
-
Compose a
|
|
79
|
-
`build_spec` assembles them into a spec, and `apply_spec` yields the survivors:
|
|
77
|
+
Filtering is opt-in. Compose a spec from small builders and apply it — `build_spec` assembles clauses, `apply_spec` yields the survivors. `NOISE_SPEC` is a ready-made spec for universal structural noise (system reminders, local-command output, skill banners):
|
|
80
78
|
|
|
81
79
|
```python
|
|
82
80
|
from cc_transcript import apply_spec, build_spec, keep_only, drop_junk, drop_short
|
|
@@ -85,22 +83,20 @@ spec = build_spec(keep_only("user", "assistant"), drop_junk("structural"), drop_
|
|
|
85
83
|
clean = list(apply_spec(events, spec))
|
|
86
84
|
```
|
|
87
85
|
|
|
88
|
-
|
|
89
|
-
local-command output, skill banners).
|
|
86
|
+
## CLI
|
|
90
87
|
|
|
91
|
-
|
|
88
|
+
Every command runs as `uvx cc-transcript <command> ...`; see `--help` on any command for its flags.
|
|
92
89
|
|
|
93
|
-
|
|
90
|
+
| Command | What it does |
|
|
91
|
+
| -------- | ------------ |
|
|
92
|
+
| `list` | Find transcripts on disk, newest first |
|
|
93
|
+
| `stats` | Summarize a session — counts, kinds, models, tools, token sizes, span |
|
|
94
|
+
| `show` | Render one compact line per event (`--signal` keeps the conversational spine) |
|
|
95
|
+
| `grep` | Search event content; hit indexes feed back into `show --range` |
|
|
96
|
+
| `slice` | Emit per-tool-call JSONL for a session UUID and time window (the bridge cc-review consumes) |
|
|
97
|
+
| `digest` | Generate and check the cross-language fixture corpus for the tool-digest contract |
|
|
94
98
|
|
|
95
|
-
|
|
96
|
-
$ uvx cc-transcript list --limit 3
|
|
97
|
-
2026-06-11 19:27 1.0MB ~/.claude/projects/-Users-yasyf-Code-captain-hook/d2ca206a-2561-4c2c-9a4c-3ecaac9f8443/subagents/agent-a804d9aea43a110b5.jsonl
|
|
98
|
-
2026-06-11 19:27 70.6KB ~/.claude/projects/-Users-yasyf-Code-cc-transcript/4c77d556-8694-4613-8f50-253d905da68e/subagents/agent-affd5dbe069a3660d.jsonl
|
|
99
|
-
2026-06-11 19:27 740.8KB ~/.claude/projects/-Users-yasyf-Code-cc-transcript/4c77d556-8694-4613-8f50-253d905da68e.jsonl
|
|
100
|
-
3 of 6608 transcripts under ~/.claude/projects
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
`stats` summarizes a session before you read any of it:
|
|
99
|
+
The output is compact by design — one line per event, hard truncation — so an agent triages a session in a few hundred tokens instead of paging through megabytes of JSONL:
|
|
104
100
|
|
|
105
101
|
```console
|
|
106
102
|
$ uvx cc-transcript stats ~/.claude/projects/-Users-yasyf-Code-cc-transcript/4c77d556-8694-4613-8f50-253d905da68e.jsonl
|
|
@@ -119,31 +115,6 @@ tool errors 0
|
|
|
119
115
|
sidechain 0
|
|
120
116
|
```
|
|
121
117
|
|
|
122
|
-
`show` renders one compact line per event; `--signal` keeps the conversational spine, and the index column is the event's position in the raw file:
|
|
123
|
-
|
|
124
|
-
```console
|
|
125
|
-
$ uvx cc-transcript show ~/.claude/projects/-Users-yasyf-Code-cc-transcript/4c77d556-8694-4613-8f50-253d905da68e.jsonl --signal --tail 4
|
|
126
|
-
189 asst 02:30:49 [claude-fable-5] Bash(rg -A3 'name = "great-docs"' /Users/yasyf/Code/cc-transcript/uv.lock | head -6; echo ---; rg -n "cl…)
|
|
127
|
-
194 asst 02:31:31 [claude-fable-5] "`cli:` support confirmed in the pinned great-docs. Checking the exact config shape before writing:"
|
|
128
|
-
195 asst 02:31:31 [claude-fable-5] TaskUpdate(8)
|
|
129
|
-
196 asst 02:31:32 [claude-fable-5] Bash(sed -n '40,60p;1750,1790p' /Users/yasyf/.cache/uv/git-v0/checkouts/a9f52a54772f9b4e/d318527/great_d…)
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
`grep` searches event content; hit indexes feed straight back into `show --range`:
|
|
133
|
-
|
|
134
|
-
```console
|
|
135
|
-
$ uvx cc-transcript grep -i "filterspec" --kind user --max-matches 3 ~/.claude/projects/-Users-yasyf-Code-cc-transcript/4c77d556-8694-4613-8f50-253d905da68e.jsonl
|
|
136
|
-
== ~/.claude/projects/-Users-yasyf-Code-cc-transcript/4c77d556-8694-4613-8f50-253d905da68e.jsonl
|
|
137
|
-
16 user 01:12:00 <-Agent (10161ch) ## Findings Report: cc-transcript Repository Based on a thorough exploration of `/Users/yasyf/Code/…
|
|
138
|
-
29 user 01:16:29 <-? (1378ch) /Users/yasyf/Code/cc-transcript/cc_transcript/: total 8648 drwxr-xr-x@ 19 yasyf staff 608 Jun 11 17…
|
|
139
|
-
69 user 01:36:17 <-Read (4247ch) 1 """Composable builder fragments for :class:`~cc_transcript.FilterSpec`. 2 3 Each fragment returns…
|
|
140
|
-
1 files, 3 matches
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
The output is compact by design — one line per event, hard truncation — so an agent triages a session in a few hundred tokens instead of paging through megabytes of JSONL.
|
|
144
|
-
|
|
145
|
-
The last two verbs serve programs, not people: `slice` emits per-tool-call JSONL for a session UUID and time window (the language-neutral bridge cc-review consumes), and `digest` generates and checks the cross-language fixture corpus for the tool-digest contract.
|
|
146
|
-
|
|
147
118
|
## Claude Code plugin
|
|
148
119
|
|
|
149
120
|
Install the bundled plugin from inside Claude Code:
|
|
@@ -155,15 +126,9 @@ Install the bundled plugin from inside Claude Code:
|
|
|
155
126
|
|
|
156
127
|
The plugin's skill teaches Claude to answer questions about its own history — "what did I ask yesterday", "find the session where we fixed the parser" — by funneling through the CLI's `list`, `stats`, `grep`, and `show` commands instead of reading raw JSONL.
|
|
157
128
|
|
|
158
|
-
##
|
|
129
|
+
## How it works
|
|
159
130
|
|
|
160
|
-
|
|
161
|
-
- **Non-lossy by design.** The event model is a superset: sidechains, `<synthetic>` turns, thinking blocks, and unrecognized entry types all survive parsing. You decide what to drop, via composable filter specs (`build_spec`).
|
|
162
|
-
- **Incremental ingestion.** `FileStateStore` tracks per-file mtimes in SQLite (WAL, safe across concurrent tasks) so re-runs only reparse changed files, and you compose your own writes in the same transaction.
|
|
163
|
-
- **Two engines, one contract.** A single `Backend` protocol with two implementations: `RustBackend` (PyO3 + rayon) is the default fast path, and `PythonBackend` is the readable reference — parity-asserted against each other. Filter specs are portable, so a spec built in Python runs Rust-side without giving up the fast path.
|
|
164
|
-
- **One activity spine.** `SessionActivity` lifts parsed events into turns, typed tool calls, and first-class edits; `ContextWindow` persists `EventRef`s plus labeled previews and hydrates back to full fidelity while the transcript lives; rendering is `Budget`-bounded in exactly one place.
|
|
165
|
-
- **Mining, judging, and sentiment.** `cc_transcript.mining` mines transcripts for user feedback — detectors, confidence calibration, candidate filtering, and the feedback store; `cc_transcript.judge` runs fidelity-aware LLM verdict passes over the mined corpus; `cc_transcript.sentiment` scores conversational sentiment per time-bucketed conversation window.
|
|
166
|
-
- **Transcript investigation for agents.** The CLI answers "what happened in that session" in a few hundred tokens, which is what makes the Claude Code plugin viable.
|
|
131
|
+
The event model is a superset: sidechains, `<synthetic>` turns, thinking blocks, and unrecognized entry types all survive parsing, so you decide what to drop via composable filter specs. Two engines sit behind one `Backend` protocol — `RustBackend` (PyO3 + rayon) is the default fast path, `PythonBackend` is the readable reference, parity-asserted against each other. On top of the spine, `SessionActivity` lifts events into turns and typed tool calls, `FileStateStore` tracks per-file mtimes in SQLite for incremental re-parses, and `cc_transcript.mining`/`judge`/`sentiment` extract feedback, run LLM verdict passes, and score conversational sentiment. The [docs site](https://yasyf.github.io/cc-transcript/) covers each of these in depth.
|
|
167
132
|
|
|
168
133
|
## Docs
|
|
169
134
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# cc-transcript
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/cc-transcript/)
|
|
6
|
+
[](https://pypi.org/project/cc-transcript/)
|
|
7
|
+
[](https://yasyf.github.io/cc-transcript/)
|
|
8
|
+
[](https://github.com/yasyf/cc-transcript/blob/main/LICENSE)
|
|
9
|
+
|
|
10
|
+
A non-lossy parser for Claude Code's on-disk JSONL transcripts, plus a CLI to investigate sessions.
|
|
11
|
+
|
|
12
|
+
`cc-transcript` parses Claude Code's JSONL transcripts into a typed superset event model — every entry type preserved, nothing dropped. The parser never silently discards sidechains, synthetic turns, tool results, or unrecognized entries; filtering is opt-in and lives in your code, not buried in the parser. It ships as a Python library, a `uvx`-runnable CLI, and a Claude Code plugin.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uvx cc-transcript --help # run the CLI
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quickstart
|
|
21
|
+
|
|
22
|
+
Discover the transcripts on disk, parse one, and walk the events:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import anyio
|
|
26
|
+
|
|
27
|
+
from cc_transcript import AssistantEvent, TranscriptDiscovery, UserEvent, parse_events_from_bytes
|
|
28
|
+
|
|
29
|
+
path = anyio.run(TranscriptDiscovery.find_transcripts)[0]
|
|
30
|
+
events = parse_events_from_bytes(path.read_bytes())
|
|
31
|
+
|
|
32
|
+
for event in events:
|
|
33
|
+
match event:
|
|
34
|
+
case UserEvent(text=text):
|
|
35
|
+
print("user:", text[:80])
|
|
36
|
+
case AssistantEvent(model=model, text=text):
|
|
37
|
+
print(f"assistant ({model}):", text[:80])
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Filtering is opt-in. Compose a spec from small builders and apply it — `build_spec` assembles clauses, `apply_spec` yields the survivors. `NOISE_SPEC` is a ready-made spec for universal structural noise (system reminders, local-command output, skill banners):
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from cc_transcript import apply_spec, build_spec, keep_only, drop_junk, drop_short
|
|
44
|
+
|
|
45
|
+
spec = build_spec(keep_only("user", "assistant"), drop_junk("structural"), drop_short(2))
|
|
46
|
+
clean = list(apply_spec(events, spec))
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## CLI
|
|
50
|
+
|
|
51
|
+
Every command runs as `uvx cc-transcript <command> ...`; see `--help` on any command for its flags.
|
|
52
|
+
|
|
53
|
+
| Command | What it does |
|
|
54
|
+
| -------- | ------------ |
|
|
55
|
+
| `list` | Find transcripts on disk, newest first |
|
|
56
|
+
| `stats` | Summarize a session — counts, kinds, models, tools, token sizes, span |
|
|
57
|
+
| `show` | Render one compact line per event (`--signal` keeps the conversational spine) |
|
|
58
|
+
| `grep` | Search event content; hit indexes feed back into `show --range` |
|
|
59
|
+
| `slice` | Emit per-tool-call JSONL for a session UUID and time window (the bridge cc-review consumes) |
|
|
60
|
+
| `digest` | Generate and check the cross-language fixture corpus for the tool-digest contract |
|
|
61
|
+
|
|
62
|
+
The output is compact by design — one line per event, hard truncation — so an agent triages a session in a few hundred tokens instead of paging through megabytes of JSONL:
|
|
63
|
+
|
|
64
|
+
```console
|
|
65
|
+
$ uvx cc-transcript stats ~/.claude/projects/-Users-yasyf-Code-cc-transcript/4c77d556-8694-4613-8f50-253d905da68e.jsonl
|
|
66
|
+
files 1
|
|
67
|
+
events 181
|
|
68
|
+
kinds other 68 · assistant 53 · user 33 · mode 22 · system 5
|
|
69
|
+
models claude-fable-5 53
|
|
70
|
+
tools TaskCreate 10 · Agent 5 · Read 5 · TaskUpdate 5 · Bash 2 · ToolSearch 2 · AskUserQuestion 1 · ExitPlanMode 1
|
|
71
|
+
text 14.8KB
|
|
72
|
+
thinking 8.7KB
|
|
73
|
+
tool io 89.0KB
|
|
74
|
+
sessions 1
|
|
75
|
+
span 2026-06-12 01:07:55 → 2026-06-12 02:28:03
|
|
76
|
+
interrupts 0
|
|
77
|
+
tool errors 0
|
|
78
|
+
sidechain 0
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Claude Code plugin
|
|
82
|
+
|
|
83
|
+
Install the bundled plugin from inside Claude Code:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
/plugin marketplace add yasyf/cc-transcript
|
|
87
|
+
/plugin install cc-transcript@cc-transcript
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The plugin's skill teaches Claude to answer questions about its own history — "what did I ask yesterday", "find the session where we fixed the parser" — by funneling through the CLI's `list`, `stats`, `grep`, and `show` commands instead of reading raw JSONL.
|
|
91
|
+
|
|
92
|
+
## How it works
|
|
93
|
+
|
|
94
|
+
The event model is a superset: sidechains, `<synthetic>` turns, thinking blocks, and unrecognized entry types all survive parsing, so you decide what to drop via composable filter specs. Two engines sit behind one `Backend` protocol — `RustBackend` (PyO3 + rayon) is the default fast path, `PythonBackend` is the readable reference, parity-asserted against each other. On top of the spine, `SessionActivity` lifts events into turns and typed tool calls, `FileStateStore` tracks per-file mtimes in SQLite for incremental re-parses, and `cc_transcript.mining`/`judge`/`sentiment` extract feedback, run LLM verdict passes, and score conversational sentiment. The [docs site](https://yasyf.github.io/cc-transcript/) covers each of these in depth.
|
|
95
|
+
|
|
96
|
+
## Docs
|
|
97
|
+
|
|
98
|
+
Each section of [the docs site](https://yasyf.github.io/cc-transcript/) is a focused guide:
|
|
99
|
+
|
|
100
|
+
- [Getting Started](https://yasyf.github.io/cc-transcript/docs/getting-started/index.html) — install, parse, filter, persist.
|
|
101
|
+
- [Filtering events](https://yasyf.github.io/cc-transcript/docs/guide/filtering-events.html) — clauses, specs, and `NOISE_SPEC`.
|
|
102
|
+
- [Scoring sentiment](https://yasyf.github.io/cc-transcript/docs/guide/scoring-sentiment.html) — the lexicon engine and score specs.
|
|
103
|
+
- [Rust/Python backends & parity](https://yasyf.github.io/cc-transcript/docs/guide/backends-and-parity.html) — the `Backend` protocol and parity testing.
|
|
104
|
+
- [Compose your own policy](https://yasyf.github.io/cc-transcript/docs/guide/compose-your-own-policy.html) — building a bespoke filtering policy.
|
|
105
|
+
- [Mining feedback](https://yasyf.github.io/cc-transcript/docs/guide/mining-feedback.html) — detectors, confidence, candidates, and verdicts.
|
|
106
|
+
- [The transcript CLI](https://yasyf.github.io/cc-transcript/docs/guide/transcript-cli.html) — `list`/`show`/`grep`/`stats` end to end.
|
|
107
|
+
- [API reference](https://yasyf.github.io/cc-transcript/reference/index.html) — the complete typed surface.
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
[PolyForm Noncommercial 1.0.0](LICENSE).
|
|
@@ -69,6 +69,12 @@ AGENT_INJECTION_GROUPS: tuple[tuple[str, str], ...] = (
|
|
|
69
69
|
INTERRUPT_MARKER_GROUPS: tuple[tuple[str, str], ...] = (("interrupt", r"\[Request interrupted by user"),)
|
|
70
70
|
STOP_HOOK_GROUPS: tuple[tuple[str, str], ...] = (("stop_hook", r"Stop hook feedback:"),)
|
|
71
71
|
|
|
72
|
+
# Raw CC-injected protocol strings carried in tool-result content: the denial banner
|
|
73
|
+
# and the markers that wrap the user's verbatim instruction in a rejected tool use.
|
|
74
|
+
DENIAL_PREFIX = "The user doesn't want to proceed with this tool use. The tool use was rejected"
|
|
75
|
+
USER_SAID_MARKER = "To tell you how to proceed, the user said:\n"
|
|
76
|
+
USER_SAID_TRAILER = "Note: The user's next message"
|
|
77
|
+
|
|
72
78
|
# Approve-and-advance directives: a user telling the agent to proceed/commit/push or
|
|
73
79
|
# to resume killed work. They follow an assistant turn but advance it rather than
|
|
74
80
|
# correcting it — the opposite of pushback — so a pushback consumer drops them. The
|
|
@@ -142,6 +148,11 @@ MILD_IMPATIENCE_GROUPS: tuple[tuple[str, str], ...] = (
|
|
|
142
148
|
),
|
|
143
149
|
)
|
|
144
150
|
|
|
151
|
+
# Hedging cues that soften a correction; a mining demote stage drops confidence on a hit.
|
|
152
|
+
HEDGE_GROUPS: tuple[tuple[str, str], ...] = (
|
|
153
|
+
("hedged", r"\b(?:maybe|perhaps|possibly|might|not sure|i think|i guess|if you (?:want|prefer)|up to you)\b"),
|
|
154
|
+
)
|
|
155
|
+
|
|
145
156
|
PORTABLE_GROUP_NAMES: frozenset[str] = frozenset(
|
|
146
157
|
name
|
|
147
158
|
for name, _ in (
|
|
@@ -152,6 +163,7 @@ PORTABLE_GROUP_NAMES: frozenset[str] = frozenset(
|
|
|
152
163
|
*COMMAND_ECHO_GROUPS,
|
|
153
164
|
*FRUSTRATION_GROUPS,
|
|
154
165
|
*MILD_IMPATIENCE_GROUPS,
|
|
166
|
+
*HEDGE_GROUPS,
|
|
155
167
|
)
|
|
156
168
|
)
|
|
157
169
|
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
# pyright: reportUnusedImport=false
|
|
3
3
|
"""The correction/feedback mining mechanism.
|
|
4
4
|
|
|
5
|
-
Neutral fact-detectors over Claude Code transcripts
|
|
6
|
-
|
|
5
|
+
Neutral fact-detectors over Claude Code transcripts, driven by a declarative
|
|
6
|
+
:class:`MiningSpec`: :func:`mine` interprets the spec and yields a
|
|
7
|
+
:class:`MiningSignal` per recognized transcript shape — a neutral fact carrying a
|
|
7
8
|
candidate trigger, confidence, and evidence, but no policy. Apps map signals to
|
|
8
9
|
their own candidate records with policy injected (their filter spec, their
|
|
9
10
|
disqualification rules, their review formats), capture each candidate's durable
|
|
@@ -11,6 +12,11 @@ disqualification rules, their review formats), capture each candidate's durable
|
|
|
11
12
|
:func:`~cc_transcript.context.capture_window`, and persist them through
|
|
12
13
|
:class:`FeedbackStore`. LLM verdict passes over the stored corpus live in
|
|
13
14
|
:mod:`cc_transcript.judge`.
|
|
15
|
+
|
|
16
|
+
The :class:`MiningSpec` is the mining analogue of :class:`~cc_transcript.FilterSpec`
|
|
17
|
+
and :class:`~cc_transcript.sentiment.ScoreSpec`: a frozen-dataclass tree with a JSON
|
|
18
|
+
contract (:func:`mining_spec_to_json`) that the Python reference executor here and,
|
|
19
|
+
when :func:`mining_spec_is_portable` holds, the Rust backend both interpret.
|
|
14
20
|
"""
|
|
15
21
|
|
|
16
22
|
from __future__ import annotations
|
|
@@ -30,6 +36,7 @@ from cc_transcript.mining.confidence import (
|
|
|
30
36
|
strong,
|
|
31
37
|
weak,
|
|
32
38
|
)
|
|
39
|
+
from cc_transcript.mining.engine import mine_signals, rehydrate_signal, rust_mine_backend
|
|
33
40
|
from cc_transcript.mining.filterspec import (
|
|
34
41
|
CandidateClause,
|
|
35
42
|
CandidateFilterSpec,
|
|
@@ -46,40 +53,15 @@ from cc_transcript.mining.filterspec import (
|
|
|
46
53
|
)
|
|
47
54
|
from cc_transcript.mining.formats import (
|
|
48
55
|
ReviewComment,
|
|
49
|
-
ReviewFormat,
|
|
50
56
|
StructuredFormat,
|
|
51
|
-
extract_all,
|
|
52
57
|
extract_structured,
|
|
53
58
|
)
|
|
54
59
|
from cc_transcript.mining.signals import (
|
|
55
|
-
DEFAULT_DETECTORS,
|
|
56
60
|
DENIAL_PREFIX,
|
|
57
|
-
EDIT_TOOLS,
|
|
58
|
-
REENTRY_LOOKBACK,
|
|
59
|
-
SUBAGENT_TOOLS,
|
|
60
61
|
USER_SAID_MARKER,
|
|
61
62
|
USER_SAID_TRAILER,
|
|
62
63
|
MiningSignal,
|
|
63
|
-
|
|
64
|
-
ScanText,
|
|
65
|
-
classify_provenance,
|
|
66
|
-
correction_text,
|
|
67
|
-
denial_results,
|
|
68
|
-
denied_tool_payload,
|
|
69
|
-
embedded_user_text,
|
|
70
|
-
interrupt_marker,
|
|
71
|
-
is_bare_interrupt_marker,
|
|
72
|
-
iter_interrupt_marker_signals,
|
|
73
|
-
iter_plan_reentry_signals,
|
|
74
|
-
iter_plan_rejection_signals,
|
|
75
|
-
iter_review_comment_signals,
|
|
76
|
-
iter_tool_denial_signals,
|
|
77
|
-
iter_user_message_signals,
|
|
78
|
-
last_edit_index,
|
|
79
|
-
marker_in,
|
|
80
|
-
nearest_assistant_index,
|
|
81
|
-
next_user_message,
|
|
82
|
-
review_scan_texts,
|
|
64
|
+
mine,
|
|
83
65
|
)
|
|
84
66
|
from cc_transcript.mining.sourcekind import (
|
|
85
67
|
INTERRUPT_REJECTION,
|
|
@@ -88,4 +70,28 @@ from cc_transcript.mining.sourcekind import (
|
|
|
88
70
|
TRANSCRIPT_MESSAGE,
|
|
89
71
|
SourceKind,
|
|
90
72
|
)
|
|
73
|
+
from cc_transcript.mining.spec import (
|
|
74
|
+
CALIBRATED_SPEC,
|
|
75
|
+
HEDGE_GROUPS,
|
|
76
|
+
USER_MESSAGE_SPEC,
|
|
77
|
+
Base,
|
|
78
|
+
BumpIfProximate,
|
|
79
|
+
BumpIfSubstantive,
|
|
80
|
+
CallableReviewFormat,
|
|
81
|
+
ConfidenceSpec,
|
|
82
|
+
ConfStage,
|
|
83
|
+
DemoteIfHedged,
|
|
84
|
+
DemoteIfShort,
|
|
85
|
+
DetectorName,
|
|
86
|
+
MiningSpec,
|
|
87
|
+
NoiseIfStructural,
|
|
88
|
+
Provenance,
|
|
89
|
+
ProvenanceSpec,
|
|
90
|
+
RegexReviewFormat,
|
|
91
|
+
ReviewFormat,
|
|
92
|
+
ReviewSpec,
|
|
93
|
+
mining_spec_is_portable,
|
|
94
|
+
mining_spec_to_json,
|
|
95
|
+
signal_to_dict,
|
|
96
|
+
)
|
|
91
97
|
from cc_transcript.mining.store import FEEDBACK_DDL, FeedbackStore, Stats, event_row
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Dual-backend mining entry — the mining analogue of
|
|
2
|
+
:func:`~cc_transcript.sentiment.engine.rust_score_backend`.
|
|
3
|
+
|
|
4
|
+
:func:`rust_mine_backend` resolves the Rust executor when the extension is built and
|
|
5
|
+
the :class:`~cc_transcript.mining.spec.MiningSpec` is portable, else None to fall
|
|
6
|
+
back to the Python reference. :func:`mine_signals` is the public dual-backend entry:
|
|
7
|
+
it takes RAW transcript bytes plus a spec and returns
|
|
8
|
+
:class:`~cc_transcript.mining.signals.MiningSignal` objects, routing to the Rust
|
|
9
|
+
parse+detect fast path or the Python :func:`~cc_transcript.mining.signals.mine` over
|
|
10
|
+
parsed events. The Rust path rehydrates the returned dicts into ``MiningSignal``
|
|
11
|
+
objects byte-identical to the Python path.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from typing import TYPE_CHECKING, Any
|
|
19
|
+
|
|
20
|
+
from cc_transcript.mining.confidence import CandidateSignal, Confidence
|
|
21
|
+
from cc_transcript.mining.signals import MiningSignal, mine
|
|
22
|
+
from cc_transcript.mining.sourcekind import SourceKind
|
|
23
|
+
from cc_transcript.mining.spec import mining_spec_is_portable, mining_spec_to_json
|
|
24
|
+
from cc_transcript.models import CcVersion, EventUuid, SessionId
|
|
25
|
+
from cc_transcript.parser import parse_events_from_bytes
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from collections.abc import Iterator, Mapping
|
|
29
|
+
from types import ModuleType
|
|
30
|
+
|
|
31
|
+
from cc_transcript.mining.spec import MiningSpec
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def rust_mine_backend(spec: MiningSpec) -> ModuleType | None:
|
|
35
|
+
"""The Rust mining executor when built and the spec is portable; else None → Python."""
|
|
36
|
+
if os.environ.get("CC_TRANSCRIPT_DISABLE_RUST"):
|
|
37
|
+
return None
|
|
38
|
+
try:
|
|
39
|
+
from cc_transcript import _parser_rs
|
|
40
|
+
except ImportError:
|
|
41
|
+
return None
|
|
42
|
+
if not hasattr(_parser_rs, "mine_signals") or not mining_spec_is_portable(spec):
|
|
43
|
+
return None
|
|
44
|
+
return _parser_rs
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def mine_signals(raw: bytes, spec: MiningSpec) -> Iterator[MiningSignal]:
|
|
48
|
+
"""Mines every :class:`MiningSignal` from raw transcript bytes via the active backend.
|
|
49
|
+
|
|
50
|
+
The Rust backend parses and detects over ``raw`` in one pass when the extension is
|
|
51
|
+
built and ``spec`` is portable; otherwise the bytes are parsed to events in Python
|
|
52
|
+
and the reference :func:`~cc_transcript.mining.signals.mine` runs. Both paths yield
|
|
53
|
+
byte-identical ``MiningSignal`` objects.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
raw: The raw bytes of a ``.jsonl`` transcript.
|
|
57
|
+
spec: The mining policy: which detectors run, with which scoring, provenance,
|
|
58
|
+
and review-format policy.
|
|
59
|
+
|
|
60
|
+
Yields:
|
|
61
|
+
Neutral mined facts, one per recognized transcript shape, in detector order.
|
|
62
|
+
"""
|
|
63
|
+
rust = rust_mine_backend(spec)
|
|
64
|
+
if rust is None:
|
|
65
|
+
yield from mine(parse_events_from_bytes(raw), spec)
|
|
66
|
+
return
|
|
67
|
+
yield from (rehydrate_signal(payload) for payload in rust.mine_signals(raw, mining_spec_to_json(spec)))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def rehydrate_signal(payload: Mapping[str, Any]) -> MiningSignal:
|
|
71
|
+
"""Rebuilds a :class:`MiningSignal` from a Rust ``mine_signals`` dict.
|
|
72
|
+
|
|
73
|
+
``occurred_at`` is parsed back from its RFC3339 string and the branded primitives
|
|
74
|
+
are re-wrapped, producing an object byte-identical to the Python reference path.
|
|
75
|
+
"""
|
|
76
|
+
return MiningSignal(
|
|
77
|
+
kind=SourceKind(payload["kind"]),
|
|
78
|
+
detector=payload["detector"],
|
|
79
|
+
session_id=SessionId(payload["session_id"]),
|
|
80
|
+
event_index=payload["event_index"],
|
|
81
|
+
event_uuid=EventUuid(payload["event_uuid"]),
|
|
82
|
+
occurred_at=datetime.fromisoformat(payload["occurred_at"]),
|
|
83
|
+
text=payload["text"],
|
|
84
|
+
cc_version=None if (version := payload["cc_version"]) is None else CcVersion(version),
|
|
85
|
+
trigger_index=payload["trigger_index"],
|
|
86
|
+
signal=CandidateSignal(
|
|
87
|
+
Confidence((signal := payload["signal"])["confidence"]), tuple(signal["reasons"]), signal["durable"]
|
|
88
|
+
),
|
|
89
|
+
lower_bound=payload["lower_bound"],
|
|
90
|
+
evidence=payload["evidence"],
|
|
91
|
+
)
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
"""Generic infrastructure for parsing structured code-review messages.
|
|
2
2
|
|
|
3
|
-
The concrete review formats are app policy; an app
|
|
4
|
-
:class
|
|
3
|
+
The concrete review formats are app policy; an app declares its formats as
|
|
4
|
+
:class:`~cc_transcript.mining.spec.RegexReviewFormat`,
|
|
5
|
+
:class:`~cc_transcript.mining.spec.CallableReviewFormat`, and
|
|
6
|
+
:class:`StructuredFormat` on a :class:`~cc_transcript.mining.spec.ReviewSpec`, which
|
|
7
|
+
:func:`~cc_transcript.mining.mine` interprets.
|
|
5
8
|
"""
|
|
6
9
|
|
|
7
10
|
from __future__ import annotations
|
|
8
11
|
|
|
9
|
-
import json
|
|
10
12
|
from dataclasses import dataclass, field
|
|
11
13
|
from typing import TYPE_CHECKING
|
|
12
14
|
|
|
15
|
+
import orjson
|
|
16
|
+
|
|
13
17
|
if TYPE_CHECKING:
|
|
14
|
-
import
|
|
15
|
-
from collections.abc import Callable, Iterator, Mapping, Sequence
|
|
18
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
16
19
|
from typing import Any
|
|
17
20
|
|
|
18
21
|
FINDING_KEYS: tuple[str, ...] = ("findings", "bugs", "improvements", "issues", "items", "verdicts")
|
|
@@ -35,39 +38,6 @@ class ReviewComment:
|
|
|
35
38
|
comment: str
|
|
36
39
|
|
|
37
40
|
|
|
38
|
-
@dataclass(frozen=True, slots=True)
|
|
39
|
-
class ReviewFormat:
|
|
40
|
-
"""A named code-review text format with a detector and extractor.
|
|
41
|
-
|
|
42
|
-
Attributes:
|
|
43
|
-
name: The format's identifier.
|
|
44
|
-
pattern: A pattern that matches when the format is present in a text.
|
|
45
|
-
extract: Parses a matching text into its review comments.
|
|
46
|
-
"""
|
|
47
|
-
|
|
48
|
-
name: str
|
|
49
|
-
pattern: re.Pattern[str]
|
|
50
|
-
extract: Callable[[str], tuple[ReviewComment, ...]]
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def extract_all(text: str, formats: Sequence[ReviewFormat]) -> Iterator[tuple[ReviewFormat, ReviewComment]]:
|
|
54
|
-
"""Yields every ``(format, comment)`` extracted by any matching format.
|
|
55
|
-
|
|
56
|
-
Args:
|
|
57
|
-
text: The raw review message text.
|
|
58
|
-
formats: The review formats to try, in order.
|
|
59
|
-
|
|
60
|
-
Yields:
|
|
61
|
-
One pair per extracted comment, across all formats whose pattern matches.
|
|
62
|
-
"""
|
|
63
|
-
return (
|
|
64
|
-
(fmt, comment)
|
|
65
|
-
for fmt in formats
|
|
66
|
-
if fmt.pattern.search(text)
|
|
67
|
-
for comment in fmt.extract(text)
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
|
|
71
41
|
@dataclass(frozen=True, slots=True)
|
|
72
42
|
class StructuredFormat:
|
|
73
43
|
"""A named JSON code-review format keyed by caller-supplied field aliases.
|
|
@@ -166,7 +136,7 @@ def extract_structured(
|
|
|
166
136
|
One pair per finding, across all formats. Non-JSON text yields nothing.
|
|
167
137
|
"""
|
|
168
138
|
try:
|
|
169
|
-
payload =
|
|
139
|
+
payload = orjson.loads(text)
|
|
170
140
|
except (ValueError, TypeError):
|
|
171
141
|
return
|
|
172
142
|
yield from ((fmt, comment) for fmt in structured_formats for comment in fmt.extract(payload))
|