oct
Loading...
Searching...
No Matches
probe.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oct/complexity/probe.py
4
5"""
6Purpose
7-------
8Run the Plan 214 worst-first CRAP probe over the Option C monorepo.
9
10Responsibilities
11----------------
12- Execute each required MAIN-tree subproject suite under coverage.py.
13- Invoke ``python -m oct.complexity`` against MAIN sources through worktree PYTHONPATH.
14- Preserve failed suites as UNMEASURED with their captured output.
15- Generate the full JSON artifact and top-25-per-project Markdown report.
16
17Diagnostics
18-----------
19Domain: OCT-COMPLEXITY
20Levels:
21 L2 - probe lifecycle
22 L3 - subproject measurement details
23 L4 - deep tracing
24
25Contracts
26---------
27- The probe measures and never gates.
28- Failed suites, parse failures, and never-imported functions are never omitted.
29- Analyzer commands run against MAIN sources with this worktree's instrument.
30
31Dependencies
32------------
33- oct.complexity.coverage (coverage suite runner)
34- oct.complexity (not imported; run as python -m oct.complexity for the JSON analysis subprocess)
35"""
36
37from __future__ import annotations
38
39import argparse
40import datetime as dt
41import json
42import os
43import subprocess
44import sys
45from dataclasses import dataclass
46from pathlib import Path
47from typing import Sequence
48
49from oct.complexity.coverage import CoverageRun, run_pytest_with_coverage
50
51# OCT-LINT: disable-file=dbg_usage reason="Probe output is a deterministic committed artifact and must not contain diagnostics noise"
52# OCT-LINT: disable-file=dbg_import reason="Diagnostics would contaminate analyzer JSON and probe console output"
53
54
55BASE_SHA = "e4d56a63"
56REPORT_BASENAME = "crap-probe-2026-08"
57
58
59@dataclass(frozen=True, slots=True)
61 """One monorepo subproject and its suite/source boundaries."""
62
63 name: str
64 project_root: str
65 pytest_args: tuple[str, ...]
66 source_paths: tuple[str, ...]
67
68
69PROJECTS = (
71 name="oct",
72 project_root="oct",
73 pytest_args=("tests", "-q"),
74 source_paths=("oct/oct",),
75 ),
77 name="oc_diagnostics",
78 project_root="oc_diagnostics",
79 pytest_args=("tests", "-q"),
80 source_paths=("oc_diagnostics/oc_diagnostics",),
81 ),
83 name="oc_chat",
84 project_root="oc_chat",
85 pytest_args=("oc_chat/tests", "-q"),
86 source_paths=("oc_chat/oc_chat",),
87 ),
89 name="oc_pricing",
90 project_root="oc_pricing",
91 pytest_args=("tests", "-q"),
92 source_paths=("oc_pricing/oc_pricing",),
93 ),
95 name="option_c_agent_contracts",
96 project_root="contracts/option_c_agent_contracts",
97 pytest_args=("tests", "-q"),
98 source_paths=("contracts/option_c_agent_contracts/option_c_agent_contracts",),
99 ),
101 name="option_c_registry_contracts",
102 project_root="contracts/option_c_registry_contracts",
103 pytest_args=("tests", "-q"),
104 source_paths=(
105 "contracts/option_c_registry_contracts/option_c_registry_contracts",
106 ),
107 ),
108)
109
110
111def _parser() -> argparse.ArgumentParser:
112 """Build the probe driver's argument parser."""
113 parser = argparse.ArgumentParser(
114 description="Run the Plan 214 monorepo CRAP probe.",
115 )
116 parser.add_argument("--repo-root", required=True, type=Path)
117 parser.add_argument("--output-dir", required=True, type=Path)
118 parser.add_argument("--data-dir", required=True, type=Path)
119 parser.add_argument(
120 "--python-executable",
121 type=Path,
122 default=Path(sys.executable),
123 )
124 return parser
125
126
127def main(argv: Sequence[str] | None = None) -> int:
128 """Run every probe project and write both committed artifacts."""
129 args = _parser().parse_args(argv)
130 repo_root = args.repo_root.resolve()
131 output_dir = args.output_dir.resolve()
132 data_dir = args.data_dir.resolve()
133 python_executable = args.python_executable.resolve()
134 output_dir.mkdir(parents=True, exist_ok=True)
135 data_dir.mkdir(parents=True, exist_ok=True)
136 projects = [
138 project,
139 repo_root=repo_root,
140 data_dir=data_dir,
141 python_executable=python_executable,
142 )
143 for project in PROJECTS
144 ]
145 generated_at = dt.datetime.now(dt.timezone.utc).isoformat()
146 payload = {
147 "report_version": 1,
148 "generated_at": generated_at,
149 "base_sha": BASE_SHA,
150 "instrument_root": str(_instrument_root()),
151 "repo_root": str(repo_root),
152 "projects": projects,
153 }
154 json_path = output_dir / f"{REPORT_BASENAME}.json"
155 markdown_path = output_dir / f"{REPORT_BASENAME}.md"
156 json_path.write_text(
157 json.dumps(payload, indent=2, sort_keys=True) + "\n",
158 encoding="utf-8",
159 )
160 markdown_path.write_text(
161 _render_markdown(payload, data_dir=data_dir),
162 encoding="utf-8",
163 )
164 print(f"Wrote {markdown_path}")
165 print(f"Wrote {json_path}")
166 return 0
167
168
170 project: ProbeProject,
171 *,
172 repo_root: Path,
173 data_dir: Path,
174 python_executable: Path,
175) -> dict[str, object]:
176 """Run one suite, then map its data through the module command."""
177 print(f"Measuring {project.name}...")
178 project_root = repo_root / project.project_root
179 source_paths = tuple(repo_root / item for item in project.source_paths)
180 data_file = data_dir / f"{project.name}.coverage"
181 suite_env = os.environ.copy()
182 suite_env.pop("PYTHONPATH", None)
183 suite = run_pytest_with_coverage(
184 project_root=project_root,
185 pytest_args=project.pytest_args,
186 data_file=data_file,
187 source=source_paths,
188 python_executable=python_executable,
189 env=suite_env,
190 )
191 suite_record = _suite_record(suite)
192 base_record: dict[str, object] = {
193 "name": project.name,
194 "project_root": project.project_root,
195 "source_paths": list(project.source_paths),
196 "suite": suite_record,
197 }
198 if suite.returncode != 0:
199 reason = f"pytest exited {suite.returncode}"
200 if not suite_record["output"]:
201 suite_record["output"] = "(no pytest output captured)"
202 return {
203 **base_record,
204 "status": "UNMEASURED",
205 "reason": reason,
206 "analysis_command": None,
207 "functions": [],
208 "parse_errors": [],
209 }
210 analysis_command = (
211 str(python_executable),
212 "-m",
213 "oct.complexity",
214 *(str(item) for item in source_paths),
215 "--coverage-data",
216 str(data_file),
217 "--json",
218 )
219 analysis_env = os.environ.copy()
220 analysis_env["PYTHONPATH"] = str(_instrument_root())
221 analysis = subprocess.run(
222 analysis_command,
223 cwd=repo_root,
224 env=analysis_env,
225 capture_output=True,
226 text=True,
227 errors="replace",
228 )
229 if analysis.returncode != 0:
230 reason = f"complexity analysis exited {analysis.returncode}"
231 failure_output = _joined_output(analysis.stdout, analysis.stderr)
232 suite_record["output"] = _joined_output(
233 str(suite_record["output"]),
234 failure_output or "(no complexity output captured)",
235 )
236 return {
237 **base_record,
238 "status": "UNMEASURED",
239 "reason": reason,
240 "analysis_command": _command_text(analysis_command),
241 "functions": [],
242 "parse_errors": [],
243 }
244 try:
245 analysis_payload = json.loads(analysis.stdout)
246 except json.JSONDecodeError as exc:
247 reason = f"complexity JSON decode failed: {exc}"
248 suite_record["output"] = _joined_output(
249 str(suite_record["output"]),
250 analysis.stdout,
251 analysis.stderr,
252 )
253 return {
254 **base_record,
255 "status": "UNMEASURED",
256 "reason": reason,
257 "analysis_command": _command_text(analysis_command),
258 "functions": [],
259 "parse_errors": [],
260 }
261 functions = [
262 _relativize_row(row, repo_root)
263 for row in analysis_payload["functions"]
264 ]
265 parse_errors = [
266 _relativize_error(row, repo_root)
267 for row in analysis_payload["parse_errors"]
268 ]
269 return {
270 **base_record,
271 "status": "MEASURED",
272 "reason": None,
273 "analysis_command": _command_text(analysis_command),
274 "functions": functions,
275 "parse_errors": parse_errors,
276 }
277
278
279def _suite_record(suite: CoverageRun) -> dict[str, object]:
280 """Convert a coverage suite result to stable probe JSON."""
281 return {
282 "command": _command_text(suite.command),
283 "returncode": suite.returncode,
284 "output": suite.output,
285 "coverage_data": suite.data_file,
286 }
287
288
289def _relativize_row(row: dict[str, object], repo_root: Path) -> dict[str, object]:
290 """Replace an absolute source filename with a repository-relative path."""
291 result = dict(row)
292 result["file"] = _relative_path(str(row["file"]), repo_root)
293 return result
294
295
297 row: dict[str, object],
298 repo_root: Path,
299) -> dict[str, object]:
300 """Replace an absolute parse-error filename with a relative path."""
301 result = dict(row)
302 result["file"] = _relative_path(str(row["file"]), repo_root)
303 return result
304
305
306def _relative_path(filename: str, repo_root: Path) -> str:
307 """Return a forward-slash repository-relative path when possible."""
308 try:
309 return Path(filename).resolve().relative_to(repo_root).as_posix()
310 except ValueError:
311 return Path(filename).as_posix()
312
313
315 payload: dict[str, object],
316 *,
317 data_dir: Path,
318) -> str:
319 """Render the human probe report from the same payload as JSON."""
320 lines = [
321 "# CRAP probe - August 2026",
322 "",
323 "This report is a measurement snapshot, not a gate. It presents data only and makes no recommendations.",
324 "",
325 f"- BASE_SHA: `{payload['base_sha']}`",
326 f"- Generated (UTC): `{payload['generated_at']}`",
327 f"- MAIN source tree: `{payload['repo_root']}`",
328 f"- Instrument worktree path: `{payload['instrument_root']}`",
329 "",
330 "## Reproduction",
331 "",
332 "The probe was launched from the MAIN tree with the worktree instrument on `PYTHONPATH`:",
333 "",
334 "```powershell",
335 f"$env:PYTHONPATH='{payload['instrument_root']}'",
336 _probe_command(payload, data_dir),
337 "```",
338 "",
339 "For each measured project, the exact coverage and analysis commands actually executed are recorded below.",
340 "",
341 ]
342 for project in payload["projects"]:
343 lines.extend(_render_project(project))
344 return "\n".join(lines).rstrip() + "\n"
345
346
347def _probe_command(payload: dict[str, object], data_dir: Path) -> str:
348 """Return the exact top-level probe command for the report."""
349 output_dir = Path(__file__).resolve().parents[3] / "oc_chat" / "oc_chat" / "docs" / "reports"
350 command = (
351 sys.executable,
352 "-m",
353 "oct.complexity.probe",
354 "--repo-root",
355 str(payload["repo_root"]),
356 "--output-dir",
357 str(output_dir),
358 "--data-dir",
359 str(data_dir),
360 "--python-executable",
361 sys.executable,
362 )
363 return f"& {_command_text(command)}"
364
365
366def _render_project(project: dict[str, object]) -> list[str]:
367 """Render one measured or honestly unmeasured project section."""
368 suite = project["suite"]
369 lines = [
370 f"## {project['name']}",
371 "",
372 f"Status: **{project['status']}**",
373 "",
374 f"- Coverage command: `{suite['command']}`",
375 f"- Suite return code: `{suite['returncode']}`",
376 ]
377 if project["analysis_command"]:
378 lines.append(f"- Analysis command: `{project['analysis_command']}`")
379 lines.append("")
380 if project["status"] == "UNMEASURED":
381 lines.extend(
382 [
383 f"Reason: {project['reason']}",
384 "",
385 "```text",
386 str(suite["output"]).rstrip(),
387 "```",
388 "",
389 ]
390 )
391 return lines
392 functions = project["functions"]
393 lines.extend(
394 [
395 f"Measured functions: {len(functions)}. Worst 25 shown.",
396 "",
397 "| Function | CC | Coverage | CRAP | Status | Span |",
398 "|---|---:|---:|---:|---|---|",
399 ]
400 )
401 for row in functions[:25]:
402 path = str(row["file"])
403 span = f"{path}:{row['lineno']}-{row['end_lineno']}"
404 link = f"../../../../{path}#L{row['lineno']}-L{row['end_lineno']}"
405 lines.append(
406 f"| {_escape_cell(row['qualname'])} | {row['complexity']} | "
407 f"{row['coverage'] * 100:.1f}% | {row['crap']:.2f} | "
408 f"{row['coverage_status']} | [{span}]({link}) |"
409 )
410 lines.append("")
411 parse_errors = project["parse_errors"]
412 if parse_errors:
413 lines.append("Parse errors:")
414 lines.append("")
415 for error in parse_errors:
416 lines.append(f"- `{error['file']}`: {error['error']}")
417 lines.append("")
418 else:
419 lines.extend(["Parse errors: none.", ""])
420 return lines
421
422
423def _escape_cell(value: object) -> str:
424 """Escape Markdown table separators in one cell value."""
425 return str(value).replace("|", "\\|")
426
427
428def _instrument_root() -> Path:
429 """Return the worktree oct project directory used for PYTHONPATH."""
430 return Path(__file__).resolve().parents[2]
431
432
433def _command_text(command: Sequence[str]) -> str:
434 """Return a Windows-rerunnable rendering of an argument sequence."""
435 return subprocess.list2cmdline([str(item) for item in command])
436
437
438def _joined_output(*parts: str) -> str:
439 """Join captured output fragments without silently dropping any."""
440 return "\n".join(part.rstrip() for part in parts if part).strip()
441
442
443if __name__ == "__main__":
444 raise SystemExit(main())
str _command_text(Sequence[str] command)
Definition probe.py:433
int main(Sequence[str]|None argv=None)
Definition probe.py:127
dict[str, object] _suite_record(CoverageRun suite)
Definition probe.py:279
dict[str, object] _run_project(ProbeProject project, *, Path repo_root, Path data_dir, Path python_executable)
Definition probe.py:175
str _joined_output(*str parts)
Definition probe.py:438
str _relative_path(str filename, Path repo_root)
Definition probe.py:306
str _escape_cell(object value)
Definition probe.py:423
argparse.ArgumentParser _parser()
Definition probe.py:111
str _probe_command(dict[str, object] payload, Path data_dir)
Definition probe.py:347
Path _instrument_root()
Definition probe.py:428
dict[str, object] _relativize_error(dict[str, object] row, Path repo_root)
Definition probe.py:299
list[str] _render_project(dict[str, object] project)
Definition probe.py:366
dict[str, object] _relativize_row(dict[str, object] row, Path repo_root)
Definition probe.py:289
str _render_markdown(dict[str, object] payload, *, Path data_dir)
Definition probe.py:318