cc-transcript 4.0.0__tar.gz → 5.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 (61) hide show
  1. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/Cargo.lock +1 -1
  2. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/PKG-INFO +2 -2
  3. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/__init__.py +66 -0
  4. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/__main__.py +2 -0
  5. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/_parser_rs.pyi +4 -1
  6. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/corrections_cli.py +2 -3
  7. cc_transcript-5.0.0/cc_transcript/cost.py +138 -0
  8. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/decisions.py +10 -10
  9. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/extract/correct.py +2 -5
  10. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/judge/llm.py +20 -27
  11. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/judge/verdicts.py +4 -8
  12. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/mining/__init__.py +12 -1
  13. cc_transcript-5.0.0/cc_transcript/mining/formats.py +172 -0
  14. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/mining/signals.py +79 -24
  15. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/mining/store.py +2 -2
  16. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/models.py +190 -0
  17. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/parser.py +112 -0
  18. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/render.py +2 -2
  19. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/sentiment/engine.py +3 -1
  20. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/store.py +2 -9
  21. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/tools.py +4 -7
  22. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/pyproject.toml +2 -2
  23. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/rust/Cargo.toml +1 -1
  24. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/rust/src/event.rs +195 -3
  25. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/rust/src/lib.rs +9 -1
  26. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/rust/src/model.rs +9 -0
  27. cc_transcript-4.0.0/cc_transcript/mining/formats.py +0 -64
  28. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/Cargo.toml +0 -0
  29. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/LICENSE +0 -0
  30. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/README.md +0 -0
  31. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/activity.py +0 -0
  32. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/backend.py +0 -0
  33. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/builders.py +0 -0
  34. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/cli.py +0 -0
  35. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/context.py +0 -0
  36. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/corrections.py +0 -0
  37. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/discovery.py +0 -0
  38. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/disktruth.py +0 -0
  39. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/evidence.py +0 -0
  40. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/extract/__init__.py +0 -0
  41. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/filterspec.py +0 -0
  42. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/ids.py +2 -2
  43. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/judge/__init__.py +0 -0
  44. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/messages.py +0 -0
  45. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/mining/candidates.py +0 -0
  46. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/mining/confidence.py +0 -0
  47. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/mining/filterspec.py +0 -0
  48. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/mining/sourcekind.py +0 -0
  49. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/py.typed +0 -0
  50. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/query.py +0 -0
  51. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/rust.py +0 -0
  52. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/sentiment/__init__.py +0 -0
  53. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/sentiment/buckets.py +0 -0
  54. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/sentiment/lexicon.py +0 -0
  55. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/cc_transcript/sentiment/scorespec.py +0 -0
  56. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/rust/data/afinn-en-165.tsv +0 -0
  57. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/rust/data/domain_overrides.tsv +0 -0
  58. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/rust/src/filter.rs +0 -0
  59. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/rust/src/lexicon.rs +0 -0
  60. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/rust/src/score.rs +0 -0
  61. {cc_transcript-4.0.0 → cc_transcript-5.0.0}/rust/src/value.rs +0 -0
