oddrun 0.1.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.
oddrun-0.1.0/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OddRun Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO MECHANICAL FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
18
+ FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
20
+ THE USE OR OTHER DEALINGS IN THE SOFTWARE.
oddrun-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: oddrun
3
+ Version: 0.1.0
4
+ Summary: Investigates why the same Python program behaves differently across environments.
5
+ Author: OddRun Contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/oddrun/oddrun
8
+ Project-URL: Documentation, https://github.com/oddrun/oddrun#readme
9
+ Project-URL: Repository, https://github.com/oddrun/oddrun
10
+ Project-URL: Issues, https://github.com/oddrun/oddrun/issues
11
+ Keywords: debugging,environment,reproducibility,diff,snapshot,developer-tools
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Software Development :: Debuggers
21
+ Classifier: Topic :: Software Development :: Testing
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
27
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
28
+ Requires-Dist: build>=1.0.0; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ # OddRun
32
+
33
+ > When the same code doesn't behave the same.
34
+
35
+ OddRun is a Python developer tool that investigates why the same program behaves differently across environments.
36
+
37
+ Traditional environment tools answer:
38
+ > *"What is different between these environments?"*
39
+
40
+ OddRun answers:
41
+ > *"Which difference experimentally explains the observed failure?"*
42
+
43
+ ---
44
+
45
+ ## Core Product Model
46
+
47
+ | Subcommand | Question | Input Artifact |
48
+ | :--- | :--- | :--- |
49
+ | `oddrun capture` | **What's different?** Captures environment state | `EnvironmentSnapshot` JSON |
50
+ | `oddrun compare` | **What changed?** Compares two environment snapshots | Two `EnvironmentSnapshot` JSONs |
51
+ | `oddrun record` | **What actually happened here?** Executes command 1x on target | `ExecutionRecord` JSON |
52
+ | `oddrun why` | **Which difference explains the target behavior?** | `ExecutionRecord` JSON + `<command>` |
53
+
54
+ ---
55
+
56
+ ## Quickstart
57
+
58
+ ### Installation
59
+
60
+ ```bash
61
+ pip install oddrun
62
+ ```
63
+
64
+ ### The Distributed Workflow (`record` → `why`)
65
+
66
+ #### 1. On Target Environment (e.g. CI / Remote Server where code fails)
67
+ Execute the failing command once to record the environment state and observed failure signature:
68
+
69
+ ```bash
70
+ oddrun record -o target-run.json -- pytest tests/test_date.py
71
+ ```
72
+ *(Generates `target-run.json` containing safe environment metadata and the observed failure `BehaviorSignature`)*
73
+
74
+ #### 2. On Developer Machine (Baseline environment where code passes)
75
+ Diagnose which environmental difference reproduces the target failure:
76
+
77
+ ```bash
78
+ oddrun why target-run.json -- pytest tests/test_date.py
79
+ ```
80
+
81
+ OddRun will:
82
+ 1. Verify baseline stability (3/3 identical passing runs in current environment).
83
+ 2. Rank environmental differences by safety tier (`TZ`, `LANG`, `LC_*`, application variables).
84
+ 3. Execute controlled perturbations (1x exploratory + 2x confirmation).
85
+ 4. Match structural behavior signatures (`exit_code`, `exception_type`, normalized diagnostic output).
86
+ 5. Report **Experimentally Supported Causal Factors** with remediation recommendations.
87
+
88
+ ---
89
+
90
+ ## Direct Environment Comparison (`capture` → `compare`)
91
+
92
+ ```bash
93
+ # Capture local environment snapshot
94
+ oddrun capture -o local.json
95
+
96
+ # Compare local environment against target snapshot
97
+ oddrun compare local.json target-run.json
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Python API Usage
103
+
104
+ ```python
105
+ from oddrun import (
106
+ capture_environment,
107
+ compare_snapshots,
108
+ record_execution,
109
+ diagnose_behavior,
110
+ load_record,
111
+ )
112
+
113
+ # 1. Record execution
114
+ record = record_execution(["pytest", "tests/test_date.py"])
115
+ print(f"Recorded outcome: {record.execution_result.behavior_signature.is_success}")
116
+
117
+ # 2. Diagnose behavior against target record
118
+ report = diagnose_behavior("target-run.json", ["pytest", "tests/test_date.py"])
119
+ for factor in report.causal_factors:
120
+ print(f"Causal Factor: {factor.candidate.key} ({factor.evidence_level.value})")
121
+ ```
122
+
123
+ ---
124
+
125
+ ## Security & Privacy
126
+
127
+ OddRun is strictly **local-first**:
128
+ - Zero telemetry or analytics.
129
+ - Zero network calls or cloud dependencies; operates 100% offline.
130
+ - **Automatic Secret Redaction**: Sensitive environment keys (`API_KEY`, `SECRET`, `PASSWORD`, `TOKEN`, `CREDENTIAL`, `AUTH`, etc.) are automatically replaced with `"<present>"` before saving snapshots or execution records to disk.
131
+
132
+ ---
133
+
134
+ ## License
135
+
136
+ [MIT License](LICENSE)
oddrun-0.1.0/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # OddRun
2
+
3
+ > When the same code doesn't behave the same.
4
+
5
+ OddRun is a Python developer tool that investigates why the same program behaves differently across environments.
6
+
7
+ Traditional environment tools answer:
8
+ > *"What is different between these environments?"*
9
+
10
+ OddRun answers:
11
+ > *"Which difference experimentally explains the observed failure?"*
12
+
13
+ ---
14
+
15
+ ## Core Product Model
16
+
17
+ | Subcommand | Question | Input Artifact |
18
+ | :--- | :--- | :--- |
19
+ | `oddrun capture` | **What's different?** Captures environment state | `EnvironmentSnapshot` JSON |
20
+ | `oddrun compare` | **What changed?** Compares two environment snapshots | Two `EnvironmentSnapshot` JSONs |
21
+ | `oddrun record` | **What actually happened here?** Executes command 1x on target | `ExecutionRecord` JSON |
22
+ | `oddrun why` | **Which difference explains the target behavior?** | `ExecutionRecord` JSON + `<command>` |
23
+
24
+ ---
25
+
26
+ ## Quickstart
27
+
28
+ ### Installation
29
+
30
+ ```bash
31
+ pip install oddrun
32
+ ```
33
+
34
+ ### The Distributed Workflow (`record` → `why`)
35
+
36
+ #### 1. On Target Environment (e.g. CI / Remote Server where code fails)
37
+ Execute the failing command once to record the environment state and observed failure signature:
38
+
39
+ ```bash
40
+ oddrun record -o target-run.json -- pytest tests/test_date.py
41
+ ```
42
+ *(Generates `target-run.json` containing safe environment metadata and the observed failure `BehaviorSignature`)*
43
+
44
+ #### 2. On Developer Machine (Baseline environment where code passes)
45
+ Diagnose which environmental difference reproduces the target failure:
46
+
47
+ ```bash
48
+ oddrun why target-run.json -- pytest tests/test_date.py
49
+ ```
50
+
51
+ OddRun will:
52
+ 1. Verify baseline stability (3/3 identical passing runs in current environment).
53
+ 2. Rank environmental differences by safety tier (`TZ`, `LANG`, `LC_*`, application variables).
54
+ 3. Execute controlled perturbations (1x exploratory + 2x confirmation).
55
+ 4. Match structural behavior signatures (`exit_code`, `exception_type`, normalized diagnostic output).
56
+ 5. Report **Experimentally Supported Causal Factors** with remediation recommendations.
57
+
58
+ ---
59
+
60
+ ## Direct Environment Comparison (`capture` → `compare`)
61
+
62
+ ```bash
63
+ # Capture local environment snapshot
64
+ oddrun capture -o local.json
65
+
66
+ # Compare local environment against target snapshot
67
+ oddrun compare local.json target-run.json
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Python API Usage
73
+
74
+ ```python
75
+ from oddrun import (
76
+ capture_environment,
77
+ compare_snapshots,
78
+ record_execution,
79
+ diagnose_behavior,
80
+ load_record,
81
+ )
82
+
83
+ # 1. Record execution
84
+ record = record_execution(["pytest", "tests/test_date.py"])
85
+ print(f"Recorded outcome: {record.execution_result.behavior_signature.is_success}")
86
+
87
+ # 2. Diagnose behavior against target record
88
+ report = diagnose_behavior("target-run.json", ["pytest", "tests/test_date.py"])
89
+ for factor in report.causal_factors:
90
+ print(f"Causal Factor: {factor.candidate.key} ({factor.evidence_level.value})")
91
+ ```
92
+
93
+ ---
94
+
95
+ ## Security & Privacy
96
+
97
+ OddRun is strictly **local-first**:
98
+ - Zero telemetry or analytics.
99
+ - Zero network calls or cloud dependencies; operates 100% offline.
100
+ - **Automatic Secret Redaction**: Sensitive environment keys (`API_KEY`, `SECRET`, `PASSWORD`, `TOKEN`, `CREDENTIAL`, `AUTH`, etc.) are automatically replaced with `"<present>"` before saving snapshots or execution records to disk.
101
+
102
+ ---
103
+
104
+ ## License
105
+
106
+ [MIT License](LICENSE)
@@ -0,0 +1,75 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "oddrun"
7
+ version = "0.1.0"
8
+ description = "Investigates why the same Python program behaves differently across environments."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "OddRun Contributors" }
14
+ ]
15
+ keywords = [
16
+ "debugging",
17
+ "environment",
18
+ "reproducibility",
19
+ "diff",
20
+ "snapshot",
21
+ "developer-tools"
22
+ ]
23
+ classifiers = [
24
+ "Development Status :: 3 - Alpha",
25
+ "Intended Audience :: Developers",
26
+ "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3.10",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.13",
31
+ "Programming Language :: Python :: 3.14",
32
+ "Topic :: Software Development :: Debuggers",
33
+ "Topic :: Software Development :: Testing",
34
+ ]
35
+ dependencies = []
36
+
37
+ [project.optional-dependencies]
38
+ dev = [
39
+ "pytest>=7.0.0",
40
+ "ruff>=0.1.0",
41
+ "build>=1.0.0",
42
+ ]
43
+
44
+ [project.scripts]
45
+ oddrun = "oddrun.cli:main"
46
+
47
+ [project.urls]
48
+ Homepage = "https://github.com/oddrun/oddrun"
49
+ Documentation = "https://github.com/oddrun/oddrun#readme"
50
+ Repository = "https://github.com/oddrun/oddrun"
51
+ Issues = "https://github.com/oddrun/oddrun/issues"
52
+
53
+ [tool.setuptools.packages.find]
54
+ where = ["src"]
55
+
56
+ [tool.ruff]
57
+ line-length = 88
58
+ target-version = "py310"
59
+
60
+ [tool.ruff.lint]
61
+ select = [
62
+ "E", # pycodestyle errors
63
+ "W", # pycodestyle warnings
64
+ "F", # pyflakes
65
+ "I", # isort
66
+ "B", # flake8-bugbear
67
+ "C4", # flake8-comprehensions
68
+ "UP", # pyupgrade
69
+ ]
70
+ ignore = []
71
+
72
+ [tool.pytest.ini_options]
73
+ testpaths = ["tests"]
74
+ python_files = ["test_*.py"]
75
+ addopts = "-v"
oddrun-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,34 @@
1
+ """OddRun - When the same code doesn't behave the same."""
2
+
3
+ from oddrun.compare import compare_snapshots
4
+ from oddrun.diagnose import diagnose_behavior
5
+ from oddrun.execution import ExecutionRecord, record_execution
6
+ from oddrun.io import load_record, load_snapshot, save_record, save_snapshot
7
+ from oddrun.models import (
8
+ DiffItem,
9
+ EnvironmentSnapshot,
10
+ PythonInfo,
11
+ SnapshotDiff,
12
+ SystemInfo,
13
+ )
14
+ from oddrun.snapshot import capture_environment
15
+
16
+ __version__ = "0.1.0"
17
+
18
+ __all__ = [
19
+ "__version__",
20
+ "EnvironmentSnapshot",
21
+ "ExecutionRecord",
22
+ "PythonInfo",
23
+ "SystemInfo",
24
+ "SnapshotDiff",
25
+ "DiffItem",
26
+ "capture_environment",
27
+ "compare_snapshots",
28
+ "diagnose_behavior",
29
+ "load_record",
30
+ "load_snapshot",
31
+ "record_execution",
32
+ "save_record",
33
+ "save_snapshot",
34
+ ]
@@ -0,0 +1,325 @@
1
+ """Command-line interface for OddRun."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from collections.abc import Sequence
9
+
10
+ from oddrun import __version__
11
+ from oddrun.compare import compare_snapshots
12
+ from oddrun.diagnose import diagnose_behavior, format_diagnosis_text
13
+ from oddrun.execution import ExecutionStatus, record_execution
14
+ from oddrun.io import SnapshotIOError, load_snapshot, save_record, save_snapshot
15
+ from oddrun.models import SnapshotDiff
16
+ from oddrun.snapshot import capture_environment
17
+
18
+
19
+ def format_diff_text(diff: SnapshotDiff) -> str:
20
+ """Format a SnapshotDiff into readable text lines."""
21
+ if not diff.has_differences:
22
+ return "No differences found between snapshots."
23
+
24
+ lines: list[str] = [
25
+ f"OddRun Environment Comparison: {diff.left_label} vs {diff.right_label}",
26
+ f"Total Differences: {len(diff.differences)}",
27
+ "-" * 60,
28
+ ]
29
+
30
+ current_cat = ""
31
+ for item in diff.differences:
32
+ if item.category != current_cat:
33
+ current_cat = item.category
34
+ lines.append(f"\n[{current_cat.upper()}]")
35
+
36
+ if item.change_type == "added":
37
+ lines.append(f" + {item.key}: {item.right_value}")
38
+ elif item.change_type == "removed":
39
+ lines.append(f" - {item.key}: (was: {item.left_value})")
40
+ else:
41
+ lines.append(f" ~ {item.key}: {item.left_value} -> {item.right_value}")
42
+
43
+ return "\n".join(lines)
44
+
45
+
46
+ def run_capture(args: argparse.Namespace) -> int:
47
+ """Execute 'oddrun capture' subcommand."""
48
+ snapshot = capture_environment()
49
+ output_path = args.output or "oddrun-snapshot.json"
50
+
51
+ try:
52
+ save_snapshot(snapshot, output_path)
53
+ print(f"Successfully captured environment snapshot: {output_path}")
54
+ return 0
55
+ except SnapshotIOError as exc:
56
+ print(f"ERROR: Could not write snapshot: {output_path}", file=sys.stderr)
57
+ print(f"Reason: {exc}", file=sys.stderr)
58
+ return 1
59
+ except Exception as exc:
60
+ if args.debug:
61
+ raise
62
+ print(f"ERROR: Unexpected failure during capture: {exc}", file=sys.stderr)
63
+ return 1
64
+
65
+
66
+ def run_record(args: argparse.Namespace) -> int:
67
+ """Execute 'oddrun record' subcommand."""
68
+ command_argv = args.command
69
+ if not command_argv:
70
+ print("ERROR: No command specified for recording.", file=sys.stderr)
71
+ msg = "Usage: oddrun record --output RECORD.json -- COMMAND [ARGUMENTS...]"
72
+ print(msg, file=sys.stderr)
73
+ return 1
74
+
75
+ output_path = args.output or "oddrun-record.json"
76
+
77
+ try:
78
+ record = record_execution(command_argv, timeout_seconds=args.timeout)
79
+ if record.execution_result.status == ExecutionStatus.EXEC_ERROR:
80
+ err_msg = record.execution_result.error_message or "Execution failed"
81
+ print(f"ERROR: Could not execute command: {err_msg}", file=sys.stderr)
82
+ return 1
83
+
84
+ save_record(record, output_path)
85
+ outcome = "PASS" if record.execution_result.exit_code == 0 else "FAIL"
86
+ code = record.execution_result.exit_code
87
+ print(
88
+ f"Successfully recorded execution "
89
+ f"(Outcome: {outcome}, Exit Code: {code}): {output_path}"
90
+ )
91
+ return 0
92
+ except SnapshotIOError as exc:
93
+ print(f"ERROR: Could not write record file: {output_path}", file=sys.stderr)
94
+ print(f"Reason: {exc}", file=sys.stderr)
95
+ return 1
96
+ except Exception as exc:
97
+ if args.debug:
98
+ raise
99
+ print(f"ERROR: Unexpected failure during recording: {exc}", file=sys.stderr)
100
+ return 1
101
+
102
+
103
+ def run_compare(args: argparse.Namespace) -> int:
104
+ """Execute 'oddrun compare' subcommand."""
105
+ left_file = args.snapshot1
106
+ right_file = args.snapshot2
107
+
108
+ try:
109
+ left_snap = load_snapshot(left_file)
110
+ except SnapshotIOError as exc:
111
+ print(f"ERROR: Could not read snapshot: {left_file}", file=sys.stderr)
112
+ print(f"Reason: {exc}", file=sys.stderr)
113
+ return 1
114
+ except Exception as exc:
115
+ if args.debug:
116
+ raise
117
+ print(f"ERROR: Failed loading snapshot '{left_file}': {exc}", file=sys.stderr)
118
+ return 1
119
+
120
+ try:
121
+ right_snap = load_snapshot(right_file)
122
+ except SnapshotIOError as exc:
123
+ print(f"ERROR: Could not read snapshot: {right_file}", file=sys.stderr)
124
+ print(f"Reason: {exc}", file=sys.stderr)
125
+ return 1
126
+ except Exception as exc:
127
+ if args.debug:
128
+ raise
129
+ print(f"ERROR: Failed loading snapshot '{right_file}': {exc}", file=sys.stderr)
130
+ return 1
131
+
132
+ diff = compare_snapshots(
133
+ left_snap, right_snap, left_label=str(left_file), right_label=str(right_file)
134
+ )
135
+
136
+ if args.json:
137
+ print(json.dumps(diff.to_dict(), indent=2))
138
+ else:
139
+ print(format_diff_text(diff))
140
+
141
+ return 0
142
+
143
+
144
+ def run_why(args: argparse.Namespace) -> int:
145
+ """Execute 'oddrun why' subcommand."""
146
+ target_record = args.target_record
147
+ command_argv = args.command
148
+
149
+ if not command_argv:
150
+ print("ERROR: No command specified for diagnosis.", file=sys.stderr)
151
+ msg = "Usage: oddrun why TARGET_RECORD.json -- COMMAND [ARGUMENTS...]"
152
+ print(msg, file=sys.stderr)
153
+ return 1
154
+
155
+ try:
156
+ report = diagnose_behavior(
157
+ target_record=target_record,
158
+ command=command_argv,
159
+ allow_path_perturbation=args.allow_path_perturbation,
160
+ max_experiments=args.max_experiments,
161
+ timeout_seconds=args.timeout,
162
+ )
163
+ print(format_diagnosis_text(report))
164
+ return 0 if not report.error_message else 1
165
+ except SnapshotIOError as exc:
166
+ print(f"ERROR: Could not read target record: {target_record}", file=sys.stderr)
167
+ print(f"Reason: {exc}", file=sys.stderr)
168
+ return 1
169
+ except Exception as exc:
170
+ if args.debug:
171
+ raise
172
+ print(f"ERROR: Causal diagnosis failed: {exc}", file=sys.stderr)
173
+ return 1
174
+
175
+
176
+ def build_parser() -> argparse.ArgumentParser:
177
+ """Construct the main argument parser for OddRun CLI."""
178
+ parser = argparse.ArgumentParser(
179
+ prog="oddrun",
180
+ description="OddRun - When the same code doesn't behave the same.",
181
+ )
182
+ parser.add_argument(
183
+ "--version",
184
+ action="version",
185
+ version=f"%(prog)s {__version__}",
186
+ )
187
+ parser.add_argument(
188
+ "--debug",
189
+ action="store_true",
190
+ help="Enable full exception tracebacks on error.",
191
+ )
192
+
193
+ subparsers = parser.add_subparsers(dest="subcommand", help="Subcommands")
194
+
195
+ # Command: capture
196
+ capture_parser = subparsers.add_parser(
197
+ "capture",
198
+ help="Capture a privacy-aware snapshot of the current environment.",
199
+ )
200
+ capture_parser.add_argument(
201
+ "-o",
202
+ "--output",
203
+ default="oddrun-snapshot.json",
204
+ help="Path to output JSON file (default: oddrun-snapshot.json).",
205
+ )
206
+
207
+ # Command: record
208
+ record_parser = subparsers.add_parser(
209
+ "record",
210
+ help="Execute a command and record environment snapshot + behavior signature.",
211
+ )
212
+ record_parser.add_argument(
213
+ "-o",
214
+ "--output",
215
+ default="oddrun-record.json",
216
+ help="Path to output JSON record file (default: oddrun-record.json).",
217
+ )
218
+ record_parser.add_argument(
219
+ "--timeout",
220
+ type=float,
221
+ default=30.0,
222
+ help="Timeout in seconds for recording command execution (default: 30.0).",
223
+ )
224
+ record_parser.add_argument(
225
+ "command",
226
+ nargs=argparse.REMAINDER,
227
+ help="Command and arguments to execute after '--' separator.",
228
+ )
229
+
230
+ # Command: compare
231
+ compare_parser = subparsers.add_parser(
232
+ "compare",
233
+ help="Compare two environment snapshot files.",
234
+ )
235
+ compare_parser.add_argument(
236
+ "snapshot1",
237
+ help="Path to first snapshot JSON file.",
238
+ )
239
+ compare_parser.add_argument(
240
+ "snapshot2",
241
+ help="Path to second snapshot JSON file.",
242
+ )
243
+ compare_parser.add_argument(
244
+ "--json",
245
+ action="store_true",
246
+ help="Output comparison results in structured JSON format.",
247
+ )
248
+
249
+ # Command: why
250
+ why_parser = subparsers.add_parser(
251
+ "why",
252
+ help="Diagnose which environmental difference causes a program to fail.",
253
+ )
254
+ why_parser.add_argument(
255
+ "target_record",
256
+ help="Path to target ExecutionRecord JSON file.",
257
+ )
258
+ why_parser.add_argument(
259
+ "--allow-path-perturbation",
260
+ action="store_true",
261
+ help=(
262
+ "Allow testing Tier-2 executable/import factors "
263
+ "(PATH, PYTHONPATH, PYTHONHOME)."
264
+ ),
265
+ )
266
+ why_parser.add_argument(
267
+ "--max-experiments",
268
+ type=int,
269
+ default=10,
270
+ help="Maximum candidate perturbation experiments to test (default: 10).",
271
+ )
272
+ why_parser.add_argument(
273
+ "--timeout",
274
+ type=float,
275
+ default=30.0,
276
+ help="Timeout in seconds per perturbation experiment (default: 30.0).",
277
+ )
278
+ why_parser.add_argument(
279
+ "command",
280
+ nargs=argparse.REMAINDER,
281
+ help="Command and arguments to execute after '--' separator.",
282
+ )
283
+
284
+ return parser
285
+
286
+
287
+ def main(argv: Sequence[str] | None = None) -> int:
288
+ """CLI entry point."""
289
+ parser = build_parser()
290
+
291
+ raw_argv = list(argv) if argv is not None else sys.argv[1:]
292
+ command_argv: list[str] = []
293
+
294
+ if "--" in raw_argv:
295
+ split_idx = raw_argv.index("--")
296
+ oddrun_args = raw_argv[:split_idx]
297
+ command_argv = raw_argv[split_idx + 1 :]
298
+ else:
299
+ oddrun_args = raw_argv
300
+
301
+ args = parser.parse_args(oddrun_args)
302
+ if command_argv:
303
+ args.command = command_argv
304
+ elif hasattr(args, "command") and args.command and args.command[0] == "--":
305
+ args.command = args.command[1:]
306
+
307
+ if not args.subcommand:
308
+ parser.print_help()
309
+ return 0
310
+
311
+ if args.subcommand == "capture":
312
+ return run_capture(args)
313
+ elif args.subcommand == "record":
314
+ return run_record(args)
315
+ elif args.subcommand == "compare":
316
+ return run_compare(args)
317
+ elif args.subcommand == "why":
318
+ return run_why(args)
319
+
320
+ parser.print_help()
321
+ return 0
322
+
323
+
324
+ if __name__ == "__main__":
325
+ sys.exit(main())