Private
Public Access
0
0
Files
manual_slop/scripts/audit_gui2_imports.py
ed 6f9a3af201 feat(audit): add main-thread import graph audit + baseline measurements
Phase 1, Tasks T1.2 + T1.4 of the startup_speedup_20260606 track.

NEW: scripts/audit_main_thread_imports.py
  Static CI gate that AST-walks the import graph reachable from
  sloppy.py and fails (exit 1) if any heavy module is imported at the
  top of a main-thread-reachable file. Walks into if/elif/else and
  try/except branches (which run at import time) but skips function
  bodies (which only run when called). Allowlist: stdlib + the lean
  gui_2 skeleton (imgui_bundle, defer, src.imgui_scopes, src.theme_2,
  src.theme_models, src.paths, src.models, src.events).

NEW: scripts/audit_gui2_imports.py
  Read-only analysis tool that lists every top-level and function-level
  import in src/gui_2.py, classified by location. Used in Phase 5D to
  identify which imports to remove.

NEW: tests/test_audit_main_thread_imports.py
  9 tests covering: --help exits 0, clean stdlib-only passes, heavy
  third-party fails, google.genai fails, transitive walks, function-
  body imports ignored, if-branch imports flagged, try-block imports
  flagged, file:line reported. All 9 pass.

NEW: docs/reports/startup_baseline_20260606.txt
  3-run median cold-start benchmark. Worst offenders: src.gui_2
  (1770ms), simulation.user_agent (1517ms), google.genai (1001ms),
  openai (482ms), anthropic (441ms), imgui_bundle (255ms),
  src.theme_nerv* (485ms combined), src.markdown_table (243ms),
  src.command_palette (242ms).

NEW: docs/reports/startup_audit_20260606.txt
  Audit output on the CURRENT codebase. Reports 67 violations across
  the main-thread import graph (incl. numpy in src/gui_2.py:9,
  tomli_w in src/gui_2.py:18, fastapi + requests in src/app_controller,
  tree_sitter_* in src/file_cache, pydantic in src/models, plus all
  the src.* subsystem imports that drag in heavy transitive deps).
  Phase 3-5 of the track will resolve these one by one.

After Phase 3-5, this audit must exit 0 (no violations).

Co-located reports in docs/reports/ per project convention; the other
agent finished their work in docs/superpowers/ and is unrelated.
2026-06-06 14:22:18 -04:00

115 lines
3.6 KiB
Python

#!/usr/bin/env python
"""
Audit top-level imports in src/gui_2.py and classify them.
For each top-level `import X` or `from X import Y` statement in gui_2.py,
report:
- file:line
- the imported module
- whether it's at module level (always loaded on main thread) or inside
a function (potentially feature-gated)
This is a static analysis tool for the startup_speedup_20260606 track.
The output is meant to be read by a human who knows which functions
are first-frame vs feature-gated.
Output format (text):
MODULE-LEVEL imports (these run on the main thread's import chain):
src/gui_2.py:1: import imgui_bundle
src/gui_2.py:15: from src.app_controller import AppController
...
FUNCTION-LEVEL imports (potentially feature-gated; candidates for _require_warmed):
src/gui_2.py:42 (inside _render_command_palette): from src.command_palette import ...
...
"""
import ast
import sys
from pathlib import Path
from typing import Iterable
def classify_imports(source: str) -> tuple[list[tuple[int, str, str]], list[tuple[int, str, str, str]]]:
"""Parse a Python source and return (module_level, function_level) imports.
Each entry is (line, imported_name, full_statement).
"""
tree = ast.parse(source)
module_level: list[tuple[int, str, str]] = []
function_level: list[tuple[int, str, str, str]] = []
def imported_names(node: ast.stmt) -> list[str]:
if isinstance(node, ast.Import):
return [alias.name for alias in node.names]
if isinstance(node, ast.ImportFrom):
if not node.module or node.level != 0:
return []
return [node.module]
return []
for node in tree.body:
names = imported_names(node)
if not names:
continue
for name in names:
stmt = ast.unparse(node).strip().replace("\n", " ")
module_level.append((node.lineno, name, stmt))
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
for child in node.body:
names = imported_names(child)
if not names:
continue
for name in names:
stmt = ast.unparse(child).strip().replace("\n", " ")
function_level.append((child.lineno, node.name, name, stmt))
return module_level, function_level
def render_report(source_path: Path) -> str:
source = source_path.read_text(encoding="utf-8", errors="replace")
module_level, function_level = classify_imports(source)
lines: list[str] = []
lines.append(f"Audit of {source_path}")
lines.append("=" * 80)
lines.append("")
lines.append(f"MODULE-LEVEL imports: {len(module_level)} (these run on the main thread's import chain)")
lines.append("-" * 80)
for lineno, name, stmt in module_level:
lines.append(f" L{lineno:>5} {name:<40} {stmt[:60]}")
lines.append("")
lines.append(f"FUNCTION-LEVEL imports: {len(function_level)} (potentially feature-gated)")
lines.append("-" * 80)
if function_level:
by_function: dict[str, list[tuple[int, str, str]]] = {}
for lineno, fname, name, stmt in function_level:
by_function.setdefault(fname, []).append((lineno, name, stmt))
for fname in sorted(by_function):
entries = by_function[fname]
lines.append(f" {fname} ({len(entries)} imports)")
for lineno, name, stmt in entries:
lines.append(f" L{lineno:>5} {name:<40} {stmt[:60]}")
else:
lines.append(" (none)")
lines.append("")
return "\n".join(lines)
def main(argv: list[str]) -> int:
if len(argv) < 2:
print("usage: audit_gui2_imports.py <path-to-gui_2.py>", file=sys.stderr)
return 2
path = Path(argv[1])
if not path.exists():
print(f"file not found: {path}", file=sys.stderr)
return 2
print(render_report(path))
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))