@@ -66,7 +66,7 @@ dependencies = [
66
66
 
67
67
  [[package]]
68
68
  name = "cc_transcript_parser"
69
- version = "0.6.0"
69
+ version = "0.7.0"
70
70
  dependencies = [
71
71
  "chrono",
72
72
  "crossbeam-channel",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cc-transcript
3
- Version: 4.0.0
3
+ Version: 5.0.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -16,7 +16,7 @@ Requires-Dist: orjson>=3.10
16
16
  Requires-Dist: pytest>=8.0 ; extra == 'dev'
17
17
  Requires-Dist: ty>=0.0.44 ; extra == 'dev'
18
18
  Requires-Dist: ruff>=0.8 ; extra == 'dev'
19
- Requires-Dist: spawnllm>=0.2.0 ; extra == 'llm'
19
+ Requires-Dist: spawnllm>=0.4.0 ; extra == 'llm'
20
20
  Requires-Dist: spacy>=3.8 ; extra == 'sentiment'
21
21
  Requires-Dist: afinn>=0.1 ; extra == 'sentiment'
22
22
  Provides-Extra: dev
@@ -57,19 +57,28 @@ EXPORTS: dict[str, str] = {
57
57
  ),
58
58
  "cc_transcript.models": (
59
59
  "AssistantEvent",
60
+ "CacheCreation",
60
61
  "CcVersion",
61
62
  "ContentBlock",
62
63
  "EntryMeta",
63
64
  "FallbackBlock",
65
+ "InitInfo",
66
+ "McpServer",
64
67
  "ModeEvent",
68
+ "ModelUsage",
65
69
  "OtherBlock",
66
70
  "OtherEvent",
71
+ "Plugin",
72
+ "PrintMessage",
73
+ "PrintResult",
74
+ "ServerToolUse",
67
75
  "SystemEvent",
68
76
  "TextBlock",
69
77
  "ThinkingBlock",
70
78
  "ToolResultBlock",
71
79
  "ToolUseBlock",
72
80
  "TranscriptEvent",
81
+ "Usage",
73
82
  "UserEvent",
74
83
  ),
75
84
  "cc_transcript.parser": (
@@ -77,6 +86,15 @@ EXPORTS: dict[str, str] = {
77
86
  "parse_event",
78
87
  "parse_events_async",
79
88
  "parse_events_from_bytes",
89
+ "parse_print_result",
90
+ ),
91
+ "cc_transcript.cost": (
92
+ "PRICING",
93
+ "CostBreakdown",
94
+ "ModelPricing",
95
+ "cost_of",
96
+ "cost_of_assistant",
97
+ "resolve_pricing",
80
98
  ),
81
99
  "cc_transcript.backend": ("Backend", "ParsedTranscript"),
82
100
  "cc_transcript.discovery": (
@@ -271,6 +289,24 @@ if TYPE_CHECKING:
271
289
  from cc_transcript.corrections import (
272
290
  Origin as Origin,
273
291
  )
292
+ from cc_transcript.cost import (
293
+ PRICING as PRICING,
294
+ )
295
+ from cc_transcript.cost import (
296
+ CostBreakdown as CostBreakdown,
297
+ )
298
+ from cc_transcript.cost import (
299
+ ModelPricing as ModelPricing,
300
+ )
301
+ from cc_transcript.cost import (
302
+ cost_of as cost_of,
303
+ )
304
+ from cc_transcript.cost import (
305
+ cost_of_assistant as cost_of_assistant,
306
+ )
307
+ from cc_transcript.cost import (
308
+ resolve_pricing as resolve_pricing,
309
+ )
274
310
  from cc_transcript.decisions import (
275
311
  DECISIONS_DDL as DECISIONS_DDL,
276
312
  )
@@ -415,6 +451,9 @@ if TYPE_CHECKING:
415
451
  from cc_transcript.models import (
416
452
  AssistantEvent as AssistantEvent,
417
453
  )
454
+ from cc_transcript.models import (
455
+ CacheCreation as CacheCreation,
456
+ )
418
457
  from cc_transcript.models import (
419
458
  CcVersion as CcVersion,
420
459
  )
@@ -427,15 +466,36 @@ if TYPE_CHECKING:
427
466
  from cc_transcript.models import (
428
467
  FallbackBlock as FallbackBlock,
429
468
  )
469
+ from cc_transcript.models import (
470
+ InitInfo as InitInfo,
471
+ )
472
+ from cc_transcript.models import (
473
+ McpServer as McpServer,
474
+ )
430
475
  from cc_transcript.models import (
431
476
  ModeEvent as ModeEvent,
432
477
  )
478
+ from cc_transcript.models import (
479
+ ModelUsage as ModelUsage,
480
+ )
433
481
  from cc_transcript.models import (
434
482
  OtherBlock as OtherBlock,
435
483
  )
436
484
  from cc_transcript.models import (
437
485
  OtherEvent as OtherEvent,
438
486
  )
487
+ from cc_transcript.models import (
488
+ Plugin as Plugin,
489
+ )
490
+ from cc_transcript.models import (
491
+ PrintMessage as PrintMessage,
492
+ )
493
+ from cc_transcript.models import (
494
+ PrintResult as PrintResult,
495
+ )
496
+ from cc_transcript.models import (
497
+ ServerToolUse as ServerToolUse,
498
+ )
439
499
  from cc_transcript.models import (
440
500
  SystemEvent as SystemEvent,
441
501
  )
@@ -454,6 +514,9 @@ if TYPE_CHECKING:
454
514
  from cc_transcript.models import (
455
515
  TranscriptEvent as TranscriptEvent,
456
516
  )
517
+ from cc_transcript.models import (
518
+ Usage as Usage,
519
+ )
457
520
  from cc_transcript.models import (
458
521
  UserEvent as UserEvent,
459
522
  )
@@ -469,6 +532,9 @@ if TYPE_CHECKING:
469
532
  from cc_transcript.parser import (
470
533
  parse_events_from_bytes as parse_events_from_bytes,
471
534
  )
535
+ from cc_transcript.parser import (
536
+ parse_print_result as parse_print_result,
537
+ )
472
538
  from cc_transcript.query import (
473
539
  FileRef as FileRef,
474
540
  )
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from cc_transcript.cli import cli
2
4
 
3
5
  cli()
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from cc_transcript.models import TranscriptEvent
3
+ from cc_transcript.models import PrintResult, TranscriptEvent
4
4
 
5
5
  class ParseStream:
6
6
  """A streaming handle over a rayon-parsed batch of transcript files."""
@@ -18,6 +18,9 @@ def stream_parse(paths: list[tuple[str, float]], prefetch: int, spec_json: str |
18
18
  are dropped during parsing, before any Python object is built.
19
19
  """
20
20
 
21
+ def parse_print_result(raw: bytes, /) -> PrintResult:
22
+ """Parses a 'claude -p --output-format json' result from raw JSON bytes."""
23
+
21
24
  def lexicon_available() -> bool:
22
25
  """Whether the UDPipe English model loaded (downloading + caching on first call)."""
23
26
 
@@ -11,7 +11,6 @@ from __future__ import annotations
11
11
  import json
12
12
  import time
13
13
  from dataclasses import asdict
14
- from typing import cast
15
14
 
16
15
  import click
17
16
  import orjson
@@ -54,7 +53,7 @@ def add(
54
53
  anchor: str,
55
54
  incorrect_file: str,
56
55
  ts_ms: int | None,
57
- origin: str | None,
56
+ origin: Origin | None,
58
57
  incorrect_old: str,
59
58
  incorrect_new: str,
60
59
  incorrect_digest: str | None,
@@ -79,7 +78,7 @@ def add(
79
78
  incorrect_file=incorrect_file,
80
79
  incorrect_old=incorrect_old,
81
80
  incorrect_new=incorrect_new,
82
- correction_origin=cast("Origin | None", origin),
81
+ correction_origin=origin,
83
82
  correction_file=correction_file,
84
83
  correction_old=correction_old,
85
84
  correction_new=correction_new,
@@ -0,0 +1,138 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from collections.abc import Mapping
8
+
9
+ from cc_transcript.models import AssistantEvent, Usage
10
+
11
+ MTOK = 1_000_000
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class ModelPricing:
16
+ """USD-per-million-token rates for a model family.
17
+
18
+ Standard service tier, standard (non-long-context) pricing — current Claude
19
+ 4.x models serve their 1M context at standard rates with no long-context
20
+ premium. Override via the `pricing` argument to cost_of for other tiers.
21
+
22
+ Attributes:
23
+ input: USD per million uncached input tokens.
24
+ output: USD per million output tokens.
25
+ cache_read: USD per million tokens served from the cache.
26
+ cache_write_5m: USD per million tokens written to the 5-minute cache.
27
+ cache_write_1h: USD per million tokens written to the 1-hour cache.
28
+ """
29
+
30
+ input: float
31
+ output: float
32
+ cache_read: float
33
+ cache_write_5m: float
34
+ cache_write_1h: float
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class CostBreakdown:
39
+ """The per-component and total USD cost of a single turn's token usage.
40
+
41
+ Attributes:
42
+ input_cost: USD cost of the uncached input tokens.
43
+ output_cost: USD cost of the output tokens.
44
+ cache_read_cost: USD cost of the cache-read tokens.
45
+ cache_write_cost: USD cost of the cache-creation tokens, summed across TTL buckets.
46
+ total: The sum of the four component costs.
47
+ """
48
+
49
+ input_cost: float
50
+ output_cost: float
51
+ cache_read_cost: float
52
+ cache_write_cost: float
53
+ total: float
54
+
55
+
56
+ PRICING: Mapping[str, ModelPricing] = {
57
+ "fable": ModelPricing(input=10.0, output=50.0, cache_read=1.0, cache_write_5m=12.5, cache_write_1h=20.0),
58
+ "opus": ModelPricing(input=5.0, output=25.0, cache_read=0.5, cache_write_5m=6.25, cache_write_1h=10.0),
59
+ "sonnet": ModelPricing(input=3.0, output=15.0, cache_read=0.3, cache_write_5m=3.75, cache_write_1h=6.0),
60
+ "haiku": ModelPricing(input=1.0, output=5.0, cache_read=0.1, cache_write_5m=1.25, cache_write_1h=2.0),
61
+ }
62
+
63
+
64
+ def resolve_pricing(model: str, *, pricing: Mapping[str, ModelPricing] = PRICING) -> ModelPricing:
65
+ """Return the pricing row whose family key is a substring of `model`.
66
+
67
+ Family keys are mutually exclusive substrings, so at most one matches —
68
+ ``"claude-haiku-4-5-20251001"`` resolves to haiku, ``"claude-opus-4-8"`` to
69
+ opus, and a bare ``"sonnet"`` to sonnet.
70
+
71
+ Args:
72
+ model: The model id to resolve.
73
+ pricing: The family-keyed pricing table to search.
74
+
75
+ Raises:
76
+ KeyError: If no family key is a substring of `model`.
77
+
78
+ Example:
79
+ >>> resolve_pricing("claude-opus-4-8").input
80
+ 5.0
81
+ """
82
+ for family, row in pricing.items():
83
+ if family in model:
84
+ return row
85
+ raise KeyError(f"no pricing for model: {model}")
86
+
87
+
88
+ def cost_of(usage: Usage, model: str, *, pricing: Mapping[str, ModelPricing] = PRICING) -> CostBreakdown:
89
+ """Compute the USD cost of a turn's token usage under a model's rates.
90
+
91
+ The cache-creation split prefers the per-TTL ``usage.cache_creation`` when
92
+ present, falling back to the flat ``usage.cache_creation_input_tokens`` as
93
+ 5-minute writes with no 1-hour share.
94
+
95
+ Args:
96
+ usage: The turn's token usage and cache accounting.
97
+ model: The model id whose rates apply.
98
+ pricing: The family-keyed pricing table to resolve against.
99
+
100
+ Returns:
101
+ The per-component and total cost.
102
+
103
+ Example:
104
+ >>> cost_of(usage, "claude-haiku-4-5-20251001").total
105
+ 0.05759
106
+ """
107
+ row = resolve_pricing(model, pricing=pricing)
108
+ write_5m, write_1h = (
109
+ (usage.cache_creation.ephemeral_5m_input_tokens, usage.cache_creation.ephemeral_1h_input_tokens)
110
+ if usage.cache_creation is not None
111
+ else (usage.cache_creation_input_tokens, 0)
112
+ )
113
+ input_cost = usage.input_tokens / MTOK * row.input
114
+ output_cost = usage.output_tokens / MTOK * row.output
115
+ cache_read_cost = usage.cache_read_input_tokens / MTOK * row.cache_read
116
+ cache_write_cost = write_5m / MTOK * row.cache_write_5m + write_1h / MTOK * row.cache_write_1h
117
+ return CostBreakdown(
118
+ input_cost=input_cost,
119
+ output_cost=output_cost,
120
+ cache_read_cost=cache_read_cost,
121
+ cache_write_cost=cache_write_cost,
122
+ total=input_cost + output_cost + cache_read_cost + cache_write_cost,
123
+ )
124
+
125
+
126
+ def cost_of_assistant(event: AssistantEvent, *, pricing: Mapping[str, ModelPricing] = PRICING) -> CostBreakdown | None:
127
+ """Compute the cost of an assistant turn, or None when it carries no usage.
128
+
129
+ Args:
130
+ event: The assistant turn whose usage is priced.
131
+ pricing: The family-keyed pricing table to resolve against.
132
+
133
+ Returns:
134
+ The cost breakdown, or None when ``event.usage`` is None.
135
+ """
136
+ if event.usage is None:
137
+ return None
138
+ return cost_of(event.usage, event.model, pricing=pricing)
@@ -1,6 +1,6 @@
1
1
  """The unified decision ledger shared by every hook and gate writer.
2
2
 
3
- One SQLite table (``decisions_v1``) records every allow/block/warn/nudge/note
3
+ One SQLite table (``decisions``) records every allow/block/warn/nudge/note
4
4
  decision across the family. Python (:class:`DecisionLog`) and cc-review's Go
5
5
  daemon write the same file directly, so the DDL below is vendored
6
6
  byte-identical into the Go repo and byte-compared in CI. Attribution joins by
@@ -22,7 +22,7 @@ if TYPE_CHECKING:
22
22
  from cc_transcript.ids import EventUuid, SessionId, ToolDigest
23
23
 
24
24
  DECISIONS_DDL = """\
25
- CREATE TABLE IF NOT EXISTS decisions_v1 (
25
+ CREATE TABLE IF NOT EXISTS decisions (
26
26
  id INTEGER PRIMARY KEY,
27
27
  ts_ms INTEGER NOT NULL,
28
28
  session_id TEXT NOT NULL,
@@ -39,11 +39,11 @@ CREATE TABLE IF NOT EXISTS decisions_v1 (
39
39
  UNIQUE (session_id, ts_ms, source, kind, tool_digest)
40
40
  );
41
41
 
42
- CREATE INDEX IF NOT EXISTS idx_decisions_v1_session_ts ON decisions_v1 (session_id, ts_ms);
42
+ CREATE INDEX IF NOT EXISTS idx_decisions_session_ts ON decisions (session_id, ts_ms);
43
43
 
44
- CREATE INDEX IF NOT EXISTS idx_decisions_v1_tool_digest ON decisions_v1 (tool_digest);
44
+ CREATE INDEX IF NOT EXISTS idx_decisions_tool_digest ON decisions (tool_digest);
45
45
 
46
- CREATE INDEX IF NOT EXISTS idx_decisions_v1_source_file ON decisions_v1 (source_file);
46
+ CREATE INDEX IF NOT EXISTS idx_decisions_source_file ON decisions (source_file);
47
47
  """
48
48
 
49
49
  Action = Literal["allow", "block", "warn", "nudge", "note"]
@@ -92,7 +92,7 @@ class Decision:
92
92
 
93
93
 
94
94
  class DecisionLog:
95
- """The ``decisions_v1`` ledger at ``~/.cc-transcript/decisions.db``.
95
+ """The ``decisions`` ledger at ``~/.cc-transcript/decisions.db``.
96
96
 
97
97
  Opened in WAL mode with a busy timeout because cc-review's Go daemon
98
98
  writes the same file concurrently. Durable by convention: rows are never
@@ -136,7 +136,7 @@ class DecisionLog:
136
136
  re-running the same integer-ms timestamp.
137
137
  """
138
138
  self.conn.execute(
139
- f"INSERT OR IGNORE INTO decisions_v1 ({DECISION_COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
139
+ f"INSERT OR IGNORE INTO decisions ({DECISION_COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
140
140
  (
141
141
  decision.ts_ms,
142
142
  decision.session_id,
@@ -158,7 +158,7 @@ class DecisionLog:
158
158
  return tuple(
159
159
  decision_of(row)
160
160
  for row in self.conn.execute(
161
- "SELECT * FROM decisions_v1 WHERE session_id = ? ORDER BY ts_ms, id", (session_id,)
161
+ "SELECT * FROM decisions WHERE session_id = ? ORDER BY ts_ms, id", (session_id,)
162
162
  )
163
163
  )
164
164
 
@@ -177,7 +177,7 @@ class DecisionLog:
177
177
  ``[near_ts_ms - window_ms, near_ts_ms]``.
178
178
  """
179
179
  row = self.conn.execute(
180
- "SELECT * FROM decisions_v1 WHERE session_id = ? AND tool_digest = ? AND ts_ms BETWEEN ? AND ?"
180
+ "SELECT * FROM decisions WHERE session_id = ? AND tool_digest = ? AND ts_ms BETWEEN ? AND ?"
181
181
  " ORDER BY ts_ms DESC, id DESC LIMIT 1",
182
182
  (session_id, tool_digest, near_ts_ms - window_ms, near_ts_ms),
183
183
  ).fetchone()
@@ -205,7 +205,7 @@ class DecisionLog:
205
205
  """
206
206
  kind_clause, kind_params = ("AND kind = ? ", (kind,)) if kind is not None else ("", ())
207
207
  row = self.conn.execute(
208
- f"SELECT * FROM decisions_v1 WHERE session_id = ? AND event = ? {kind_clause}AND ts_ms BETWEEN ? AND ?"
208
+ f"SELECT * FROM decisions WHERE session_id = ? AND event = ? {kind_clause}AND ts_ms BETWEEN ? AND ?"
209
209
  " ORDER BY ABS(ts_ms - ?), id DESC LIMIT 1",
210
210
  (session_id, event, *kind_params, near_ts_ms - window_ms, near_ts_ms + window_ms, near_ts_ms),
211
211
  ).fetchone()
@@ -167,12 +167,9 @@ async def extract_correction(
167
167
  if log.for_anchor(anchor.session_id, anchor.event_uuid):
168
168
  return None
169
169
  pairs = await anyio.to_thread.run_sync(partial(harvest_pairs, activity, anchor, repo=repo))
170
- if not pairs:
170
+ if not pairs or (turn := activity.turn_of(anchor)) is None:
171
171
  return None
172
- turn = activity.turn_of(anchor)
173
- pick = await choose_pair(
174
- pairs, feedback=feedback, anchor_turn=turn.index if turn else 0, tier=tier, backend=backend
175
- )
172
+ pick = await choose_pair(pairs, feedback=feedback, anchor_turn=turn.index, tier=tier, backend=backend)
176
173
  if pick is None or (row := lower_pair(activity, anchor, pick, source=source)) is None:
177
174
  return None
178
175
  if repo is not None:
@@ -1,22 +1,18 @@
1
1
  """Headless structured completions via spawnllm's first ready CLI backend.
2
2
 
3
- Argv construction and envelope parsing come from the shared ``spawnllm`` library;
4
- the spawn stays local (``anyio.run_process``). The backend is whichever of
5
- spawnllm's CLIs is installed and authenticated (:func:`spawnllm.select_backend`),
6
- using the caller's existing CLI auth — no API key, no pinned provider.
7
- ``spawnllm`` and ``pydantic`` load lazily inside each function, so importing the
8
- judge package needs no extra installed.
3
+ Run dispatch and envelope parsing come from the shared ``spawnllm`` library;
4
+ :func:`spawnllm.run` drives the spawn and built-in transient retry. The backend
5
+ is whichever of spawnllm's CLIs is installed and authenticated
6
+ (:func:`spawnllm.select_backend`), using the caller's existing CLI auth — no API
7
+ key, no pinned provider. ``spawnllm`` and ``pydantic`` load lazily inside each
8
+ function, so importing the judge package needs no extra installed.
9
9
  """
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
- import os
14
- import subprocess
15
13
  from functools import cache
16
14
  from typing import TYPE_CHECKING, cast
17
15
 
18
- import anyio
19
-
20
16
  if TYPE_CHECKING:
21
17
  from collections.abc import Awaitable, Callable
22
18
 
@@ -55,27 +51,24 @@ async def run_structured_on[M: BaseModel](
55
51
 
56
52
  A caller that resolves a backend once per pass passes it here, so a large pass
57
53
  probes auth once; :func:`run_structured` is the select-and-run convenience.
54
+ :func:`spawnllm.run` retries transient failures with backoff before returning.
58
55
 
59
56
  Raises:
60
- subprocess.SubprocessError: If the backend CLI exits non-zero or times out.
57
+ subprocess.TimeoutExpired: If the backend CLI outlives ``timeout``.
61
58
  pydantic.ValidationError: If the response does not match the schema.
62
59
  """
63
- from spawnllm import resolve_schema_path, schema_for
64
-
65
- argv, stdin = backend.invocation(
66
- prompt,
67
- model=backend.models[tier],
68
- schema_path=resolve_schema_path(backend, schema_for(response_model)),
69
- agent=False,
60
+ from spawnllm import RunSpec, run
61
+
62
+ rr = await run(
63
+ RunSpec(
64
+ prompt=prompt,
65
+ model=backend.models[tier],
66
+ schema=backend.schema_for(response_model),
67
+ timeout=timeout,
68
+ ),
69
+ backend=backend,
70
70
  )
71
- try:
72
- with anyio.fail_after(timeout):
73
- result = await anyio.run_process(
74
- argv, input=stdin.encode() if stdin is not None else None, check=True, env=os.environ | backend.env()
75
- )
76
- except TimeoutError as exc:
77
- raise subprocess.TimeoutExpired(argv, timeout) from exc
78
- return cast(response_model, backend.parse_response(result.stdout.decode(), response_model))
71
+ return cast(M, backend.parse_response(rr.stdout, response_model))
79
72
 
80
73
 
81
74
  async def run_structured[M: BaseModel](
@@ -88,7 +81,7 @@ async def run_structured[M: BaseModel](
88
81
 
89
82
  Raises:
90
83
  spawnllm.BackendUnavailable: If no backend is installed and authenticated.
91
- subprocess.SubprocessError: If the backend CLI exits non-zero or times out.
84
+ subprocess.TimeoutExpired: If the backend CLI outlives ``timeout``.
92
85
  pydantic.ValidationError: If the response does not match the schema.
93
86
  """
94
87
  return await run_structured_on(default_backend(), prompt, response_model=response_model, tier=tier, timeout=timeout)
@@ -421,7 +421,7 @@ class FlipReport:
421
421
 
422
422
  async def run_verdicts[V](
423
423
  rows: Sequence[Mapping[str, object]],
424
- prompt_for: Callable[[Mapping[str, object]], str | Awaitable[str]],
424
+ prompt_for: Callable[[Mapping[str, object]], Awaitable[str]],
425
425
  judge: Callable[[str], Awaitable[V]],
426
426
  persist: Callable[[Mapping[str, object], V], Awaitable[None]],
427
427
  *,
@@ -436,8 +436,8 @@ async def run_verdicts[V](
436
436
 
437
437
  Args:
438
438
  rows: The rows to judge.
439
- prompt_for: Builds one row's prompt; sync, or async for prompts that
440
- hydrate their context window first.
439
+ prompt_for: Builds one row's prompt; async, so prompts may hydrate their
440
+ context window first.
441
441
  judge: Turns one prompt into a verdict payload, e.g. ``structured_judge(...)``.
442
442
  persist: Persists one row's verdict, e.g. ``store.record_verdict`` applied.
443
443
  concurrency: The maximum number of concurrent judge calls.
@@ -451,11 +451,7 @@ async def run_verdicts[V](
451
451
  async def worker(row: Mapping[str, object]) -> None:
452
452
  async with limiter:
453
453
  try:
454
- match prompt_for(row):
455
- case str() as prompt:
456
- verdict = await judge(prompt)
457
- case awaitable:
458
- verdict = await judge(await awaitable)
454
+ verdict = await judge(await prompt_for(row))
459
455
  except Exception:
460
456
  counts["failed"] += 1
461
457
  return
@@ -44,15 +44,25 @@ from cc_transcript.mining.filterspec import (
44
44
  keep_candidate,
45
45
  only_kinds,
46
46
  )
47
- from cc_transcript.mining.formats import ReviewComment, ReviewFormat, extract_all
47
+ from cc_transcript.mining.formats import (
48
+ ReviewComment,
49
+ ReviewFormat,
50
+ StructuredFormat,
51
+ extract_all,
52
+ extract_structured,
53
+ )
48
54
  from cc_transcript.mining.signals import (
49
55
  DEFAULT_DETECTORS,
50
56
  DENIAL_PREFIX,
51
57
  EDIT_TOOLS,
52
58
  REENTRY_LOOKBACK,
59
+ SUBAGENT_TOOLS,
53
60
  USER_SAID_MARKER,
54
61
  USER_SAID_TRAILER,
55
62
  MiningSignal,
63
+ Provenance,
64
+ ScanText,
65
+ classify_provenance,
56
66
  correction_text,
57
67
  denial_results,
58
68
  denied_tool_payload,
@@ -69,6 +79,7 @@ from cc_transcript.mining.signals import (
69
79
  marker_in,
70
80
  nearest_assistant_index,
71
81
  next_user_message,
82
+ review_scan_texts,
72
83
  )
73
84
  from cc_transcript.mining.sourcekind import (
74
85
  INTERRUPT_REJECTION,