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.
Files changed (64) hide show
  1. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/PKG-INFO +19 -54
  2. cc_transcript-6.0.0/README.md +111 -0
  3. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/filterspec.py +12 -0
  4. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/mining/__init__.py +34 -28
  5. cc_transcript-6.0.0/cc_transcript/mining/engine.py +91 -0
  6. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/mining/formats.py +9 -39
  7. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/mining/signals.py +104 -109
  8. cc_transcript-6.0.0/cc_transcript/mining/spec.py +524 -0
  9. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/pyproject.toml +1 -1
  10. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/rust/src/event.rs +3 -3
  11. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/rust/src/lib.rs +14 -1
  12. cc_transcript-6.0.0/rust/src/mining.rs +1250 -0
  13. cc_transcript-5.0.0/README.md +0 -146
  14. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/Cargo.lock +0 -0
  15. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/Cargo.toml +0 -0
  16. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/LICENSE +0 -0
  17. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/__init__.py +0 -0
  18. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/__main__.py +0 -0
  19. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/_parser_rs.pyi +0 -0
  20. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/activity.py +0 -0
  21. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/backend.py +0 -0
  22. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/builders.py +0 -0
  23. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/cli.py +0 -0
  24. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/context.py +0 -0
  25. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/corrections.py +0 -0
  26. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/corrections_cli.py +0 -0
  27. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/cost.py +0 -0
  28. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/decisions.py +0 -0
  29. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/discovery.py +0 -0
  30. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/disktruth.py +0 -0
  31. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/evidence.py +0 -0
  32. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/extract/__init__.py +0 -0
  33. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/extract/correct.py +0 -0
  34. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/ids.py +0 -0
  35. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/judge/__init__.py +0 -0
  36. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/judge/llm.py +0 -0
  37. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/judge/verdicts.py +0 -0
  38. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/messages.py +0 -0
  39. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/mining/candidates.py +0 -0
  40. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/mining/confidence.py +0 -0
  41. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/mining/filterspec.py +0 -0
  42. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/mining/sourcekind.py +0 -0
  43. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/mining/store.py +0 -0
  44. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/models.py +0 -0
  45. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/parser.py +0 -0
  46. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/py.typed +0 -0
  47. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/query.py +0 -0
  48. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/render.py +0 -0
  49. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/rust.py +0 -0
  50. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/sentiment/__init__.py +0 -0
  51. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/sentiment/buckets.py +0 -0
  52. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/sentiment/engine.py +0 -0
  53. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/sentiment/lexicon.py +0 -0
  54. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/sentiment/scorespec.py +0 -0
  55. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/store.py +0 -0
  56. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/cc_transcript/tools.py +0 -0
  57. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/rust/Cargo.toml +0 -0
  58. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/rust/data/afinn-en-165.tsv +0 -0
  59. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/rust/data/domain_overrides.tsv +0 -0
  60. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/rust/src/filter.rs +0 -0
  61. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/rust/src/lexicon.rs +0 -0
  62. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/rust/src/model.rs +0 -0
  63. {cc_transcript-5.0.0 → cc_transcript-6.0.0}/rust/src/score.rs +0 -0
  64. {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: 5.0.0
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
  [![Docs](https://img.shields.io/github/actions/workflow/status/yasyf/cc-transcript/docs.yml?branch=main&label=docs)](https://yasyf.github.io/cc-transcript/)
45
45
  [![License: PolyForm Noncommercial](https://img.shields.io/badge/License-PolyForm--Noncommercial--1.0.0-blue.svg)](https://github.com/yasyf/cc-transcript/blob/main/LICENSE)
46
46
 
47
- `cc-transcript` parses Claude Code's on-disk JSONL transcripts into a **typed superset event model** — every entry type preserved, nothing dropped — so you build on one faithful representation and apply your own semantic filtering on top.
47
+ A non-lossy parser for Claude Code's on-disk JSONL transcripts, plus a CLI to investigate sessions.
48
48
 
49
- The one property that makes it worth using: the parser is non-lossy. It never silently discards sidechains, synthetic turns, tool results, or unrecognized entry types; 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.
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
- uv add cc-transcript # or: pip install cc-transcript
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 look at the events:
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 filter from small builders and apply it. The builders return clauses,
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
- `NOISE_SPEC` is a ready-made spec for the universal structural noise (system reminders,
89
- local-command output, skill banners).
86
+ ## CLI
90
87
 
91
- ## The CLI
88
+ Every command runs as `uvx cc-transcript <command> ...`; see `--help` on any command for its flags.
92
89
 
93
- Six commands `list`, `show`, `grep`, `stats`, `slice`, `digest` — and every one runs as `uvx cc-transcript ...`, no install step. `list` finds transcripts, newest first:
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
- ```console
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
- ## What problems does this solve?
129
+ ## How it works
159
130
 
160
- - **One faithful parse.** Anything reading Claude Code transcripts re-implements the same JSONL quirks (str-or-list content, tool results nested two ways, envelope-less mode markers). This is that parser, written once and typed strictly.
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
+ ![cc-transcript banner](https://github.com/yasyf/cc-transcript/raw/main/docs/assets/readme-banner.webp)
4
+
5
+ [![PyPI](https://img.shields.io/pypi/v/cc-transcript.svg)](https://pypi.org/project/cc-transcript/)
6
+ [![Python](https://img.shields.io/pypi/pyversions/cc-transcript.svg)](https://pypi.org/project/cc-transcript/)
7
+ [![Docs](https://img.shields.io/github/actions/workflow/status/yasyf/cc-transcript/docs.yml?branch=main&label=docs)](https://yasyf.github.io/cc-transcript/)
8
+ [![License: PolyForm Noncommercial](https://img.shields.io/badge/License-PolyForm--Noncommercial--1.0.0-blue.svg)](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: each iterator recognizes a
6
- transcript shape and yields a :class:`MiningSignal` a neutral fact carrying a
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
- Provenance,
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 injects its own
4
- :class:`ReviewFormat` sequence into :func:`extract_all`.
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 re
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 = json.loads(text)
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))