cc-transcript 5.0.0__tar.gz → 7.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 (66) hide show
  1. cc_transcript-7.0.0/PKG-INFO +149 -0
  2. cc_transcript-7.0.0/README.md +111 -0
  3. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/extract/correct.py +4 -3
  4. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/filterspec.py +12 -0
  5. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/judge/__init__.py +1 -1
  6. cc_transcript-7.0.0/cc_transcript/judge/llm.py +62 -0
  7. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/mining/__init__.py +34 -28
  8. cc_transcript-7.0.0/cc_transcript/mining/engine.py +91 -0
  9. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/mining/formats.py +9 -39
  10. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/mining/signals.py +104 -109
  11. cc_transcript-7.0.0/cc_transcript/mining/spec.py +524 -0
  12. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/pyproject.toml +2 -2
  13. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/rust/src/event.rs +3 -3
  14. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/rust/src/lib.rs +14 -1
  15. cc_transcript-7.0.0/rust/src/mining.rs +1250 -0
  16. cc_transcript-5.0.0/PKG-INFO +0 -184
  17. cc_transcript-5.0.0/README.md +0 -146
  18. cc_transcript-5.0.0/cc_transcript/judge/llm.py +0 -99
  19. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/Cargo.lock +0 -0
  20. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/Cargo.toml +0 -0
  21. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/LICENSE +0 -0
  22. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/__init__.py +0 -0
  23. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/__main__.py +0 -0
  24. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/_parser_rs.pyi +0 -0
  25. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/activity.py +0 -0
  26. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/backend.py +0 -0
  27. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/builders.py +0 -0
  28. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/cli.py +0 -0
  29. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/context.py +0 -0
  30. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/corrections.py +0 -0
  31. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/corrections_cli.py +0 -0
  32. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/cost.py +0 -0
  33. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/decisions.py +0 -0
  34. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/discovery.py +0 -0
  35. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/disktruth.py +0 -0
  36. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/evidence.py +0 -0
  37. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/extract/__init__.py +0 -0
  38. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/ids.py +0 -0
  39. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/judge/verdicts.py +0 -0
  40. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/messages.py +0 -0
  41. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/mining/candidates.py +0 -0
  42. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/mining/confidence.py +0 -0
  43. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/mining/filterspec.py +0 -0
  44. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/mining/sourcekind.py +0 -0
  45. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/mining/store.py +0 -0
  46. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/models.py +0 -0
  47. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/parser.py +0 -0
  48. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/py.typed +0 -0
  49. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/query.py +0 -0
  50. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/render.py +0 -0
  51. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/rust.py +0 -0
  52. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/sentiment/__init__.py +0 -0
  53. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/sentiment/buckets.py +0 -0
  54. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/sentiment/engine.py +0 -0
  55. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/sentiment/lexicon.py +0 -0
  56. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/sentiment/scorespec.py +0 -0
  57. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/store.py +0 -0
  58. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/tools.py +0 -0
  59. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/rust/Cargo.toml +0 -0
  60. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/rust/data/afinn-en-165.tsv +0 -0
  61. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/rust/data/domain_overrides.tsv +0 -0
  62. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/rust/src/filter.rs +0 -0
  63. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/rust/src/lexicon.rs +0 -0
  64. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/rust/src/model.rs +0 -0
  65. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/rust/src/score.rs +0 -0
  66. {cc_transcript-5.0.0 → cc_transcript-7.0.0}/rust/src/value.rs +0 -0
