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.
- cc_transcript-7.0.0/PKG-INFO +149 -0
- cc_transcript-7.0.0/README.md +111 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/extract/correct.py +4 -3
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/filterspec.py +12 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/judge/__init__.py +1 -1
- cc_transcript-7.0.0/cc_transcript/judge/llm.py +62 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/mining/__init__.py +34 -28
- cc_transcript-7.0.0/cc_transcript/mining/engine.py +91 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/mining/formats.py +9 -39
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/mining/signals.py +104 -109
- cc_transcript-7.0.0/cc_transcript/mining/spec.py +524 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/pyproject.toml +2 -2
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/rust/src/event.rs +3 -3
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/rust/src/lib.rs +14 -1
- cc_transcript-7.0.0/rust/src/mining.rs +1250 -0
- cc_transcript-5.0.0/PKG-INFO +0 -184
- cc_transcript-5.0.0/README.md +0 -146
- cc_transcript-5.0.0/cc_transcript/judge/llm.py +0 -99
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/Cargo.lock +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/Cargo.toml +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/LICENSE +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/__init__.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/__main__.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/_parser_rs.pyi +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/activity.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/backend.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/builders.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/cli.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/context.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/corrections.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/corrections_cli.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/cost.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/decisions.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/discovery.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/disktruth.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/evidence.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/extract/__init__.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/ids.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/judge/verdicts.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/messages.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/mining/candidates.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/mining/confidence.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/mining/filterspec.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/mining/sourcekind.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/mining/store.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/models.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/parser.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/py.typed +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/query.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/render.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/rust.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/sentiment/__init__.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/sentiment/buckets.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/sentiment/engine.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/sentiment/lexicon.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/sentiment/scorespec.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/store.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/cc_transcript/tools.py +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/rust/Cargo.toml +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/rust/data/afinn-en-165.tsv +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/rust/data/domain_overrides.tsv +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/rust/src/filter.rs +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/rust/src/lexicon.rs +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/rust/src/model.rs +0 -0
- {cc_transcript-5.0.0 → cc_transcript-7.0.0}/rust/src/score.rs +0 -0
- {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
|
+

|
|
41
|
+
|
|
42
|
+
[](https://pypi.org/project/cc-transcript/)
|
|
43
|
+
[](https://pypi.org/project/cc-transcript/)
|
|
44
|
+
[](https://yasyf.github.io/cc-transcript/)
|
|
45
|
+
[](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
|
+

|
|
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).
|
|
@@ -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
|
|
144
|
-
|
|
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
|
|
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
|
|
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))
|