scripts.ast_report
Reference › scripts › ast_report
Produces an advisory AST health report: one Markdown digest of the codebase built from the AST toolkit's analyses. A human or a CI step runs it, and main always returns 0 because the report is advisory.
Invocation: --source takes a comma-separated list of directories or files (a default is built in), and --output writes the Markdown to a file instead of standard output. build_report loads the toolkit with _load_tool, runs its checks over the sources and assembles sections with _section and _item_line, each giving a count line followed by a limited list of rows. It reads source only and writes only the optional output file.
At a glance
| Source | scripts/ast_report.py |
| Lines of code | 210 |
| Classes | 0 |
| Functions | 6 |
| Imports (internal) | none |
| Imported by | none |
| Tests that import it | none |
Class diagram
No classes; see Functions.
Dependency diagram
No internal imports and no importers.
API
Functions
| Function | Signature | Summary |
|---|---|---|
_load_tool |
_load_tool() |
Load the AST toolkit module by path (same pattern as ast_api.py). |
_sources |
_sources(source: str) -> list[str] |
Split a comma-separated --source value into a clean list. |
_item_line |
_item_line(row: dict) -> str |
Render one finding as - file:line name (name optional). |
_section |
_section(heading: str, count_line: str, rows: list[dict], limit: int) -> list[str] |
Build a ## heading section: a one-line count plus the top items. |
build_report |
build_report(sources: list[str]) -> str |
Build the full markdown advisory health report for sources. |
main |
main(argv: list[str] | None = None) -> int |
CLI entry point. |
Source
Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/scripts/ast_report.py
"""Advisory AST health report — a single markdown digest of the codebase.
Aggregates the read-only analyses from ``scripts/serialize_codebase_ast.py``
(cyclomatic complexity, nesting depth, long parameter lists, unused symbols,
the logging/exception audit, and untraced public defs) into one markdown
document.
It is purely advisory: it never fails. The process always exits 0 regardless
of how many findings surface, so it can run as a non-blocking CI step, a final
pipeline stage, or a manual pre-commit hook without ever gating a commit.
Run:
uv run python scripts/ast_report.py # report to stdout
uv run python scripts/ast_report.py --output ast-health.md
uv run python scripts/ast_report.py --source src/pkg,tests
"""
from __future__ import annotations
import argparse
import importlib.util
import sys
from collections import Counter
from pathlib import Path
_TOOL_PATH = Path(__file__).resolve().parent / "serialize_codebase_ast.py"
_DEFAULT_SOURCE = "src/orchestrata_ultimate"
# Per-section thresholds and how many top items to surface.
_COMPLEXITY_THRESHOLD = 10
_NESTING_THRESHOLD = 4
_LONG_PARAM_THRESHOLD = 5
_TOP_N = 15
def _load_tool():
"""Load the AST toolkit module by path (same pattern as ``ast_api.py``)."""
spec = importlib.util.spec_from_file_location("serialize_codebase_ast", _TOOL_PATH)
if spec is None or spec.loader is None:
raise ImportError(f"cannot load AST toolkit from {_TOOL_PATH}")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def _sources(source: str) -> list[str]:
"""Split a comma-separated ``--source`` value into a clean list."""
return [s.strip() for s in source.split(",") if s.strip()]
def _item_line(row: dict) -> str:
"""Render one finding as ``- file:line name`` (name optional)."""
location = f"{row.get('file', '?')}:{row.get('line', '?')}"
name = row.get("name")
return f"- {location} {name}" if name else f"- {location}"
def _section(heading: str, count_line: str, rows: list[dict], limit: int) -> list[str]:
"""Build a ``## heading`` section: a one-line count plus the top items."""
lines = [f"## {heading}", "", count_line]
if rows:
lines.append("")
lines.extend(_item_line(r) for r in rows[:limit])
lines.append("")
return lines
def build_report(sources: list[str]) -> str:
"""Build the full markdown advisory health report for *sources*.
*sources* is a list of directories/files (e.g. ``["src/orchestrata_ultimate"]``).
Returns the report as a markdown string; never raises for "findings", only
for genuinely unreadable inputs.
"""
tool = _load_tool()
complexity = tool.measure_complexity(sources, threshold=_COMPLEXITY_THRESHOLD)
nesting = tool.measure_nesting(sources, threshold=_NESTING_THRESHOLD)
long_params = tool.find_long_param_lists(sources, threshold=_LONG_PARAM_THRESHOLD)
unused = tool.find_unused(sources)
audit = tool.audit_error_handling(sources)
untraced = tool.find_untraced(sources)
out: list[str] = [
"# AST Health Report (advisory)",
"",
f"Sources: {', '.join(sources)}",
"",
"Advisory only — findings never block a commit, merge, or pipeline.",
"",
]
out += _section(
"Complexity",
f"{len(complexity)} function(s) with cyclomatic complexity "
f">= {_COMPLEXITY_THRESHOLD} (top {_TOP_N}).",
[{**r, "name": f"{r['name']} (cc={r['complexity']})"} for r in complexity],
_TOP_N,
)
out += _section(
"Nesting",
f"{len(nesting)} function(s) with nesting depth "
f">= {_NESTING_THRESHOLD} (top {_TOP_N}).",
[{**r, "name": f"{r['name']} (depth={r['depth']})"} for r in nesting],
_TOP_N,
)
out += _section(
"Long-params",
f"{len(long_params)} function(s) with more than "
f"{_LONG_PARAM_THRESHOLD} parameters (top {_TOP_N}).",
[{**r, "name": f"{r['name']} ({r['params']} params)"} for r in long_params],
_TOP_N,
)
out += _section(
"Unused",
f"{len(unused)} module-level def(s) never referenced "
"(precision-filtered — review before deleting).",
unused,
len(unused),
)
severities = Counter(f.get("severity", "info") for f in audit)
audit_rows = [
{**f, "name": f"[{f.get('rule', '?')}] {f.get('detail', '')}"} for f in audit
]
out += _section(
"Audit",
f"{len(audit)} error-handling/logging finding(s): "
f"{severities.get('error', 0)} error, {severities.get('warning', 0)} warning "
f"(top {_TOP_N}).",
audit_rows,
_TOP_N,
)
out += _section(
"Untraced",
f"{len(untraced)} public def(s) lacking an @traces decorator (top {_TOP_N}).",
untraced,
_TOP_N,
)
cycles = tool.find_import_cycles(sources)
out += ["## Import cycles", "", f"{len(cycles)} first-party import cycle(s)."]
if cycles:
out.append("")
out += [f"- {' -> '.join(cycle)}" for cycle in cycles[:_TOP_N]]
out.append("")
dead = tool.find_dead_params(sources)
out += _section(
"Dead params",
f"{len(dead)} function(s) with unused parameter(s) "
f"(interface/abstract declarations excluded; top {_TOP_N}).",
[{**r, "name": f"{r['name']} ({', '.join(r['params'])})"} for r in dead],
_TOP_N,
)
untyped = tool.find_untyped(sources)
out += _section(
"Untyped",
f"{len(untyped)} function(s) missing type hints "
f"(precondition for type-resolved refactors; top {_TOP_N}).",
[
{
**r,
"name": r["name"]
+ (f" (params: {', '.join(r['params'])})" if r["params"] else "")
+ (" (return)" if r["return"] else ""),
}
for r in untyped
],
_TOP_N,
)
return "\n".join(out).rstrip() + "\n"
def main(argv: list[str] | None = None) -> int:
"""CLI entry point. Always returns 0 — the report is advisory."""
parser = argparse.ArgumentParser(
description="Advisory AST health report (markdown). Always exits 0."
)
parser.add_argument(
"--source",
default=_DEFAULT_SOURCE,
help="Comma-separated dirs/files to analyze (default: %(default)s).",
)
parser.add_argument(
"--output",
default=None,
help="Write the markdown report here (default: stdout).",
)
args = parser.parse_args(argv)
report = build_report(_sources(args.source))
if args.output:
Path(args.output).write_text(report, encoding="utf-8")
else:
sys.stdout.write(report)
return 0
if __name__ == "__main__":
raise SystemExit(main())