@@ -0,0 +1,149 @@
1
+ Metadata-Version: 2.4
2
+ Name: cc-transcript
3
+ Version: 7.0.0
4
+ Classifier: Development Status :: 3 - Alpha
5
+ Classifier: Environment :: Console
6
+ Classifier: Intended Audience :: Developers
7
+ Classifier: Operating System :: OS Independent
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Programming Language :: Rust
11
+ Classifier: Typing :: Typed
12
+ Requires-Dist: aiosqlite>=0.20
13
+ Requires-Dist: anyio>=4.4
14
+ Requires-Dist: click>=8.1
15
+ Requires-Dist: orjson>=3.10
16
+ Requires-Dist: pytest>=8.0 ; extra == 'dev'
17
+ Requires-Dist: ty>=0.0.44 ; extra == 'dev'
18
+ Requires-Dist: ruff>=0.8 ; extra == 'dev'
19
+ Requires-Dist: spawnllm>=0.5.0 ; extra == 'llm'
20
+ Requires-Dist: spacy>=3.8 ; extra == 'sentiment'
21
+ Requires-Dist: afinn>=0.1 ; extra == 'sentiment'
22
+ Provides-Extra: dev
23
+ Provides-Extra: llm
24
+ Provides-Extra: sentiment
25
+ License-File: LICENSE
26
+ Summary: The Claude Code session-activity platform: a superset JSONL parser (Rust fast path, Python reference), typed tool calls with a cross-language content digest, turns/edits/evidence over one spine, context windows with labeled fidelity, unified decision and correction ledgers, LLM judging, and a transcript-investigation CLI.
27
+ Keywords: claude-code,claude,anthropic,transcripts,jsonl,parser,cli,agents,sentiment,feedback-mining
28
+ Author-email: Yasyf Mohamedali <yasyfm@gmail.com>
29
+ License-Expression: PolyForm-Noncommercial-1.0.0
30
+ Requires-Python: >=3.13
31
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
32
+ Project-URL: Changelog, https://github.com/yasyf/cc-transcript/blob/main/CHANGELOG.md
33
+ Project-URL: Documentation, https://yasyf.github.io/cc-transcript/
34
+ Project-URL: Homepage, https://github.com/yasyf/cc-transcript
35
+ Project-URL: Issues, https://github.com/yasyf/cc-transcript/issues
36
+ Project-URL: Repository, https://github.com/yasyf/cc-transcript
37
+
38
+ # cc-transcript
39
+
40
+ ![cc-transcript banner](https://github.com/yasyf/cc-transcript/raw/main/docs/assets/readme-banner.webp)
41
+
42
+ [![PyPI](https://img.shields.io/pypi/v/cc-transcript.svg)](https://pypi.org/project/cc-transcript/)
43
+ [![Python](https://img.shields.io/pypi/pyversions/cc-transcript.svg)](https://pypi.org/project/cc-transcript/)
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
+ [![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
+
47
+ A non-lossy parser for Claude Code's on-disk JSONL transcripts, plus a CLI to investigate sessions.
48
+
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
+
51
+ ## Install
52
+
53
+ ```bash
54
+ uvx cc-transcript --help # run the CLI
55
+ ```
56
+
57
+ ## Quickstart
58
+
59
+ Discover the transcripts on disk, parse one, and walk the events:
60
+
61
+ ```python
62
+ import anyio
63
+
64
+ from cc_transcript import AssistantEvent, TranscriptDiscovery, UserEvent, parse_events_from_bytes
65
+
66
+ path = anyio.run(TranscriptDiscovery.find_transcripts)[0]
67
+ events = parse_events_from_bytes(path.read_bytes())
68
+
69
+ for event in events:
70
+ match event:
71
+ case UserEvent(text=text):
72
+ print("user:", text[:80])
73
+ case AssistantEvent(model=model, text=text):
74
+ print(f"assistant ({model}):", text[:80])
75
+ ```
76
+
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):
78
+
79
+ ```python
80
+ from cc_transcript import apply_spec, build_spec, keep_only, drop_junk, drop_short
81
+
82
+ spec = build_spec(keep_only("user", "assistant"), drop_junk("structural"), drop_short(2))
83
+ clean = list(apply_spec(events, spec))
84
+ ```
85
+
86
+ ## CLI
87
+
88
+ Every command runs as `uvx cc-transcript <command> ...`; see `--help` on any command for its flags.
89
+
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 |
98
+
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:
100
+
101
+ ```console
102
+ $ uvx cc-transcript stats ~/.claude/projects/-Users-yasyf-Code-cc-transcript/4c77d556-8694-4613-8f50-253d905da68e.jsonl
103
+ files 1
104
+ events 181
105
+ kinds other 68 · assistant 53 · user 33 · mode 22 · system 5
106
+ models claude-fable-5 53
107
+ tools TaskCreate 10 · Agent 5 · Read 5 · TaskUpdate 5 · Bash 2 · ToolSearch 2 · AskUserQuestion 1 · ExitPlanMode 1
108
+ text 14.8KB
109
+ thinking 8.7KB
110
+ tool io 89.0KB
111
+ sessions 1
112
+ span 2026-06-12 01:07:55 → 2026-06-12 02:28:03
113
+ interrupts 0
114
+ tool errors 0
115
+ sidechain 0
116
+ ```
117
+
118
+ ## Claude Code plugin
119
+
120
+ Install the bundled plugin from inside Claude Code:
121
+
122
+ ```
123
+ /plugin marketplace add yasyf/cc-transcript
124
+ /plugin install cc-transcript@cc-transcript
125
+ ```
126
+
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.
128
+
129
+ ## How it works
130
+
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.
132
+
133
+ ## Docs
134
+
135
+ Each section of [the docs site](https://yasyf.github.io/cc-transcript/) is a focused guide:
136
+
137
+ - [Getting Started](https://yasyf.github.io/cc-transcript/docs/getting-started/index.html) — install, parse, filter, persist.
138
+ - [Filtering events](https://yasyf.github.io/cc-transcript/docs/guide/filtering-events.html) — clauses, specs, and `NOISE_SPEC`.
139
+ - [Scoring sentiment](https://yasyf.github.io/cc-transcript/docs/guide/scoring-sentiment.html) — the lexicon engine and score specs.
140
+ - [Rust/Python backends & parity](https://yasyf.github.io/cc-transcript/docs/guide/backends-and-parity.html) — the `Backend` protocol and parity testing.
141
+ - [Compose your own policy](https://yasyf.github.io/cc-transcript/docs/guide/compose-your-own-policy.html) — building a bespoke filtering policy.
142
+ - [Mining feedback](https://yasyf.github.io/cc-transcript/docs/guide/mining-feedback.html) — detectors, confidence, candidates, and verdicts.
143
+ - [The transcript CLI](https://yasyf.github.io/cc-transcript/docs/guide/transcript-cli.html) — `list`/`show`/`grep`/`stats` end to end.
144
+ - [API reference](https://yasyf.github.io/cc-transcript/reference/index.html) — the complete typed surface.
145
+
146
+ ## License
147
+
148
+ [PolyForm Noncommercial 1.0.0](LICENSE).
149
+
@@ -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).
@@ -25,7 +25,6 @@ import anyio.to_thread
25
25
  from pydantic import BaseModel
26
26
 
27
27
  from cc_transcript.evidence import GitFix, harvest_pairs, lower_pair
28
- from cc_transcript.judge.llm import run_structured_on
29
28
  from cc_transcript.render import Budget, hunk_lines
30
29
 
31
30
  if TYPE_CHECKING:
@@ -138,10 +137,12 @@ def usable_backend() -> LlmBackend | None:
138
137
  async def choose_pair(
139
138
  pairs: Sequence[CandidatePair], *, feedback: str, anchor_turn: int, tier: TModel, backend: LlmBackend | None
140
139
  ) -> CandidatePair | None:
140
+ from spawnllm import extract
141
+
141
142
  if backend is None:
142
143
  return max(pairs, key=lambda pair: pair.overlap)
143
- pick = await run_structured_on(
144
- backend, build_pick_prompt(feedback, pairs, anchor_turn=anchor_turn), response_model=CorrectionPick, tier=tier
144
+ pick = await extract(
145
+ build_pick_prompt(feedback, pairs, anchor_turn=anchor_turn), CorrectionPick, backend=backend, model=tier
145
146
  )
146
147
  return pairs[pick.candidate - 1] if pick.candidate is not None and 1 <= pick.candidate <= len(pairs) else None
147
148
 
@@ -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
 
@@ -12,7 +12,7 @@ prompts, the verdict model, and any SQL views over the verdict table.
12
12
 
13
13
  from __future__ import annotations
14
14
 
15
- from cc_transcript.judge.llm import resolved_model, run_structured, run_structured_on, structured_judge
15
+ from cc_transcript.judge.llm import default_backend, resolved_model, structured_judge
16
16
  from cc_transcript.judge.verdicts import (
17
17
  AuditEstimate,
18
18
  AuditSample,
@@ -0,0 +1,62 @@
1
+ """Headless structured completions via spawnllm's first ready CLI backend.
2
+
3
+ The structured call comes from the shared ``spawnllm`` library;
4
+ :func:`spawnllm.extract` resolves a backend, runs the spawn, and validates the
5
+ response into the model. The backend is whichever of spawnllm's CLIs is
6
+ installed and authenticated (:func:`spawnllm.select_backend`), using the
7
+ caller's existing CLI auth — no API key, no pinned provider. ``spawnllm`` and
8
+ ``pydantic`` load lazily inside each function, so importing the judge package
9
+ needs no extra installed.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from functools import cache
15
+ from typing import TYPE_CHECKING
16
+
17
+ if TYPE_CHECKING:
18
+ from collections.abc import Awaitable, Callable
19
+
20
+ from pydantic import BaseModel
21
+ from spawnllm import LlmBackend, TModel
22
+
23
+ LLM_TIMEOUT = 180
24
+
25
+
26
+ @cache
27
+ def default_backend() -> LlmBackend:
28
+ """The first installed, authenticated spawnllm backend, cached per process.
29
+
30
+ Raises:
31
+ spawnllm.BackendUnavailable: When no backend is installed and authenticated.
32
+ """
33
+ from spawnllm import select_backend
34
+
35
+ return select_backend()
36
+
37
+
38
+ def resolved_model(tier: TModel) -> str:
39
+ """The concrete model name the active backend runs for an abstract tier.
40
+
41
+ The verdict store keys on this string; it reflects whichever backend
42
+ :func:`spawnllm.select_backend` resolves, so a judged corpus stays coherent
43
+ within one backend environment.
44
+ """
45
+ return default_backend().models[tier]
46
+
47
+
48
+ def structured_judge[M: BaseModel](
49
+ response_model: type[M], *, tier: TModel, timeout: int = LLM_TIMEOUT
50
+ ) -> Callable[[str], Awaitable[M]]:
51
+ """Returns a prompt-to-verdict callable that plugs into :func:`run_verdicts`.
52
+
53
+ Each call runs one structured completion on the cached default backend via
54
+ :func:`spawnllm.extract`, which validates the response into ``response_model``.
55
+
56
+ Example:
57
+ >>> judge = structured_judge(Verdict, tier="medium")
58
+ >>> await run_verdicts(rows, prompt_for, judge, persist, concurrency=8)
59
+ """
60
+ from spawnllm import extract
61
+
62
+ return lambda prompt: extract(prompt, response_model, backend=default_backend(), model=tier, timeout=timeout)
@@ -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))