cr-proc 0.1.8__py3-none-any.whl → 0.1.10__py3-none-any.whl
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.
- code_recorder_processor/api/build.py +6 -0
- code_recorder_processor/api/document.py +337 -0
- code_recorder_processor/api/load.py +58 -0
- code_recorder_processor/api/output.py +70 -0
- code_recorder_processor/api/verify.py +171 -32
- code_recorder_processor/cli.py +514 -494
- code_recorder_processor/display.py +201 -0
- code_recorder_processor/playback.py +116 -0
- cr_proc-0.1.10.dist-info/METADATA +280 -0
- cr_proc-0.1.10.dist-info/RECORD +13 -0
- cr_proc-0.1.8.dist-info/METADATA +0 -142
- cr_proc-0.1.8.dist-info/RECORD +0 -9
- {cr_proc-0.1.8.dist-info → cr_proc-0.1.10.dist-info}/WHEEL +0 -0
- {cr_proc-0.1.8.dist-info → cr_proc-0.1.10.dist-info}/entry_points.txt +0 -0
code_recorder_processor/cli.py
CHANGED
|
@@ -1,491 +1,500 @@
|
|
|
1
|
+
"""Command-line interface for code recorder processor."""
|
|
1
2
|
import argparse
|
|
2
|
-
import
|
|
3
|
-
import os
|
|
3
|
+
import glob
|
|
4
4
|
import sys
|
|
5
|
-
import time
|
|
6
|
-
from datetime import datetime
|
|
7
5
|
from pathlib import Path
|
|
8
6
|
from typing import Any
|
|
9
7
|
|
|
10
8
|
from .api.build import reconstruct_file_from_events
|
|
9
|
+
from .api.document import (
|
|
10
|
+
filter_events_by_document,
|
|
11
|
+
get_recorded_documents,
|
|
12
|
+
resolve_document,
|
|
13
|
+
resolve_template_file,
|
|
14
|
+
find_matching_template,
|
|
15
|
+
)
|
|
11
16
|
from .api.load import load_jsonl
|
|
12
|
-
from .api.
|
|
17
|
+
from .api.output import write_batch_json_output
|
|
18
|
+
from .api.verify import (
|
|
19
|
+
check_time_limit,
|
|
20
|
+
combine_time_info,
|
|
21
|
+
detect_external_copypaste,
|
|
22
|
+
template_diff,
|
|
23
|
+
verify,
|
|
24
|
+
)
|
|
25
|
+
from .display import (
|
|
26
|
+
display_suspicious_events,
|
|
27
|
+
display_template_diff,
|
|
28
|
+
display_time_info,
|
|
29
|
+
print_batch_header,
|
|
30
|
+
print_batch_summary,
|
|
31
|
+
)
|
|
32
|
+
from .playback import playback_recording
|
|
13
33
|
|
|
14
34
|
|
|
15
|
-
def
|
|
16
|
-
docs: list[str], template_path: Path, override: str | None
|
|
17
|
-
) -> str | None:
|
|
35
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
18
36
|
"""
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Parameters
|
|
22
|
-
----------
|
|
23
|
-
docs : list[str]
|
|
24
|
-
List of document paths found in the recording
|
|
25
|
-
template_path : Path
|
|
26
|
-
Path to the template file
|
|
27
|
-
override : str | None
|
|
28
|
-
Explicit document name or path override
|
|
37
|
+
Create and configure the argument parser.
|
|
29
38
|
|
|
30
39
|
Returns
|
|
31
40
|
-------
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
Raises
|
|
36
|
-
------
|
|
37
|
-
ValueError
|
|
38
|
-
If document resolution is ambiguous or the override doesn't match
|
|
41
|
+
argparse.ArgumentParser
|
|
42
|
+
Configured argument parser
|
|
39
43
|
"""
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if override:
|
|
44
|
-
matches = [
|
|
45
|
-
d for d in docs if d.endswith(override) or Path(d).name == override
|
|
46
|
-
]
|
|
47
|
-
if not matches:
|
|
48
|
-
raise ValueError(
|
|
49
|
-
f"No document in recording matches '{override}'. Available: {docs}"
|
|
50
|
-
)
|
|
51
|
-
if len(matches) > 1:
|
|
52
|
-
raise ValueError(
|
|
53
|
-
f"Ambiguous document override '{override}'. Matches: {matches}"
|
|
54
|
-
)
|
|
55
|
-
return matches[0]
|
|
56
|
-
|
|
57
|
-
template_ext = template_path.suffix
|
|
58
|
-
ext_matches = [d for d in docs if Path(d).suffix == template_ext]
|
|
59
|
-
if len(ext_matches) == 1:
|
|
60
|
-
return ext_matches[0]
|
|
61
|
-
if len(ext_matches) > 1:
|
|
62
|
-
raise ValueError(
|
|
63
|
-
f"Multiple documents share extension '{template_ext}': {ext_matches}. "
|
|
64
|
-
"Use --document to choose one."
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
if len(docs) == 1:
|
|
68
|
-
return docs[0]
|
|
69
|
-
|
|
70
|
-
raise ValueError(
|
|
71
|
-
"Could not determine document to process. Use --document to select one. "
|
|
72
|
-
f"Available documents: {docs}"
|
|
44
|
+
parser = argparse.ArgumentParser(
|
|
45
|
+
description="Process and verify code recorder JSONL files"
|
|
73
46
|
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"files",
|
|
49
|
+
type=str,
|
|
50
|
+
nargs="+",
|
|
51
|
+
help="Path(s) to JSONL file(s) and optionally a template file. "
|
|
52
|
+
"JSONL files: compressed JSONL file(s) (*.recording.jsonl.gz). "
|
|
53
|
+
"Supports glob patterns like 'recordings/*.jsonl.gz'. "
|
|
54
|
+
"Template file (optional last positional): template file path. "
|
|
55
|
+
"Omit to use --template-dir instead.",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--template-dir",
|
|
59
|
+
type=Path,
|
|
60
|
+
default=None,
|
|
61
|
+
help="Directory containing template files (overrides positional template file). "
|
|
62
|
+
"Will search for files matching the document name. "
|
|
63
|
+
"If no match found, reconstruction proceeds with warning.",
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"-t",
|
|
67
|
+
"--time-limit",
|
|
68
|
+
type=int,
|
|
69
|
+
default=None,
|
|
70
|
+
help="Maximum allowed time in minutes between first and last edit. "
|
|
71
|
+
"If exceeded, recording is flagged. Applied individually to each recording file.",
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"-d",
|
|
75
|
+
"--document",
|
|
76
|
+
type=str,
|
|
77
|
+
default=None,
|
|
78
|
+
help="Document path or filename to process from the recording. "
|
|
79
|
+
"Defaults to the document whose extension matches the template file.",
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"-o",
|
|
83
|
+
"--output-json",
|
|
84
|
+
type=Path,
|
|
85
|
+
default=None,
|
|
86
|
+
help="Path to output JSON file with verification results. "
|
|
87
|
+
"Uses consistent format for both single and batch modes, with batch_mode flag. "
|
|
88
|
+
"In batch mode, includes combined_time_info across all files.",
|
|
89
|
+
)
|
|
90
|
+
parser.add_argument(
|
|
91
|
+
"-f",
|
|
92
|
+
"--output-file",
|
|
93
|
+
type=Path,
|
|
94
|
+
default=None,
|
|
95
|
+
help="Write reconstructed code to specified file instead of stdout. "
|
|
96
|
+
"In batch mode, this should be a directory where files will be named after the input files.",
|
|
97
|
+
)
|
|
98
|
+
parser.add_argument(
|
|
99
|
+
"--output-dir",
|
|
100
|
+
type=Path,
|
|
101
|
+
default=None,
|
|
102
|
+
help="Directory to write reconstructed code files in batch mode (one file per recording). "
|
|
103
|
+
"Files are named based on input recording filenames.",
|
|
104
|
+
)
|
|
105
|
+
parser.add_argument(
|
|
106
|
+
"-s",
|
|
107
|
+
"--show-autocomplete-details",
|
|
108
|
+
action="store_true",
|
|
109
|
+
help="Show individual auto-complete events in addition to "
|
|
110
|
+
"aggregate statistics",
|
|
111
|
+
)
|
|
112
|
+
parser.add_argument(
|
|
113
|
+
"-p",
|
|
114
|
+
"--playback",
|
|
115
|
+
action="store_true",
|
|
116
|
+
help="Play back the recording in real-time, showing code evolution",
|
|
117
|
+
)
|
|
118
|
+
parser.add_argument(
|
|
119
|
+
"--playback-speed",
|
|
120
|
+
type=float,
|
|
121
|
+
default=1.0,
|
|
122
|
+
help="Playback speed multiplier (1.0 = real-time, 2.0 = 2x speed, 0.5 = half speed)",
|
|
123
|
+
)
|
|
124
|
+
return parser
|
|
74
125
|
|
|
75
126
|
|
|
76
|
-
def
|
|
77
|
-
"""
|
|
78
|
-
Extract unique document paths from recording events.
|
|
79
|
-
|
|
80
|
-
Parameters
|
|
81
|
-
----------
|
|
82
|
-
events : tuple[dict[str, Any], ...]
|
|
83
|
-
Recording events loaded from JSONL
|
|
84
|
-
|
|
85
|
-
Returns
|
|
86
|
-
-------
|
|
87
|
-
list[str]
|
|
88
|
-
Sorted list of unique document paths
|
|
89
|
-
"""
|
|
90
|
-
documents = {
|
|
91
|
-
e.get("document")
|
|
92
|
-
for e in events
|
|
93
|
-
if "document" in e and e.get("document") is not None
|
|
94
|
-
}
|
|
95
|
-
return sorted([d for d in documents if d is not None])
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def filter_events_by_document(
|
|
99
|
-
events: tuple[dict[str, Any], ...], document: str | None
|
|
100
|
-
) -> tuple[dict[str, Any], ...]:
|
|
127
|
+
def expand_file_patterns(patterns: list[str]) -> list[Path]:
|
|
101
128
|
"""
|
|
102
|
-
|
|
129
|
+
Expand glob patterns and validate files exist.
|
|
103
130
|
|
|
104
131
|
Parameters
|
|
105
132
|
----------
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
document : str | None
|
|
109
|
-
Document path to filter by, or None to return all events
|
|
133
|
+
patterns : list[str]
|
|
134
|
+
List of file paths or glob patterns
|
|
110
135
|
|
|
111
136
|
Returns
|
|
112
137
|
-------
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
"""
|
|
116
|
-
if document:
|
|
117
|
-
return tuple(e for e in events if e.get("document") == document)
|
|
118
|
-
return events
|
|
119
|
-
|
|
138
|
+
list[Path]
|
|
139
|
+
List of existing file paths
|
|
120
140
|
|
|
121
|
-
|
|
141
|
+
Raises
|
|
142
|
+
------
|
|
143
|
+
FileNotFoundError
|
|
144
|
+
If no files are found
|
|
122
145
|
"""
|
|
123
|
-
|
|
146
|
+
jsonl_files = []
|
|
147
|
+
for pattern in patterns:
|
|
148
|
+
expanded = glob.glob(pattern)
|
|
149
|
+
if expanded:
|
|
150
|
+
jsonl_files.extend([Path(f) for f in expanded])
|
|
151
|
+
else:
|
|
152
|
+
# If no glob match, treat as literal path
|
|
153
|
+
jsonl_files.append(Path(pattern))
|
|
124
154
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
time_info : dict[str, Any] | None
|
|
128
|
-
Time information from check_time_limit, or None if no time data
|
|
129
|
-
"""
|
|
130
|
-
if not time_info:
|
|
131
|
-
return
|
|
155
|
+
if not jsonl_files:
|
|
156
|
+
raise FileNotFoundError("No JSONL files found")
|
|
132
157
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
158
|
+
# Check if files exist
|
|
159
|
+
existing_files = [f for f in jsonl_files if f.exists()]
|
|
160
|
+
if not existing_files:
|
|
161
|
+
raise FileNotFoundError("None of the specified files exist")
|
|
137
162
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
)
|
|
144
|
-
time_span = (last_ts - first_ts).total_seconds() / 60
|
|
145
|
-
|
|
146
|
-
print(f"Time span (first to last edit): {time_span:.2f} minutes", file=sys.stderr)
|
|
163
|
+
# Warn about missing files
|
|
164
|
+
if len(existing_files) < len(jsonl_files):
|
|
165
|
+
missing = [f for f in jsonl_files if f not in existing_files]
|
|
166
|
+
for f in missing:
|
|
167
|
+
print(f"Warning: File not found: {f}", file=sys.stderr)
|
|
147
168
|
|
|
148
|
-
|
|
149
|
-
print("\nTime limit exceeded!", file=sys.stderr)
|
|
150
|
-
print(f" Limit: {time_info['time_limit_minutes']} minutes", file=sys.stderr)
|
|
151
|
-
print(f" First edit: {time_info['first_timestamp']}", file=sys.stderr)
|
|
152
|
-
print(f" Last edit: {time_info['last_timestamp']}", file=sys.stderr)
|
|
169
|
+
return existing_files
|
|
153
170
|
|
|
154
171
|
|
|
155
|
-
def
|
|
172
|
+
def process_single_file(
|
|
173
|
+
jsonl_path: Path,
|
|
174
|
+
template_data: str,
|
|
175
|
+
target_document: str | None,
|
|
176
|
+
time_limit: int | None,
|
|
177
|
+
) -> tuple[bool, str, list[dict[str, Any]], dict[str, Any] | None, str]:
|
|
156
178
|
"""
|
|
157
|
-
|
|
179
|
+
Process a single JSONL recording file.
|
|
158
180
|
|
|
159
181
|
Parameters
|
|
160
182
|
----------
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
183
|
+
jsonl_path : Path
|
|
184
|
+
Path to the JSONL file
|
|
185
|
+
template_data : str
|
|
186
|
+
Template file content
|
|
187
|
+
target_document : str | None
|
|
188
|
+
Document to process
|
|
189
|
+
time_limit : int | None
|
|
190
|
+
Time limit in minutes
|
|
191
|
+
|
|
192
|
+
Returns
|
|
193
|
+
-------
|
|
194
|
+
tuple
|
|
195
|
+
(verified, reconstructed_code, suspicious_events, time_info, template_diff_text)
|
|
165
196
|
"""
|
|
166
|
-
|
|
197
|
+
try:
|
|
198
|
+
json_data = load_jsonl(jsonl_path)
|
|
199
|
+
except (FileNotFoundError, ValueError, IOError) as e:
|
|
200
|
+
print(f"Error loading {jsonl_path}: {e}", file=sys.stderr)
|
|
201
|
+
return False, "", [], None, ""
|
|
167
202
|
|
|
168
|
-
#
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
total_chars = event["total_chars"]
|
|
203
|
+
# Filter events for target document
|
|
204
|
+
doc_events = filter_events_by_document(json_data, target_document)
|
|
205
|
+
if target_document and not doc_events:
|
|
172
206
|
print(
|
|
173
|
-
f"
|
|
174
|
-
f"({total_chars} total chars)",
|
|
207
|
+
f"Warning: No events found for document '{target_document}' in {jsonl_path}",
|
|
175
208
|
file=sys.stderr,
|
|
176
209
|
)
|
|
210
|
+
return False, "", [], None, ""
|
|
177
211
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
for detail in event["detailed_events"]:
|
|
181
|
-
detail_idx = detail["event_index"]
|
|
182
|
-
detail_lines = detail["line_count"]
|
|
183
|
-
detail_chars = detail["char_count"]
|
|
184
|
-
detail_frag = detail["newFragment"]
|
|
185
|
-
print(
|
|
186
|
-
f" Event #{detail_idx}: {detail_lines} lines, "
|
|
187
|
-
f"{detail_chars} chars",
|
|
188
|
-
file=sys.stderr,
|
|
189
|
-
)
|
|
190
|
-
print(" ```", file=sys.stderr)
|
|
191
|
-
for line in detail_frag.split("\n"):
|
|
192
|
-
print(f" {line}", file=sys.stderr)
|
|
193
|
-
print(" ```", file=sys.stderr)
|
|
194
|
-
|
|
195
|
-
elif "event_indices" in event and reason == "rapid one-line pastes (AI indicator)":
|
|
196
|
-
# Rapid paste sequences (AI indicator) - show aggregate style
|
|
197
|
-
indices = event["event_indices"]
|
|
198
|
-
print(
|
|
199
|
-
f" AI Rapid Paste: Events #{indices[0]}-#{indices[-1]} "
|
|
200
|
-
f"({event['line_count']} lines, {event['char_count']} chars, "
|
|
201
|
-
f"{len(indices)} events in < 1 second)",
|
|
202
|
-
file=sys.stderr,
|
|
203
|
-
)
|
|
212
|
+
# Check time information
|
|
213
|
+
time_info = check_time_limit(doc_events, time_limit)
|
|
204
214
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
print(" Combined output:", file=sys.stderr)
|
|
211
|
-
print(" ```", file=sys.stderr)
|
|
212
|
-
for line in combined_content.split("\n"):
|
|
213
|
-
print(f" {line}", file=sys.stderr)
|
|
214
|
-
print(" ```", file=sys.stderr)
|
|
215
|
-
|
|
216
|
-
elif "event_indices" in event:
|
|
217
|
-
# Other multi-event clusters
|
|
218
|
-
indices = event.get("event_indices", [event["event_index"]])
|
|
219
|
-
print(
|
|
220
|
-
f" Events #{indices[0]}-#{indices[-1]} ({reason}): "
|
|
221
|
-
f"{event['line_count']} lines, {event['char_count']} chars",
|
|
222
|
-
file=sys.stderr,
|
|
215
|
+
# Verify and process the recording
|
|
216
|
+
try:
|
|
217
|
+
verified_template, suspicious_events = verify(template_data, doc_events)
|
|
218
|
+
reconstructed = reconstruct_file_from_events(
|
|
219
|
+
doc_events, verified_template, document_path=target_document
|
|
223
220
|
)
|
|
221
|
+
return True, reconstructed, suspicious_events, time_info, ""
|
|
222
|
+
except ValueError as e:
|
|
223
|
+
# If verification fails but we have events, still try to reconstruct
|
|
224
|
+
print(f"Warning: Verification failed for {jsonl_path}: {e}", file=sys.stderr)
|
|
225
|
+
try:
|
|
226
|
+
if not doc_events:
|
|
227
|
+
return False, "", [], time_info, ""
|
|
224
228
|
|
|
225
|
-
|
|
226
|
-
|
|
229
|
+
# Compute diff against template and still detect suspicious events
|
|
230
|
+
diff_text = template_diff(template_data, doc_events)
|
|
231
|
+
suspicious_events = detect_external_copypaste(doc_events)
|
|
232
|
+
|
|
233
|
+
# Reconstruct using the initial recorded state
|
|
234
|
+
initial_state = doc_events[0].get("newFragment", "")
|
|
235
|
+
reconstructed = reconstruct_file_from_events(
|
|
236
|
+
doc_events, initial_state, document_path=target_document
|
|
237
|
+
)
|
|
238
|
+
return False, reconstructed, suspicious_events, time_info, diff_text
|
|
239
|
+
except Exception as reconstruction_error:
|
|
240
|
+
print(
|
|
241
|
+
f"Error reconstructing {jsonl_path}: {type(reconstruction_error).__name__}: {reconstruction_error}",
|
|
242
|
+
file=sys.stderr,
|
|
243
|
+
)
|
|
244
|
+
return False, "", [], time_info, ""
|
|
245
|
+
except Exception as e:
|
|
227
246
|
print(
|
|
228
|
-
f"
|
|
229
|
-
f"{event['line_count']} lines, {event['char_count']} chars - "
|
|
230
|
-
f"newFragment:\n ```\n {new_fragment}\n ```",
|
|
247
|
+
f"Error processing {jsonl_path}: {type(e).__name__}: {e}",
|
|
231
248
|
file=sys.stderr,
|
|
232
249
|
)
|
|
250
|
+
return False, "", [], time_info, ""
|
|
233
251
|
|
|
234
252
|
|
|
235
|
-
def
|
|
236
|
-
|
|
237
|
-
|
|
253
|
+
def write_reconstructed_file(
|
|
254
|
+
output_path: Path,
|
|
255
|
+
content: str,
|
|
256
|
+
file_description: str = "Reconstructed code"
|
|
257
|
+
) -> bool:
|
|
238
258
|
"""
|
|
239
|
-
|
|
259
|
+
Write reconstructed code to a file.
|
|
240
260
|
|
|
241
261
|
Parameters
|
|
242
262
|
----------
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
263
|
+
output_path : Path
|
|
264
|
+
Path to write to
|
|
265
|
+
content : str
|
|
266
|
+
Content to write
|
|
267
|
+
file_description : str
|
|
268
|
+
Description for success message
|
|
269
|
+
|
|
270
|
+
Returns
|
|
271
|
+
-------
|
|
272
|
+
bool
|
|
273
|
+
True if successful, False otherwise
|
|
247
274
|
"""
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return event["detailed_events"][0].get("event_index", float("inf"))
|
|
257
|
-
event_idx = event.get("event_index", -1)
|
|
258
|
-
return event_idx if event_idx >= 0 else float("inf")
|
|
259
|
-
|
|
260
|
-
sorted_events = sorted(suspicious_events, key=get_sort_key)
|
|
261
|
-
|
|
262
|
-
for event in sorted_events:
|
|
263
|
-
display_suspicious_event(event, show_details)
|
|
264
|
-
else:
|
|
265
|
-
print("Success! No suspicious events detected.", file=sys.stderr)
|
|
275
|
+
try:
|
|
276
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
277
|
+
output_path.write_text(content)
|
|
278
|
+
print(f"{file_description} written to: {output_path}", file=sys.stderr)
|
|
279
|
+
return True
|
|
280
|
+
except Exception as e:
|
|
281
|
+
print(f"Error writing output file: {e}", file=sys.stderr)
|
|
282
|
+
return False
|
|
266
283
|
|
|
267
284
|
|
|
268
|
-
def
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
) -> None:
|
|
285
|
+
def handle_playback_mode(
|
|
286
|
+
jsonl_file: Path,
|
|
287
|
+
template_file: Path,
|
|
288
|
+
template_data: str,
|
|
289
|
+
document_override: str | None,
|
|
290
|
+
speed: float,
|
|
291
|
+
) -> int:
|
|
276
292
|
"""
|
|
277
|
-
|
|
293
|
+
Handle playback mode for a single file.
|
|
278
294
|
|
|
279
295
|
Parameters
|
|
280
296
|
----------
|
|
281
|
-
|
|
282
|
-
Path to
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
verified : bool
|
|
292
|
-
Whether the file passed verification
|
|
297
|
+
jsonl_file : Path
|
|
298
|
+
Path to the recording file
|
|
299
|
+
template_file : Path
|
|
300
|
+
Path to the template file
|
|
301
|
+
template_data : str
|
|
302
|
+
Template file content
|
|
303
|
+
document_override : str | None
|
|
304
|
+
Document override
|
|
305
|
+
speed : float
|
|
306
|
+
Playback speed
|
|
293
307
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
308
|
+
Returns
|
|
309
|
+
-------
|
|
310
|
+
int
|
|
311
|
+
Exit code (0 for success, 1 for error)
|
|
298
312
|
"""
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
"suspicious_events": suspicious_events,
|
|
304
|
-
"reconstructed_code": reconstructed_code,
|
|
305
|
-
}
|
|
313
|
+
try:
|
|
314
|
+
json_data = load_jsonl(jsonl_file)
|
|
315
|
+
recorded_docs = get_recorded_documents(json_data)
|
|
316
|
+
target_document = resolve_document(recorded_docs, template_file, document_override)
|
|
306
317
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
318
|
+
if target_document:
|
|
319
|
+
playback_recording(json_data, target_document, template_data, speed)
|
|
320
|
+
return 0
|
|
321
|
+
else:
|
|
322
|
+
print("Error: No documents found in recording", file=sys.stderr)
|
|
323
|
+
return 1
|
|
324
|
+
except Exception as e:
|
|
325
|
+
print(f"Error loading file for playback: {e}", file=sys.stderr)
|
|
326
|
+
return 1
|
|
311
327
|
|
|
312
328
|
|
|
313
|
-
def
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
) ->
|
|
329
|
+
def process_batch(
|
|
330
|
+
jsonl_files: list[Path],
|
|
331
|
+
template_base: Path | None,
|
|
332
|
+
template_data: str,
|
|
333
|
+
args: argparse.Namespace,
|
|
334
|
+
) -> tuple[list[dict[str, Any]], bool]:
|
|
319
335
|
"""
|
|
320
|
-
|
|
336
|
+
Process multiple recording files in batch mode.
|
|
321
337
|
|
|
322
338
|
Parameters
|
|
323
339
|
----------
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
340
|
+
jsonl_files : list[Path]
|
|
341
|
+
List of JSONL files to process
|
|
342
|
+
template_base : Path
|
|
343
|
+
Path to template file or directory
|
|
344
|
+
template_data : str
|
|
345
|
+
Template file content
|
|
346
|
+
args : argparse.Namespace
|
|
347
|
+
Command-line arguments
|
|
348
|
+
|
|
349
|
+
Returns
|
|
350
|
+
-------
|
|
351
|
+
tuple
|
|
352
|
+
(results, all_verified)
|
|
332
353
|
"""
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
return
|
|
339
|
-
|
|
340
|
-
# Start with template
|
|
341
|
-
current_content = template
|
|
342
|
-
last_timestamp = None
|
|
343
|
-
|
|
344
|
-
def clear_screen():
|
|
345
|
-
"""Clear the terminal screen."""
|
|
346
|
-
os.system('cls' if os.name == 'nt' else 'clear')
|
|
347
|
-
|
|
348
|
-
def parse_timestamp(ts_str: str) -> datetime:
|
|
349
|
-
"""Parse ISO timestamp string."""
|
|
350
|
-
return datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
|
|
351
|
-
|
|
352
|
-
# Show initial template
|
|
353
|
-
clear_screen()
|
|
354
|
-
print(f"=" * 80)
|
|
355
|
-
print(f"PLAYBACK: {document} (Speed: {speed}x)")
|
|
356
|
-
print(f"Event 0 / {len(doc_events)} - Initial Template")
|
|
357
|
-
print(f"=" * 80)
|
|
358
|
-
print(current_content)
|
|
359
|
-
print(f"\n{'=' * 80}")
|
|
360
|
-
print("Press Ctrl+C to stop playback")
|
|
361
|
-
time.sleep(2.0 / speed)
|
|
354
|
+
results = []
|
|
355
|
+
all_verified = True
|
|
356
|
+
output_dir = args.output_dir or (
|
|
357
|
+
args.output_file if args.output_file and args.output_file.is_dir() else None
|
|
358
|
+
)
|
|
362
359
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
360
|
+
for i, jsonl_file in enumerate(jsonl_files, 1):
|
|
361
|
+
print_batch_header(i, len(jsonl_files), jsonl_file.name)
|
|
362
|
+
|
|
363
|
+
# Determine target document for this file
|
|
364
|
+
try:
|
|
365
|
+
file_data = load_jsonl(jsonl_file)
|
|
366
|
+
recorded_docs = get_recorded_documents(file_data)
|
|
367
|
+
target_document = resolve_document(recorded_docs, template_base, args.document)
|
|
368
|
+
except (FileNotFoundError, ValueError, IOError) as e:
|
|
369
|
+
print(f"Error determining document: {e}", file=sys.stderr)
|
|
370
|
+
all_verified = False
|
|
371
|
+
continue
|
|
372
|
+
|
|
373
|
+
# If using template directory, find the matching template for this document
|
|
374
|
+
if args.template_dir and target_document:
|
|
375
|
+
matching_template_path = find_matching_template(args.template_dir, target_document)
|
|
376
|
+
if matching_template_path:
|
|
377
|
+
file_template_data = matching_template_path.read_text()
|
|
378
|
+
print(f"Using template: {matching_template_path.name}", file=sys.stderr)
|
|
382
379
|
else:
|
|
383
|
-
|
|
380
|
+
file_template_data = ""
|
|
381
|
+
print(
|
|
382
|
+
f"Warning: No matching template found for {target_document}. "
|
|
383
|
+
"Reconstruction will proceed without template verification.",
|
|
384
|
+
file=sys.stderr
|
|
385
|
+
)
|
|
386
|
+
else:
|
|
387
|
+
file_template_data = template_data
|
|
384
388
|
|
|
385
|
-
|
|
389
|
+
# Process the file
|
|
390
|
+
verified, reconstructed, suspicious_events, time_info, diff_text = process_single_file(
|
|
391
|
+
jsonl_file, file_template_data, target_document, args.time_limit
|
|
392
|
+
)
|
|
386
393
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
current_content = current_content[:offset] + new_frag + current_content[offset + len(old_frag):]
|
|
394
|
+
if not verified:
|
|
395
|
+
all_verified = False
|
|
390
396
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
397
|
+
# Display results
|
|
398
|
+
display_time_info(time_info)
|
|
399
|
+
display_suspicious_events(suspicious_events, args.show_autocomplete_details)
|
|
400
|
+
display_template_diff(diff_text)
|
|
401
|
+
|
|
402
|
+
# Store results
|
|
403
|
+
results.append({
|
|
404
|
+
"jsonl_file": jsonl_file,
|
|
405
|
+
"target_document": target_document,
|
|
406
|
+
"verified": verified,
|
|
407
|
+
"reconstructed": reconstructed,
|
|
408
|
+
"suspicious_events": suspicious_events,
|
|
409
|
+
"time_info": time_info,
|
|
410
|
+
"template_diff": diff_text,
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
# Write output file if requested
|
|
414
|
+
if reconstructed and output_dir:
|
|
415
|
+
output_name = jsonl_file.stem.replace(".recording.jsonl", "") + ".py"
|
|
416
|
+
output_path = output_dir / output_name
|
|
417
|
+
write_reconstructed_file(output_path, reconstructed, "Written to")
|
|
418
|
+
|
|
419
|
+
return results, all_verified
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def process_single(
|
|
423
|
+
jsonl_file: Path,
|
|
424
|
+
template_base: Path | None,
|
|
425
|
+
template_data: str,
|
|
426
|
+
args: argparse.Namespace,
|
|
427
|
+
) -> tuple[list[dict[str, Any]], bool]:
|
|
428
|
+
"""
|
|
429
|
+
Process a single recording file.
|
|
396
430
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
431
|
+
Parameters
|
|
432
|
+
----------
|
|
433
|
+
jsonl_file : Path
|
|
434
|
+
Path to JSONL file
|
|
435
|
+
template_base : Path
|
|
436
|
+
Path to template file or directory
|
|
437
|
+
template_data : str
|
|
438
|
+
Template file content
|
|
439
|
+
args : argparse.Namespace
|
|
440
|
+
Command-line arguments
|
|
401
441
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
442
|
+
Returns
|
|
443
|
+
-------
|
|
444
|
+
tuple
|
|
445
|
+
(results, verified)
|
|
446
|
+
"""
|
|
447
|
+
try:
|
|
448
|
+
file_data = load_jsonl(jsonl_file)
|
|
449
|
+
recorded_docs = get_recorded_documents(file_data)
|
|
450
|
+
target_document = resolve_document(recorded_docs, template_base, args.document)
|
|
451
|
+
except (FileNotFoundError, ValueError, IOError) as e:
|
|
452
|
+
print(f"Error determining document: {e}", file=sys.stderr)
|
|
453
|
+
return [], False
|
|
454
|
+
|
|
455
|
+
# If using template directory, find the matching template for this document
|
|
456
|
+
if args.template_dir and target_document:
|
|
457
|
+
matching_template_path = find_matching_template(args.template_dir, target_document)
|
|
458
|
+
if matching_template_path:
|
|
459
|
+
file_template_data = matching_template_path.read_text()
|
|
460
|
+
print(f"Using template: {matching_template_path.name}", file=sys.stderr)
|
|
461
|
+
else:
|
|
462
|
+
file_template_data = ""
|
|
463
|
+
print(
|
|
464
|
+
f"Warning: No matching template found for {target_document}. "
|
|
465
|
+
"Reconstruction will proceed without template verification.",
|
|
466
|
+
file=sys.stderr
|
|
467
|
+
)
|
|
468
|
+
else:
|
|
469
|
+
file_template_data = template_data
|
|
407
470
|
|
|
408
|
-
|
|
409
|
-
print("\n\nPlayback stopped by user.", file=sys.stderr)
|
|
410
|
-
return
|
|
471
|
+
print(f"Processing: {target_document or template_base}", file=sys.stderr)
|
|
411
472
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
473
|
+
verified, reconstructed, suspicious_events, time_info, diff_text = process_single_file(
|
|
474
|
+
jsonl_file, file_template_data, target_document, args.time_limit
|
|
475
|
+
)
|
|
415
476
|
|
|
477
|
+
# Display results
|
|
478
|
+
display_time_info(time_info)
|
|
479
|
+
display_suspicious_events(suspicious_events, args.show_autocomplete_details)
|
|
480
|
+
display_template_diff(diff_text)
|
|
416
481
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
482
|
+
# Write output file if requested
|
|
483
|
+
if reconstructed and args.output_file:
|
|
484
|
+
if not write_reconstructed_file(args.output_file, reconstructed):
|
|
485
|
+
return [], False
|
|
420
486
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
help="Path to the compressed JSONL file (*.recording.jsonl.gz)",
|
|
433
|
-
)
|
|
434
|
-
parser.add_argument(
|
|
435
|
-
"template_file",
|
|
436
|
-
type=Path,
|
|
437
|
-
help="Path to the initial template file that was recorded",
|
|
438
|
-
)
|
|
439
|
-
parser.add_argument(
|
|
440
|
-
"-t",
|
|
441
|
-
"--time-limit",
|
|
442
|
-
type=int,
|
|
443
|
-
default=None,
|
|
444
|
-
help="Maximum allowed time in minutes between first and last edit. "
|
|
445
|
-
"If exceeded, recording is flagged.",
|
|
446
|
-
)
|
|
447
|
-
parser.add_argument(
|
|
448
|
-
"-d",
|
|
449
|
-
"--document",
|
|
450
|
-
type=str,
|
|
451
|
-
default=None,
|
|
452
|
-
help="Document path or filename to process from the recording. "
|
|
453
|
-
"Defaults to the document whose extension matches the template file.",
|
|
454
|
-
)
|
|
455
|
-
parser.add_argument(
|
|
456
|
-
"-o",
|
|
457
|
-
"--output-json",
|
|
458
|
-
type=Path,
|
|
459
|
-
default=None,
|
|
460
|
-
help="Path to output JSON file with verification results "
|
|
461
|
-
"(time info and suspicious events).",
|
|
462
|
-
)
|
|
463
|
-
parser.add_argument(
|
|
464
|
-
"-s",
|
|
465
|
-
"--show-autocomplete-details",
|
|
466
|
-
action="store_true",
|
|
467
|
-
help="Show individual auto-complete events in addition to "
|
|
468
|
-
"aggregate statistics",
|
|
469
|
-
)
|
|
470
|
-
parser.add_argument(
|
|
471
|
-
"-q",
|
|
472
|
-
"--quiet",
|
|
473
|
-
action="store_true",
|
|
474
|
-
help="Suppress output of reconstructed code to stdout",
|
|
475
|
-
)
|
|
476
|
-
parser.add_argument(
|
|
477
|
-
"-p",
|
|
478
|
-
"--playback",
|
|
479
|
-
action="store_true",
|
|
480
|
-
help="Play back the recording in real-time, showing code evolution",
|
|
481
|
-
)
|
|
482
|
-
parser.add_argument(
|
|
483
|
-
"--playback-speed",
|
|
484
|
-
type=float,
|
|
485
|
-
default=1.0,
|
|
486
|
-
help="Playback speed multiplier (1.0 = real-time, 2.0 = 2x speed, 0.5 = half speed)",
|
|
487
|
-
)
|
|
488
|
-
return parser
|
|
487
|
+
results = [{
|
|
488
|
+
"jsonl_file": jsonl_file,
|
|
489
|
+
"target_document": target_document,
|
|
490
|
+
"verified": verified,
|
|
491
|
+
"reconstructed": reconstructed,
|
|
492
|
+
"suspicious_events": suspicious_events,
|
|
493
|
+
"time_info": time_info,
|
|
494
|
+
"template_diff": diff_text,
|
|
495
|
+
}]
|
|
496
|
+
|
|
497
|
+
return results, verified
|
|
489
498
|
|
|
490
499
|
|
|
491
500
|
def main() -> int:
|
|
@@ -500,112 +509,123 @@ def main() -> int:
|
|
|
500
509
|
parser = create_parser()
|
|
501
510
|
args = parser.parse_args()
|
|
502
511
|
|
|
503
|
-
#
|
|
512
|
+
# Parse files argument: last one may be template_file if it's not a JSONL file
|
|
513
|
+
files_list = args.files
|
|
514
|
+
template_file = None
|
|
515
|
+
jsonl_patterns = files_list
|
|
516
|
+
|
|
517
|
+
# If we have more than one file and the last one doesn't look like a JSONL file,
|
|
518
|
+
# treat it as the template file
|
|
519
|
+
if len(files_list) > 1 and not files_list[-1].endswith(('.jsonl', '.jsonl.gz')):
|
|
520
|
+
template_file = Path(files_list[-1])
|
|
521
|
+
jsonl_patterns = files_list[:-1]
|
|
522
|
+
|
|
523
|
+
# Validate that at least one of template_file or template_dir is provided
|
|
524
|
+
if not template_file and not args.template_dir:
|
|
525
|
+
print("Error: Either a template file or --template-dir must be provided", file=sys.stderr)
|
|
526
|
+
parser.print_help()
|
|
527
|
+
return 1
|
|
528
|
+
|
|
529
|
+
# Expand file patterns and validate
|
|
504
530
|
try:
|
|
505
|
-
|
|
531
|
+
jsonl_files = expand_file_patterns(jsonl_patterns)
|
|
506
532
|
except FileNotFoundError as e:
|
|
507
533
|
print(f"Error: {e}", file=sys.stderr)
|
|
508
534
|
return 1
|
|
509
|
-
except (ValueError, IOError) as e:
|
|
510
|
-
print(f"Error loading JSONL file: {e}", file=sys.stderr)
|
|
511
|
-
return 1
|
|
512
535
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
target_document = resolve_document(
|
|
517
|
-
recorded_docs, args.template_file, args.document
|
|
518
|
-
)
|
|
519
|
-
except ValueError as e:
|
|
520
|
-
print(f"Error determining document: {e}", file=sys.stderr)
|
|
521
|
-
return 1
|
|
536
|
+
batch_mode = len(jsonl_files) > 1
|
|
537
|
+
if batch_mode:
|
|
538
|
+
print(f"Processing {len(jsonl_files)} recording files in batch mode", file=sys.stderr)
|
|
522
539
|
|
|
523
|
-
#
|
|
524
|
-
if args.
|
|
540
|
+
# Determine template source (use template_dir if provided, otherwise template_file)
|
|
541
|
+
template_path = args.template_dir if args.template_dir else template_file
|
|
542
|
+
|
|
543
|
+
# Handle playback mode (single file only)
|
|
544
|
+
if not batch_mode and args.playback:
|
|
525
545
|
try:
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
546
|
+
json_data = load_jsonl(jsonl_files[0])
|
|
547
|
+
recorded_docs = get_recorded_documents(json_data)
|
|
548
|
+
target_document = resolve_document(recorded_docs, template_path, args.document)
|
|
549
|
+
|
|
550
|
+
# Get template data for playback
|
|
551
|
+
template_data, _ = resolve_template_file(
|
|
552
|
+
template_file if not args.template_dir else None,
|
|
553
|
+
args.template_dir,
|
|
554
|
+
target_document
|
|
555
|
+
)
|
|
530
556
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
557
|
+
if target_document:
|
|
558
|
+
playback_recording(json_data, target_document, template_data, args.playback_speed)
|
|
559
|
+
return 0
|
|
560
|
+
else:
|
|
561
|
+
print("Error: No documents found in recording", file=sys.stderr)
|
|
562
|
+
return 1
|
|
563
|
+
except Exception as e:
|
|
564
|
+
print(f"Error loading file for playback: {e}", file=sys.stderr)
|
|
536
565
|
return 1
|
|
537
566
|
|
|
538
|
-
#
|
|
539
|
-
doc_events = filter_events_by_document(json_data, target_document)
|
|
540
|
-
if target_document and not doc_events:
|
|
541
|
-
print(
|
|
542
|
-
f"Error: No events found for document '{target_document}'",
|
|
543
|
-
file=sys.stderr,
|
|
544
|
-
)
|
|
545
|
-
return 1
|
|
546
|
-
|
|
547
|
-
print(f"Processing: {target_document or args.template_file}", file=sys.stderr)
|
|
548
|
-
|
|
549
|
-
# Read template file
|
|
567
|
+
# Get template data
|
|
550
568
|
try:
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
569
|
+
# If using a template directory, skip loading a global template here
|
|
570
|
+
# Let per-file matching handle it in process_batch/process_single
|
|
571
|
+
if args.template_dir:
|
|
572
|
+
template_data = ""
|
|
573
|
+
else:
|
|
574
|
+
template_data, _ = resolve_template_file(
|
|
575
|
+
template_file if not args.template_dir else None,
|
|
576
|
+
None,
|
|
577
|
+
None
|
|
578
|
+
)
|
|
579
|
+
except (FileNotFoundError, ValueError) as e:
|
|
580
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
559
581
|
return 1
|
|
560
582
|
|
|
561
|
-
#
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
try:
|
|
570
|
-
template_data, suspicious_events = verify(template_data, doc_events)
|
|
571
|
-
reconstructed = reconstruct_file_from_events(
|
|
572
|
-
doc_events, template_data, document_path=target_document
|
|
583
|
+
# Process files
|
|
584
|
+
if batch_mode:
|
|
585
|
+
results, all_verified = process_batch(
|
|
586
|
+
jsonl_files, template_path, template_data, args
|
|
587
|
+
)
|
|
588
|
+
else:
|
|
589
|
+
results, all_verified = process_single(
|
|
590
|
+
jsonl_files[0], template_path, template_data, args
|
|
573
591
|
)
|
|
574
|
-
verified = True
|
|
575
|
-
if not args.quiet:
|
|
576
|
-
print(reconstructed)
|
|
577
|
-
|
|
578
|
-
# Display suspicious events
|
|
579
|
-
display_suspicious_events(suspicious_events, args.show_autocomplete_details)
|
|
580
592
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
print(str(e), file=sys.stderr)
|
|
584
|
-
try:
|
|
585
|
-
print(template_diff(template_data, doc_events), file=sys.stderr)
|
|
586
|
-
except Exception:
|
|
587
|
-
pass
|
|
588
|
-
verified = False
|
|
589
|
-
except Exception as e:
|
|
590
|
-
print(f"Error processing file: {type(e).__name__}: {e}", file=sys.stderr)
|
|
591
|
-
verified = False
|
|
593
|
+
if not results:
|
|
594
|
+
return 1
|
|
592
595
|
|
|
593
|
-
#
|
|
594
|
-
if
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
)
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
596
|
+
# Output summary and combined report for batch mode
|
|
597
|
+
if batch_mode:
|
|
598
|
+
failed_files = [r["jsonl_file"].name for r in results if not r["verified"]]
|
|
599
|
+
verified_count = len(results) - len(failed_files)
|
|
600
|
+
print_batch_summary(len(results), verified_count, failed_files)
|
|
601
|
+
|
|
602
|
+
# Display combined time report
|
|
603
|
+
time_infos = [r["time_info"] for r in results]
|
|
604
|
+
combined_time = None
|
|
605
|
+
if any(time_infos):
|
|
606
|
+
combined_time = combine_time_info(time_infos, args.time_limit)
|
|
607
|
+
display_time_info(combined_time, is_combined=True)
|
|
608
|
+
|
|
609
|
+
# Write JSON output
|
|
610
|
+
if args.output_json:
|
|
611
|
+
try:
|
|
612
|
+
write_batch_json_output(
|
|
613
|
+
args.output_json, results, combined_time, all_verified, batch_mode=True
|
|
614
|
+
)
|
|
615
|
+
except Exception as e:
|
|
616
|
+
print(f"Error writing batch JSON output: {e}", file=sys.stderr)
|
|
617
|
+
else:
|
|
618
|
+
# Single file mode - write JSON output
|
|
619
|
+
if args.output_json:
|
|
620
|
+
try:
|
|
621
|
+
write_batch_json_output(
|
|
622
|
+
args.output_json, results, results[0]["time_info"],
|
|
623
|
+
results[0]["verified"], batch_mode=False
|
|
624
|
+
)
|
|
625
|
+
except Exception as e:
|
|
626
|
+
print(f"Error writing JSON output: {e}", file=sys.stderr)
|
|
607
627
|
|
|
608
|
-
return 0 if
|
|
628
|
+
return 0 if all_verified else 1
|
|
609
629
|
|
|
610
630
|
|
|
611
631
|
if __name__ == "__main__":
|