cc-transcript 14.23.0__tar.gz → 14.25.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-14.23.0 → cc_transcript-14.25.0}/PKG-INFO +1 -1
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/__init__.py +2 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/__init__.pyi +4 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/_native.pyi +3 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/context.py +60 -16
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/render.py +5 -1
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/pyproject.toml +1 -1
- cc_transcript-14.25.0/rust/crates/cli/src/commands/corpus.rs +124 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/commands/grep.rs +96 -21
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/commands/mod.rs +1 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/lib.rs +80 -7
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/target.rs +16 -6
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/render.rs +21 -5
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/context.rs +53 -11
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/python.rs +4 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/Cargo.lock +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/Cargo.toml +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/LICENSE +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/README.md +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/activity.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/activity_probe.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/builders.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/codex.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/command.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/corrections.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/cost.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/decision_store.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/decisions.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/discovery.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/disktruth.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/evidence.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/extract/__init__.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/extract/correct.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/facts.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/filterspec.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/heartbeats.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/ids.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/judge/__init__.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/judge/llm.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/judge/similar.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/judge/verdicts.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/ledger.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/literals.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/mining/__init__.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/mining/candidates.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/mining/confidence.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/mining/engine.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/mining/formats.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/mining/sampling.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/mining/signals.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/mining/sourcekind.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/mining/spec.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/mining/store.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/models.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/nlp.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/notifications.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/parser.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/py.typed +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/query.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/sentiment/__init__.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/sentiment/buckets.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/sentiment/data/afinn-en-165.tsv +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/sentiment/data/domain_overrides.tsv +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/sentiment/data/en-ewt.udpipe +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/sentiment/engine.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/sentiment/lexicon.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/sentiment/scorespec.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/synthetic.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/tools.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/watch.py +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/Cargo.toml +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/commands/blame.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/commands/corrections.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/commands/digest.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/commands/facts.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/commands/list.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/commands/scratchpad.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/commands/show.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/commands/slice.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/commands/stats.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/commands/watch.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/main.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/output.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/timearg.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/Cargo.toml +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/benches/parse.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/benches/render.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/benches/toolcall.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/activity.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/actor.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/blame.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/buckets.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/codex/discovery.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/codex/lower.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/codex/mod.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/codex/parse.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/codex/probe.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/codex/protocol.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/codex/types.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/command.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/context.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/corrections.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/cost.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/discovery.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/facts.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/feedback.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/filter.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/gateway.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/ids.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/judge.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/lib.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/literals/command.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/literals/corrections.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/literals/feedback.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/literals/mining.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/literals/mod.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/literals/protocol.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/notifications.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/parse.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/protocol.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/pystr.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/query.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/rng.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/schema.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/sqlite.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/toolcall.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/types.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/value.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/watch.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/Cargo.toml +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/build.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/data/command_prefix_pins.tsv +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/actor_bridge.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/bin/stub_gen.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/codex.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/command.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/corrections.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/discovery.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/feedback.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/judge.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/lexicon.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/lib.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/mining.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/nlp.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/render.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/score.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/sqlite.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/toolcall.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/attachment.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/blocks.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/command.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/convert.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/dunder.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/events.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/meta.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/mod.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/print.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/store.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/system.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/toolcall.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/toolresult.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/transcript.rs +0 -0
- {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/watch.rs +0 -0
|
@@ -234,6 +234,8 @@ EXPORTS: dict[str, str] = {
|
|
|
234
234
|
"SchemaError",
|
|
235
235
|
"TurnRef",
|
|
236
236
|
"capture_window",
|
|
237
|
+
"capture_windows",
|
|
238
|
+
"hydrate_windows",
|
|
237
239
|
),
|
|
238
240
|
"cc_transcript.render": ("Budget", "render_session", "render_tool_call", "render_turn"),
|
|
239
241
|
"cc_transcript.notifications": ("Notifications",),
|
|
@@ -54,6 +54,8 @@ from cc_transcript.context import (
|
|
|
54
54
|
SchemaError as SchemaError,
|
|
55
55
|
TurnRef as TurnRef,
|
|
56
56
|
capture_window as capture_window,
|
|
57
|
+
capture_windows as capture_windows,
|
|
58
|
+
hydrate_windows as hydrate_windows,
|
|
57
59
|
)
|
|
58
60
|
|
|
59
61
|
from cc_transcript.corrections import (
|
|
@@ -467,6 +469,7 @@ __all__ = [
|
|
|
467
469
|
"build_spec",
|
|
468
470
|
"canonical_json",
|
|
469
471
|
"capture_window",
|
|
472
|
+
"capture_windows",
|
|
470
473
|
"command_prefix_counts",
|
|
471
474
|
"command_prefixes",
|
|
472
475
|
"cost_of",
|
|
@@ -491,6 +494,7 @@ __all__ = [
|
|
|
491
494
|
"harvest_pairs",
|
|
492
495
|
"hunk_overlap",
|
|
493
496
|
"hunks_of",
|
|
497
|
+
"hydrate_windows",
|
|
494
498
|
"keep",
|
|
495
499
|
"keep_only",
|
|
496
500
|
"labels_for",
|
|
@@ -120,6 +120,7 @@ __all__ = [
|
|
|
120
120
|
"command_parse",
|
|
121
121
|
"command_prefixes",
|
|
122
122
|
"context_capture_window",
|
|
123
|
+
"context_capture_windows",
|
|
123
124
|
"context_render_preview",
|
|
124
125
|
"context_roundtrip",
|
|
125
126
|
"cost_of_json",
|
|
@@ -2330,6 +2331,8 @@ def command_prefixes(commands: typing.Sequence[builtins.str]) -> builtins.list[b
|
|
|
2330
2331
|
|
|
2331
2332
|
def context_capture_window(raw: bytes, session_id: builtins.str, anchor_uuid: builtins.str, anchor_tool_use_id: typing.Optional[builtins.str], before: builtins.int, after: builtins.int, preview_chars: builtins.int) -> builtins.str: ...
|
|
2332
2333
|
|
|
2334
|
+
def context_capture_windows(raw: bytes, anchors: typing.Sequence[tuple[builtins.str, builtins.str, typing.Optional[builtins.str]]], before: builtins.int, after: builtins.int, preview_chars: builtins.int) -> builtins.list[builtins.str]: ...
|
|
2335
|
+
|
|
2333
2336
|
def context_render_preview(data: builtins.str, turn_chars: builtins.int) -> builtins.str: ...
|
|
2334
2337
|
|
|
2335
2338
|
def context_roundtrip(data: builtins.str) -> builtins.str: ...
|
|
@@ -21,7 +21,7 @@ from cc_transcript.models import Question
|
|
|
21
21
|
from cc_transcript.render import Budget, clip, render_turn
|
|
22
22
|
|
|
23
23
|
if TYPE_CHECKING:
|
|
24
|
-
from collections.abc import Mapping
|
|
24
|
+
from collections.abc import Mapping, Sequence
|
|
25
25
|
from typing import Any
|
|
26
26
|
|
|
27
27
|
from cc_transcript.activity import Turn
|
|
@@ -158,17 +158,7 @@ class ContextWindow:
|
|
|
158
158
|
away — callers fall back to :meth:`render_preview`, never
|
|
159
159
|
hydrate-or-fail.
|
|
160
160
|
"""
|
|
161
|
-
|
|
162
|
-
activity = SessionActivity.from_session(self.anchor.session_id)
|
|
163
|
-
except TranscriptExpiredError:
|
|
164
|
-
return None
|
|
165
|
-
turns: list[Turn] = []
|
|
166
|
-
for turn_ref in window_refs(self):
|
|
167
|
-
resolved = [turn for ref in turn_ref.refs if (turn := activity.turn_of(ref)) is not None]
|
|
168
|
-
if len(resolved) != len(turn_ref.refs) or not resolved:
|
|
169
|
-
return None
|
|
170
|
-
turns.append(resolved[0])
|
|
171
|
-
return HydratedWindow(window=self, turns=tuple(turns))
|
|
161
|
+
return hydrate_windows((self,))[0]
|
|
172
162
|
|
|
173
163
|
def to_json(self) -> str:
|
|
174
164
|
"""Serialize to the ``cc-transcript.context/2`` wire schema, byte-stably.
|
|
@@ -259,11 +249,65 @@ def capture_window(
|
|
|
259
249
|
Raises:
|
|
260
250
|
ValueError: When ``anchor`` does not resolve within ``raw``.
|
|
261
251
|
"""
|
|
262
|
-
return
|
|
263
|
-
|
|
264
|
-
|
|
252
|
+
return capture_windows(raw, (anchor,), before=before, after=after, preview_chars=preview_chars)[0]
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def capture_windows(
|
|
256
|
+
raw: bytes,
|
|
257
|
+
anchors: Sequence[EventRef],
|
|
258
|
+
*,
|
|
259
|
+
before: int = 6,
|
|
260
|
+
after: int = 2,
|
|
261
|
+
preview_chars: int = 200,
|
|
262
|
+
) -> list[ContextWindow]:
|
|
263
|
+
"""Capture ordered windows with one parse and one activity lift per session.
|
|
264
|
+
|
|
265
|
+
Uses the same budgets and anchor resolution as :func:`capture_window`.
|
|
266
|
+
An empty batch returns without parsing; any unresolved anchor raises
|
|
267
|
+
ValueError for the entire batch.
|
|
268
|
+
"""
|
|
269
|
+
return [
|
|
270
|
+
ContextWindow.from_json(data)
|
|
271
|
+
for data in _native.context_capture_windows(
|
|
272
|
+
raw,
|
|
273
|
+
[(anchor.session_id, anchor.event_uuid, anchor.tool_use_id) for anchor in anchors],
|
|
274
|
+
before,
|
|
275
|
+
after,
|
|
276
|
+
preview_chars,
|
|
265
277
|
)
|
|
266
|
-
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def hydrate_windows(windows: Sequence[ContextWindow]) -> list[HydratedWindow | None]:
|
|
282
|
+
"""Resolve an ordered batch, loading each session at most once.
|
|
283
|
+
|
|
284
|
+
Missing transcripts and compacted references yield None at that window's
|
|
285
|
+
position. The batch owns its reads; no parsed-session cache persists
|
|
286
|
+
between calls, so a later call observes transcript changes.
|
|
287
|
+
"""
|
|
288
|
+
sessions: dict[SessionId, list[tuple[int, ContextWindow]]] = {}
|
|
289
|
+
for index, window in enumerate(windows):
|
|
290
|
+
sessions.setdefault(window.anchor.session_id, []).append((index, window))
|
|
291
|
+
hydrated: list[HydratedWindow | None] = [None] * len(windows)
|
|
292
|
+
for session_id, batch in sessions.items():
|
|
293
|
+
try:
|
|
294
|
+
activity = SessionActivity.from_session(session_id)
|
|
295
|
+
except TranscriptExpiredError:
|
|
296
|
+
continue
|
|
297
|
+
for index, window in batch:
|
|
298
|
+
hydrated[index] = hydrate_from_activity(window, activity)
|
|
299
|
+
del activity
|
|
300
|
+
return hydrated
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def hydrate_from_activity(window: ContextWindow, activity: SessionActivity) -> HydratedWindow | None:
|
|
304
|
+
turns: list[Turn] = []
|
|
305
|
+
for turn_ref in window_refs(window):
|
|
306
|
+
resolved = [turn for ref in turn_ref.refs if (turn := activity.turn_of(ref)) is not None]
|
|
307
|
+
if len(resolved) != len(turn_ref.refs) or not resolved:
|
|
308
|
+
return None
|
|
309
|
+
turns.append(resolved[0])
|
|
310
|
+
return HydratedWindow(window=window, turns=tuple(turns))
|
|
267
311
|
|
|
268
312
|
|
|
269
313
|
def window_refs(window: ContextWindow) -> tuple[TurnRef, ...]:
|
|
@@ -62,7 +62,11 @@ def prefixed(prefix: str, text: str) -> tuple[str, ...]:
|
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
def render_turn(turn: Turn, *, budget: Budget, tool_results: bool = False) -> str:
|
|
65
|
-
"""Render one turn: the prompt, the
|
|
65
|
+
"""Render one turn: the prompt, the messages queued mid-turn, assistant prose, and every tool call, in order.
|
|
66
|
+
|
|
67
|
+
A message queued while the agent worked renders under who sent it: ``user:`` for
|
|
68
|
+
the user's own, ``notification:`` for a background task's, ``peer:`` for another
|
|
69
|
+
agent's, ``channel:`` for an MCP channel's.
|
|
66
70
|
|
|
67
71
|
Prose chunks clip to ``budget.turn_chars``; each tool call renders via
|
|
68
72
|
:func:`render_tool_call` under ``budget.tool_chars``. An AskUserQuestion answer
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cc-transcript"
|
|
3
|
-
version = "14.
|
|
3
|
+
version = "14.25.0" # informational — releases stamp the version from the git tag
|
|
4
4
|
description = "Grep every Claude Code session you've ever run."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "PolyForm-Noncommercial-1.0.0"
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
//! `corpus`: flatten a whole sweep into one deduped file of character windows, so the
|
|
2
|
+
//! questions that follow grep the extract instead of re-reading every transcript.
|
|
3
|
+
|
|
4
|
+
use std::collections::HashSet;
|
|
5
|
+
use std::fs::File;
|
|
6
|
+
use std::io::{BufWriter, Write};
|
|
7
|
+
use std::path::PathBuf;
|
|
8
|
+
|
|
9
|
+
use cc_transcript_core::render::haystack;
|
|
10
|
+
use regex::Regex;
|
|
11
|
+
|
|
12
|
+
use crate::commands::grep::{compile_pattern, where_flags};
|
|
13
|
+
use crate::output::{click_error, eline, CliExit};
|
|
14
|
+
use crate::target::{map_transcripts, py_path, require_files, resolve_targets};
|
|
15
|
+
use crate::DiscoveryOpts;
|
|
16
|
+
|
|
17
|
+
const USAGE: &str = "cc-transcript corpus [OPTIONS] --out <OUT> PATTERN [PATHS]...";
|
|
18
|
+
const HELP_PATH: &str = "cc-transcript corpus";
|
|
19
|
+
|
|
20
|
+
pub struct CorpusArgs {
|
|
21
|
+
pub pattern: String,
|
|
22
|
+
pub paths: Vec<PathBuf>,
|
|
23
|
+
pub discovery: DiscoveryOpts,
|
|
24
|
+
pub wheres: Vec<String>,
|
|
25
|
+
pub ignore_case: bool,
|
|
26
|
+
pub window: usize,
|
|
27
|
+
pub out: PathBuf,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Byte offset of the `window`th character before `at`, or the start of `text`.
|
|
31
|
+
fn window_start(text: &str, at: usize, window: usize) -> usize {
|
|
32
|
+
match window.checked_sub(1) {
|
|
33
|
+
None => at,
|
|
34
|
+
Some(back) => text[..at]
|
|
35
|
+
.char_indices()
|
|
36
|
+
.rev()
|
|
37
|
+
.nth(back)
|
|
38
|
+
.map_or(0, |(index, _)| index),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Byte offset just past the `window`th character after `at`, or the end of `text`.
|
|
43
|
+
fn window_end(text: &str, at: usize, window: usize) -> usize {
|
|
44
|
+
text[at..]
|
|
45
|
+
.char_indices()
|
|
46
|
+
.nth(window)
|
|
47
|
+
.map_or(text.len(), |(index, _)| at + index)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// One window per line: line breaks inside a window become their two-character escapes,
|
|
51
|
+
/// the shape `grep --corpus` then reads back.
|
|
52
|
+
fn flatten(window: &str) -> String {
|
|
53
|
+
window.replace('\n', "\\n").replace('\r', "\\r")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn windows_in(text: &str, regex: &Regex, window: usize) -> Vec<String> {
|
|
57
|
+
regex
|
|
58
|
+
.find_iter(text)
|
|
59
|
+
.map(|hit| {
|
|
60
|
+
flatten(
|
|
61
|
+
&text[window_start(text, hit.start(), window)..window_end(text, hit.end(), window)],
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
.collect()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
pub fn run(args: CorpusArgs) -> Result<(), CliExit> {
|
|
68
|
+
let regex = compile_pattern(&args.pattern, args.ignore_case, USAGE, HELP_PATH)?;
|
|
69
|
+
let (w_text, w_thinking, w_tools) = where_flags(&args.wheres);
|
|
70
|
+
let paths: Vec<PathBuf> = args
|
|
71
|
+
.paths
|
|
72
|
+
.iter()
|
|
73
|
+
.map(|p| py_path(&p.to_string_lossy()))
|
|
74
|
+
.collect();
|
|
75
|
+
require_files(&paths, "'[PATHS]...'", USAGE, HELP_PATH)?;
|
|
76
|
+
let targets = resolve_targets(
|
|
77
|
+
&paths,
|
|
78
|
+
&args.discovery.validated_root(USAGE, HELP_PATH)?,
|
|
79
|
+
args.discovery.project.as_deref(),
|
|
80
|
+
args.discovery.contains.as_deref(),
|
|
81
|
+
args.discovery.sweep_limit(),
|
|
82
|
+
None,
|
|
83
|
+
)?;
|
|
84
|
+
let per_file = map_transcripts(&targets.paths, |parsed| {
|
|
85
|
+
parsed
|
|
86
|
+
.entries
|
|
87
|
+
.iter()
|
|
88
|
+
.flat_map(|event| {
|
|
89
|
+
windows_in(
|
|
90
|
+
&haystack(event, w_text, w_thinking, w_tools),
|
|
91
|
+
®ex,
|
|
92
|
+
args.window,
|
|
93
|
+
)
|
|
94
|
+
})
|
|
95
|
+
.collect::<Vec<String>>()
|
|
96
|
+
});
|
|
97
|
+
let file = File::create(&args.out)
|
|
98
|
+
.map_err(|e| click_error(&format!("{}: {e}", args.out.display())))?;
|
|
99
|
+
let mut out = BufWriter::new(file);
|
|
100
|
+
let mut seen: HashSet<String> = HashSet::new();
|
|
101
|
+
let mut duplicates = 0usize;
|
|
102
|
+
let mut bytes = 0usize;
|
|
103
|
+
for line in per_file.into_iter().flatten() {
|
|
104
|
+
if !seen.insert(line.clone()) {
|
|
105
|
+
duplicates += 1;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
out.write_all(line.as_bytes())
|
|
109
|
+
.and_then(|()| out.write_all(b"\n"))
|
|
110
|
+
.map_err(|e| click_error(&format!("{}: {e}", args.out.display())))?;
|
|
111
|
+
bytes += line.len() + 1;
|
|
112
|
+
}
|
|
113
|
+
out.flush()
|
|
114
|
+
.map_err(|e| click_error(&format!("{}: {e}", args.out.display())))?;
|
|
115
|
+
eline(&format!(
|
|
116
|
+
"{} files scanned, {} windows kept, {duplicates} duplicates dropped, {bytes} bytes written",
|
|
117
|
+
targets.paths.len(),
|
|
118
|
+
seen.len()
|
|
119
|
+
));
|
|
120
|
+
if seen.is_empty() {
|
|
121
|
+
return Err(CliExit(1));
|
|
122
|
+
}
|
|
123
|
+
Ok(())
|
|
124
|
+
}
|
|
@@ -1,35 +1,39 @@
|
|
|
1
1
|
//! cli.py `grep`.
|
|
2
2
|
|
|
3
3
|
use std::collections::HashMap;
|
|
4
|
-
use std::
|
|
4
|
+
use std::fs::File;
|
|
5
|
+
use std::io::{BufRead, BufReader};
|
|
6
|
+
use std::path::{Path, PathBuf};
|
|
5
7
|
|
|
6
8
|
use cc_transcript_core::facts::{tool_facts, ToolFact};
|
|
7
9
|
use cc_transcript_core::filter::event_kind;
|
|
8
10
|
use cc_transcript_core::render::{
|
|
9
|
-
compact_line, event_json, haystack, tool_haystack, transcript_header, Json,
|
|
11
|
+
compact_line, display_path, event_json, haystack, tool_haystack, transcript_header, Json,
|
|
10
12
|
};
|
|
11
13
|
use cc_transcript_core::toolcall::tool_name_matches;
|
|
12
14
|
use cc_transcript_core::types::{ContentBlock, Entry};
|
|
13
15
|
use regex::{Regex, RegexBuilder};
|
|
14
16
|
|
|
15
|
-
use crate::output::{usage_error, CliExit, Out};
|
|
17
|
+
use crate::output::{click_error, eline, usage_error, CliExit, Out};
|
|
16
18
|
use crate::target::{parse_transcripts, py_path, resolve_targets, scope_note, tool_names, Parsed};
|
|
17
19
|
use crate::DiscoveryOpts;
|
|
18
20
|
|
|
19
21
|
const USAGE: &str = "cc-transcript grep [OPTIONS] PATTERN [PATHS]...";
|
|
20
22
|
const HELP_PATH: &str = "cc-transcript grep";
|
|
23
|
+
const DEFAULT_MAX_MATCHES: usize = 20;
|
|
21
24
|
|
|
22
25
|
pub struct GrepArgs {
|
|
23
26
|
pub pattern: String,
|
|
24
27
|
pub paths: Vec<PathBuf>,
|
|
25
28
|
pub discovery: DiscoveryOpts,
|
|
29
|
+
pub corpus: Option<PathBuf>,
|
|
26
30
|
pub kinds: Vec<String>,
|
|
27
31
|
pub tool: Option<String>,
|
|
28
32
|
pub errors: bool,
|
|
29
33
|
pub ignore_case: bool,
|
|
30
34
|
pub wheres: Vec<String>,
|
|
31
35
|
pub context: usize,
|
|
32
|
-
pub max_matches: usize
|
|
36
|
+
pub max_matches: Option<usize>,
|
|
33
37
|
pub width: usize,
|
|
34
38
|
pub uuids: bool,
|
|
35
39
|
pub with_result: bool,
|
|
@@ -141,11 +145,78 @@ fn merge_windows(hits: &[usize], context: usize, size: usize) -> Vec<(usize, usi
|
|
|
141
145
|
merged
|
|
142
146
|
}
|
|
143
147
|
|
|
144
|
-
fn compile_pattern(
|
|
148
|
+
pub fn compile_pattern(
|
|
149
|
+
pattern: &str,
|
|
150
|
+
ignore_case: bool,
|
|
151
|
+
usage: &str,
|
|
152
|
+
help_path: &str,
|
|
153
|
+
) -> Result<Regex, CliExit> {
|
|
145
154
|
RegexBuilder::new(pattern)
|
|
146
155
|
.case_insensitive(ignore_case)
|
|
147
156
|
.build()
|
|
148
|
-
.map_err(|e| usage_error(
|
|
157
|
+
.map_err(|e| usage_error(usage, help_path, &format!("invalid pattern: {e}")))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// An empty `--where` searches everywhere (cli.py's default).
|
|
161
|
+
pub fn where_flags(wheres: &[String]) -> (bool, bool, bool) {
|
|
162
|
+
if wheres.is_empty() {
|
|
163
|
+
return (true, true, true);
|
|
164
|
+
}
|
|
165
|
+
(
|
|
166
|
+
wheres.iter().any(|w| w == "text"),
|
|
167
|
+
wheres.iter().any(|w| w == "thinking"),
|
|
168
|
+
wheres.iter().any(|w| w == "tools"),
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/// `--max-matches 0` lifts the cap and so does leaving it off in corpus mode, where each
|
|
173
|
+
/// match is one bounded window; anything else is a budget that must announce itself when
|
|
174
|
+
/// it bites, so a whole-corpus sweep never looks complete at 20 rows.
|
|
175
|
+
fn match_cap(max_matches: Option<usize>, default: Option<usize>) -> Option<usize> {
|
|
176
|
+
max_matches.map_or(default, |cap| (cap > 0).then_some(cap))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fn warn_truncated(cap: usize) {
|
|
180
|
+
eline(&format!(
|
|
181
|
+
"warning: stopped at --max-matches {cap}; more matches may exist — raise it, or pass --max-matches 0 for no cap"
|
|
182
|
+
));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/// `grep --corpus`: one `corpus` window per line, matched verbatim. There is no event
|
|
186
|
+
/// structure left to filter on, so only the pattern, `-i`, and `--max-matches` apply.
|
|
187
|
+
fn run_over_corpus(args: &GrepArgs, corpus: &Path) -> Result<(), CliExit> {
|
|
188
|
+
let regex = compile_pattern(&args.pattern, args.ignore_case, USAGE, HELP_PATH)?;
|
|
189
|
+
let file =
|
|
190
|
+
File::open(corpus).map_err(|e| click_error(&format!("{}: {e}", corpus.display())))?;
|
|
191
|
+
let cap = match_cap(args.max_matches, None);
|
|
192
|
+
let mut out_lines: Vec<String> = Vec::new();
|
|
193
|
+
let mut truncated = false;
|
|
194
|
+
for line in BufReader::new(file).lines() {
|
|
195
|
+
let line = line.map_err(|e| click_error(&format!("{}: {e}", corpus.display())))?;
|
|
196
|
+
if !regex.is_match(&line) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if cap.is_some_and(|cap| out_lines.len() == cap) {
|
|
200
|
+
truncated = true;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
out_lines.push(line);
|
|
204
|
+
}
|
|
205
|
+
let matched = out_lines.len();
|
|
206
|
+
out_lines.push(format!(
|
|
207
|
+
"{matched} matches in {}",
|
|
208
|
+
display_path(&corpus.to_string_lossy())
|
|
209
|
+
));
|
|
210
|
+
let mut out = Out::new();
|
|
211
|
+
out.lines(out_lines)?;
|
|
212
|
+
out.finish()?;
|
|
213
|
+
if let Some(cap) = cap.filter(|_| truncated) {
|
|
214
|
+
warn_truncated(cap);
|
|
215
|
+
}
|
|
216
|
+
if matched == 0 {
|
|
217
|
+
return Err(CliExit(1));
|
|
218
|
+
}
|
|
219
|
+
Ok(())
|
|
149
220
|
}
|
|
150
221
|
|
|
151
222
|
fn fact_index(parsed: &Parsed) -> HashMap<String, ToolFact> {
|
|
@@ -159,16 +230,11 @@ fn fact_index(parsed: &Parsed) -> HashMap<String, ToolFact> {
|
|
|
159
230
|
}
|
|
160
231
|
|
|
161
232
|
pub fn run(args: GrepArgs) -> Result<(), CliExit> {
|
|
162
|
-
let
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
args.wheres.iter().any(|w| w == "text"),
|
|
168
|
-
args.wheres.iter().any(|w| w == "thinking"),
|
|
169
|
-
args.wheres.iter().any(|w| w == "tools"),
|
|
170
|
-
)
|
|
171
|
-
};
|
|
233
|
+
if let Some(corpus) = &args.corpus {
|
|
234
|
+
return run_over_corpus(&args, corpus);
|
|
235
|
+
}
|
|
236
|
+
let regex = compile_pattern(&args.pattern, args.ignore_case, USAGE, HELP_PATH)?;
|
|
237
|
+
let (w_text, w_thinking, w_tools) = where_flags(&args.wheres);
|
|
172
238
|
let paths: Vec<PathBuf> = args
|
|
173
239
|
.paths
|
|
174
240
|
.iter()
|
|
@@ -186,9 +252,11 @@ pub fn run(args: GrepArgs) -> Result<(), CliExit> {
|
|
|
186
252
|
let mut out_lines: Vec<String> = Vec::new();
|
|
187
253
|
let mut files_matched = 0usize;
|
|
188
254
|
let mut matched = 0usize;
|
|
189
|
-
let
|
|
255
|
+
let cap = match_cap(args.max_matches, Some(DEFAULT_MAX_MATCHES));
|
|
256
|
+
let mut truncated = false;
|
|
190
257
|
for parsed in parse_transcripts(&targets.paths) {
|
|
191
|
-
if
|
|
258
|
+
if cap.is_some_and(|cap| matched == cap) {
|
|
259
|
+
truncated = true;
|
|
192
260
|
break;
|
|
193
261
|
}
|
|
194
262
|
let names = tool_names(&parsed.entries);
|
|
@@ -197,7 +265,7 @@ pub fn run(args: GrepArgs) -> Result<(), CliExit> {
|
|
|
197
265
|
} else {
|
|
198
266
|
HashMap::new()
|
|
199
267
|
};
|
|
200
|
-
let hits: Vec<usize> = parsed
|
|
268
|
+
let mut hits: Vec<usize> = parsed
|
|
201
269
|
.entries
|
|
202
270
|
.iter()
|
|
203
271
|
.enumerate()
|
|
@@ -219,14 +287,18 @@ pub fn run(args: GrepArgs) -> Result<(), CliExit> {
|
|
|
219
287
|
}
|
|
220
288
|
})
|
|
221
289
|
.map(|(index, _)| index)
|
|
222
|
-
.take(budget)
|
|
223
290
|
.collect();
|
|
291
|
+
if let Some(cap) = cap {
|
|
292
|
+
if matched + hits.len() > cap {
|
|
293
|
+
truncated = true;
|
|
294
|
+
hits.truncate(cap - matched);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
224
297
|
if hits.is_empty() {
|
|
225
298
|
continue;
|
|
226
299
|
}
|
|
227
300
|
files_matched += 1;
|
|
228
301
|
matched += hits.len();
|
|
229
|
-
budget -= hits.len();
|
|
230
302
|
let windows = merge_windows(&hits, args.context, parsed.entries.len());
|
|
231
303
|
if args.json {
|
|
232
304
|
let hit_set: std::collections::HashSet<usize> = hits.iter().copied().collect();
|
|
@@ -284,6 +356,9 @@ pub fn run(args: GrepArgs) -> Result<(), CliExit> {
|
|
|
284
356
|
let mut out = Out::new();
|
|
285
357
|
out.lines(out_lines)?;
|
|
286
358
|
out.finish()?;
|
|
359
|
+
if let Some(cap) = cap.filter(|_| truncated) {
|
|
360
|
+
warn_truncated(cap);
|
|
361
|
+
}
|
|
287
362
|
if matched == 0 {
|
|
288
363
|
return Err(CliExit(1));
|
|
289
364
|
}
|
|
@@ -44,6 +44,27 @@ fn pkg_version() -> String {
|
|
|
44
44
|
const KIND_CHOICES: [&str; 6] = ["user", "assistant", "system", "mode", "other", "attachment"];
|
|
45
45
|
const WHERE_CHOICES: [&str; 3] = ["text", "thinking", "tools"];
|
|
46
46
|
const LIST_PROVIDER_CHOICES: [&str; 3] = ["claude", "codex", "all"];
|
|
47
|
+
const DEFAULT_LIMIT: usize = 50;
|
|
48
|
+
|
|
49
|
+
/// `grep --corpus` reads a flattened extract, so every flag that selects transcripts or
|
|
50
|
+
/// reaches into an event's structure is rejected rather than silently ignored.
|
|
51
|
+
const CORPUS_INCOMPATIBLE: [&str; 15] = [
|
|
52
|
+
"paths",
|
|
53
|
+
"root",
|
|
54
|
+
"project",
|
|
55
|
+
"contains",
|
|
56
|
+
"limit",
|
|
57
|
+
"all",
|
|
58
|
+
"kinds",
|
|
59
|
+
"tool",
|
|
60
|
+
"errors",
|
|
61
|
+
"wheres",
|
|
62
|
+
"context",
|
|
63
|
+
"width",
|
|
64
|
+
"uuids",
|
|
65
|
+
"with_result",
|
|
66
|
+
"json",
|
|
67
|
+
];
|
|
47
68
|
|
|
48
69
|
#[derive(Parser)]
|
|
49
70
|
#[command(
|
|
@@ -68,9 +89,9 @@ pub struct DiscoveryOpts {
|
|
|
68
89
|
/// Substring filter over transcript file names.
|
|
69
90
|
#[arg(long)]
|
|
70
91
|
pub contains: Option<String>,
|
|
71
|
-
/// Keep only the newest N transcripts.
|
|
72
|
-
#[arg(long
|
|
73
|
-
pub limit: usize
|
|
92
|
+
/// Keep only the newest N transcripts [default: 50, uncapped for corpus].
|
|
93
|
+
#[arg(long)]
|
|
94
|
+
pub limit: Option<usize>,
|
|
74
95
|
/// Ignore --limit.
|
|
75
96
|
#[arg(long)]
|
|
76
97
|
pub all: bool,
|
|
@@ -93,7 +114,17 @@ impl DiscoveryOpts {
|
|
|
93
114
|
if self.all {
|
|
94
115
|
None
|
|
95
116
|
} else {
|
|
96
|
-
Some(self.limit)
|
|
117
|
+
Some(self.limit.unwrap_or(DEFAULT_LIMIT))
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// A whole-corpus sweep's limit: unbounded unless `--limit` asks for a cap, so an
|
|
122
|
+
/// extract never silently covers only the newest 50 transcripts.
|
|
123
|
+
pub fn sweep_limit(&self) -> Option<usize> {
|
|
124
|
+
if self.all {
|
|
125
|
+
None
|
|
126
|
+
} else {
|
|
127
|
+
self.limit
|
|
97
128
|
}
|
|
98
129
|
}
|
|
99
130
|
}
|
|
@@ -166,6 +197,10 @@ pub enum Cmd {
|
|
|
166
197
|
paths: Vec<PathBuf>,
|
|
167
198
|
#[command(flatten)]
|
|
168
199
|
discovery: DiscoveryOpts,
|
|
200
|
+
/// Search a `corpus` extract instead of transcripts: its windows carry no event
|
|
201
|
+
/// structure, so discovery, kind, tool, context, and rendering flags do not apply.
|
|
202
|
+
#[arg(long, conflicts_with_all = CORPUS_INCOMPATIBLE)]
|
|
203
|
+
corpus: Option<PathBuf>,
|
|
169
204
|
/// Keep only these event kinds.
|
|
170
205
|
#[arg(long = "kind", value_parser = KIND_CHOICES)]
|
|
171
206
|
kinds: Vec<String>,
|
|
@@ -184,9 +219,9 @@ pub enum Cmd {
|
|
|
184
219
|
/// Events of context around each hit.
|
|
185
220
|
#[arg(short = 'C', long, default_value_t = 0)]
|
|
186
221
|
context: usize,
|
|
187
|
-
/// Stop after this many matches.
|
|
188
|
-
#[arg(long = "max-matches"
|
|
189
|
-
max_matches: usize
|
|
222
|
+
/// Stop after this many matches; 0 lifts the cap [default: 20, uncapped with --corpus].
|
|
223
|
+
#[arg(long = "max-matches")]
|
|
224
|
+
max_matches: Option<usize>,
|
|
190
225
|
/// Truncation width per chunk (0 = no cut).
|
|
191
226
|
#[arg(long, default_value_t = 100)]
|
|
192
227
|
width: usize,
|
|
@@ -200,6 +235,25 @@ pub enum Cmd {
|
|
|
200
235
|
#[arg(long)]
|
|
201
236
|
json: bool,
|
|
202
237
|
},
|
|
238
|
+
/// Sweep the matched transcripts into a deduped file of character windows around every hit.
|
|
239
|
+
Corpus {
|
|
240
|
+
pattern: String,
|
|
241
|
+
paths: Vec<PathBuf>,
|
|
242
|
+
#[command(flatten)]
|
|
243
|
+
discovery: DiscoveryOpts,
|
|
244
|
+
/// Search only these areas [default: all].
|
|
245
|
+
#[arg(long = "where", value_parser = WHERE_CHOICES)]
|
|
246
|
+
wheres: Vec<String>,
|
|
247
|
+
/// Case-insensitive matching.
|
|
248
|
+
#[arg(short = 'i', long = "ignore-case")]
|
|
249
|
+
ignore_case: bool,
|
|
250
|
+
/// Characters kept either side of each match.
|
|
251
|
+
#[arg(long, default_value_t = 200)]
|
|
252
|
+
window: usize,
|
|
253
|
+
/// Write the extract here, one window per line.
|
|
254
|
+
#[arg(short = 'o', long, required = true)]
|
|
255
|
+
out: PathBuf,
|
|
256
|
+
},
|
|
203
257
|
/// Summarize event, model, and tool statistics.
|
|
204
258
|
Stats {
|
|
205
259
|
paths: Vec<PathBuf>,
|
|
@@ -493,6 +547,7 @@ fn dispatch(cmd: Cmd) -> Result<(), CliExit> {
|
|
|
493
547
|
pattern,
|
|
494
548
|
paths,
|
|
495
549
|
discovery,
|
|
550
|
+
corpus,
|
|
496
551
|
kinds,
|
|
497
552
|
tool,
|
|
498
553
|
errors,
|
|
@@ -508,6 +563,7 @@ fn dispatch(cmd: Cmd) -> Result<(), CliExit> {
|
|
|
508
563
|
pattern,
|
|
509
564
|
paths,
|
|
510
565
|
discovery,
|
|
566
|
+
corpus,
|
|
511
567
|
kinds,
|
|
512
568
|
tool,
|
|
513
569
|
errors,
|
|
@@ -520,6 +576,23 @@ fn dispatch(cmd: Cmd) -> Result<(), CliExit> {
|
|
|
520
576
|
with_result,
|
|
521
577
|
json,
|
|
522
578
|
}),
|
|
579
|
+
Cmd::Corpus {
|
|
580
|
+
pattern,
|
|
581
|
+
paths,
|
|
582
|
+
discovery,
|
|
583
|
+
wheres,
|
|
584
|
+
ignore_case,
|
|
585
|
+
window,
|
|
586
|
+
out,
|
|
587
|
+
} => commands::corpus::run(commands::corpus::CorpusArgs {
|
|
588
|
+
pattern,
|
|
589
|
+
paths,
|
|
590
|
+
discovery,
|
|
591
|
+
wheres,
|
|
592
|
+
ignore_case,
|
|
593
|
+
window,
|
|
594
|
+
out,
|
|
595
|
+
}),
|
|
523
596
|
Cmd::Stats {
|
|
524
597
|
paths,
|
|
525
598
|
discovery,
|