orchestrata_ultimate.tools.ast_query_cli
Reference › orchestrata_ultimate › tools › ast_query_cli
This module is the ast-query command line over ast_query, built with typer. No other module imports it, and tests/tools/test_ast_query_cli.py covers it.
The commands documented in its module docstring are summary, stubs, find, search, modules, largest, connected, inheritance, protocols, abstract, pydantic, dataclasses, enums, diff, unmapped, coverage and stats. Shared options select the snapshot path (default plans/ast-ultimate.json), an optional comparison file and JSON output. _get_ast builds the ASTQuery, _warn_if_stale prints a one-line staleness warning to standard error when the source tree is newer than the snapshot, and _output and _print_dict print results as JSON or as indented text. main runs the typer app, which prints help when called with no arguments.
At a glance
| Source | src/orchestrata_ultimate/tools/ast_query_cli.py |
| Lines of code | 464 |
| Classes | 0 |
| Functions | 25 |
| Imports (internal) | orchestrata_ultimate.tools.ast_query |
| Imported by | none |
| Tests that import it | tests/tools/test_ast_query_cli.py |
Class diagram
No classes; see Functions.
Dependency diagram
API
Functions
| Function | Signature | Summary |
|---|---|---|
_get_ast |
_get_ast(ast_path: str, compare_path: str | None) -> ASTQuery |
|
_warn_if_stale |
_warn_if_stale(ast: ASTQuery) -> None |
Print a one-line staleness WARNING to stderr if the snapshot is stale. |
_output |
_output(data: object, as_json: bool = False) -> None |
|
_print_dict |
_print_dict(d: dict, indent: int = 0) -> None |
|
summary app.command |
summary(ast_path: str = _ast_path_opt, json_output: bool = _json_opt) -> None |
Print top-level project summary. |
modules app.command |
modules(ast_path: str = _ast_path_opt, json_output: bool = _json_opt) -> None |
List all modules with LOC, class, and function counts. |
stubs app.command |
stubs(ast_path: str = _ast_path_opt, compare_path: str | None = _compare_path_opt, min_methods: int = typer.Option(3, '--min-methods', help='Minimum methods in s…, max_ratio: float = typer.Option(0.3, '--max-ratio', help='Maximum method ratio…, json_output: bool = _json_opt) -> None |
List stub classes (<30% methods ported). |
find app.command |
find(name: str = typer.Argument(..., help='Class name to find'), ast_path: str = _ast_path_opt, compare_path: str | None = _compare_path_opt, json_output: bool = _json_opt) -> None |
Find a class by name. |
search app.command |
search(pattern: str = typer.Argument(..., help='Regex pattern to search class nam…, ast_path: str = _ast_path_opt, case_sensitive: bool = typer.Option(False, '--case', help='Case-sensitive match'), json_output: bool = _json_opt) -> None |
Search classes by regex pattern. |
largest app.command |
largest(n: int = typer.Argument(20, help='Number of classes to show'), top: int | None = typer.Option(None, '--top', help='Number of classes to show…, ast_path: str = _ast_path_opt, json_output: bool = _json_opt) -> None |
Show largest classes by method count. |
connected app.command |
connected(n: int = typer.Argument(20, help='Number of classes to show'), top: int | None = typer.Option(None, '--top', help='Number of classes to show…, ast_path: str = _ast_path_opt, json_output: bool = _json_opt) -> None |
Show classes with the most subclasses. |
inheritance app.command |
inheritance(class_name: str = typer.Argument(..., help='Class name to trace'), ast_path: str = _ast_path_opt, json_output: bool = _json_opt) -> None |
Show inheritance tree for a class. |
protocols app.command |
protocols(ast_path: str = _ast_path_opt, json_output: bool = _json_opt) -> None |
List all Protocol classes. |
abstract app.command |
abstract(ast_path: str = _ast_path_opt, json_output: bool = _json_opt) -> None |
List all abstract classes. |
pydantic app.command |
pydantic(ast_path: str = _ast_path_opt, json_output: bool = _json_opt) -> None |
List all Pydantic BaseModel classes. |
dataclasses app.command |
dataclasses(ast_path: str = _ast_path_opt, json_output: bool = _json_opt) -> None |
List all dataclass classes. |
enums app.command |
enums(ast_path: str = _ast_path_opt, json_output: bool = _json_opt) -> None |
List all Enum classes. |
interfaces app.command |
interfaces(ast_path: str = _ast_path_opt, json_output: bool = _json_opt) -> None |
List all Interface declarations (JS/TS). |
type_aliases app.command |
type_aliases(ast_path: str = _ast_path_opt, json_output: bool = _json_opt) -> None |
List all Type alias declarations (JS/TS). |
exported app.command |
exported(ast_path: str = _ast_path_opt, json_output: bool = _json_opt) -> None |
List all exported classes/interfaces (JS/TS). |
diff app.command |
diff(class_name: str = typer.Argument(..., help='Class name to diff'), ast_path: str = _ast_path_opt, compare_path: str = typer.Option(..., '--compare', help='Path to deep-compariso…, json_output: bool = _json_opt) -> None |
Show method-level diff for a class (requires --compare). |
unmapped app.command |
unmapped(ast_path: str = _ast_path_opt, compare_path: str = typer.Option(..., '--compare', help='Path to deep-compariso…, json_output: bool = _json_opt) -> None |
List unported orchestrata classes and ultimate-only classes (requires --compare). |
coverage app.command |
coverage(ast_path: str = _ast_path_opt, compare_path: str = typer.Option(..., '--compare', help='Path to deep-compariso…, json_output: bool = _json_opt) -> None |
Show porting coverage by tier (requires --compare). |
stats app.command |
stats(ast_path: str = _ast_path_opt, json_output: bool = _json_opt) -> None |
Show internal index statistics. |
main |
main() -> None |
Source
"""
CLI for the AST Query Engine.
Usage::
ast-query summary
ast-query stubs
ast-query find BaseAgent
ast-query search 'Agent$'
ast-query modules
ast-query largest 10
ast-query largest --top 10
ast-query connected
ast-query inheritance BaseAgent
ast-query protocols
ast-query abstract
ast-query pydantic
ast-query dataclasses
ast-query enums
ast-query diff BaseAgent
ast-query unmapped
ast-query coverage
ast-query stats
"""
from __future__ import annotations
import json
import sys
import typer
from orchestrata_ultimate.tools.ast_query import ASTQuery
app = typer.Typer(
name="ast-query",
help="Query the serialized AST JSON for orchestrata-ultimate.",
no_args_is_help=True,
)
# ── shared options ────────────────────────────────────────────────────
_ast_path_opt = typer.Option(
"plans/ast-ultimate.json",
"--ast",
help="Path to ultimate AST JSON",
)
_compare_path_opt = typer.Option(
None,
"--compare",
help="Path to deep-comparison JSON (enables diff/coverage/stubs-from-source)",
)
_json_opt = typer.Option(False, "--json", help="Output as JSON")
def _get_ast(ast_path: str, compare_path: str | None) -> ASTQuery:
return ASTQuery(ast_path=ast_path, compare_path=compare_path)
def _warn_if_stale(ast: ASTQuery) -> None:
"""Print a one-line staleness WARNING to stderr if the snapshot is stale.
The snapshot is stale when a ``.py`` file under its recorded source root
has been modified since it was serialized. This only ever writes to
stderr — stdout output (parsed by consumers) is never touched.
"""
msg = ast.staleness_warning()
if msg:
print(msg, file=sys.stderr)
def _output(data: object, as_json: bool = False) -> None:
if as_json:
# Convert ClassInfo objects to dicts
print(
json.dumps(
data,
indent=2,
default=lambda o: o.__dict__ if hasattr(o, "__dict__") else str(o),
)
)
elif isinstance(data, list):
for item in data:
if isinstance(item, dict):
_print_dict(item)
else:
print(item)
elif isinstance(data, dict):
_print_dict(data)
else:
print(data)
def _print_dict(d: dict, indent: int = 0) -> None:
prefix = " " * indent
for k, v in d.items():
if isinstance(v, dict):
print(f"{prefix}{k}:")
_print_dict(v, indent + 1)
elif isinstance(v, list) and len(v) > 10:
print(f"{prefix}{k}: [{len(v)} items]")
for item in v[:5]:
print(f"{prefix} - {item}")
print(f"{prefix} ... and {len(v) - 5} more")
elif isinstance(v, list):
print(f"{prefix}{k}: {v}")
else:
print(f"{prefix}{k}: {v}")
# ── commands ──────────────────────────────────────────────────────────
@app.command()
def summary(
ast_path: str = _ast_path_opt,
json_output: bool = _json_opt,
) -> None:
"""Print top-level project summary."""
ast = _get_ast(ast_path, None)
_warn_if_stale(ast)
_output(ast.summary(), json_output)
@app.command()
def modules(
ast_path: str = _ast_path_opt,
json_output: bool = _json_opt,
) -> None:
"""List all modules with LOC, class, and function counts."""
ast = _get_ast(ast_path, None)
_warn_if_stale(ast)
_output(ast.module_summary(), json_output)
@app.command()
def stubs(
ast_path: str = _ast_path_opt,
compare_path: str | None = _compare_path_opt,
min_methods: int = typer.Option(
3, "--min-methods", help="Minimum methods in source to consider"
),
max_ratio: float = typer.Option(
0.3, "--max-ratio", help="Maximum method ratio to flag as stub"
),
json_output: bool = _json_opt,
) -> None:
"""List stub classes (<30% methods ported)."""
ast = _get_ast(ast_path, compare_path)
result = ast.stubs(min_methods_in_source=min_methods, max_ratio=max_ratio)
if not result:
print("No stubs found.")
return
_output(result, json_output)
@app.command()
def find(
name: str = typer.Argument(..., help="Class name to find"),
ast_path: str = _ast_path_opt,
compare_path: str | None = _compare_path_opt,
json_output: bool = _json_opt,
) -> None:
"""Find a class by name."""
ast = _get_ast(ast_path, compare_path)
result = ast.find_class(name)
if result is None:
print(f"Class '{name}' not found.")
raise typer.Exit(1)
_output(result, json_output)
@app.command()
def search(
pattern: str = typer.Argument(..., help="Regex pattern to search class names"),
ast_path: str = _ast_path_opt,
case_sensitive: bool = typer.Option(False, "--case", help="Case-sensitive match"),
json_output: bool = _json_opt,
) -> None:
"""Search classes by regex pattern."""
ast = _get_ast(ast_path, None)
results = ast.search_classes(pattern, case_sensitive=case_sensitive)
if not results:
print(f"No classes matching '{pattern}'.")
return
for ci in results:
print(f" {ci.name:40s} {ci.module_path:50s} {ci.method_count} methods")
if json_output:
_output([ci.__dict__ for ci in results], True)
@app.command()
def largest(
n: int = typer.Argument(20, help="Number of classes to show"),
top: int | None = typer.Option(
None, "--top", help="Number of classes to show (overrides positional N)"
),
ast_path: str = _ast_path_opt,
json_output: bool = _json_opt,
) -> None:
"""Show largest classes by method count."""
count = top if top is not None else n
ast = _get_ast(ast_path, None)
results = ast.largest_classes(count)
for r in results:
print(
f" {r['name']:40s} {r['module']:50s} {r['methods']} methods (line {r.get('line', '?')})"
)
if json_output:
_output(results, True)
@app.command()
def connected(
n: int = typer.Argument(20, help="Number of classes to show"),
top: int | None = typer.Option(
None, "--top", help="Number of classes to show (overrides positional N)"
),
ast_path: str = _ast_path_opt,
json_output: bool = _json_opt,
) -> None:
"""Show classes with the most subclasses."""
count = top if top is not None else n
ast = _get_ast(ast_path, None)
results = ast.most_connected_classes(count)
for r in results:
print(f" {r['class']:60s} {r['subclasses']} subclasses")
if json_output:
_output(results, True)
@app.command()
def inheritance(
class_name: str = typer.Argument(..., help="Class name to trace"),
ast_path: str = _ast_path_opt,
json_output: bool = _json_opt,
) -> None:
"""Show inheritance tree for a class."""
ast = _get_ast(ast_path, None)
result = ast.inheritance_tree(class_name)
if result is None:
print(f"Class '{class_name}' not found in inheritance graph.")
raise typer.Exit(1)
_output(result, json_output)
@app.command()
def protocols(
ast_path: str = _ast_path_opt,
json_output: bool = _json_opt,
) -> None:
"""List all Protocol classes."""
ast = _get_ast(ast_path, None)
results = ast.protocols()
for ci in results:
print(f" {ci.name:40s} {ci.module_path}")
print(f"\n Total: {len(results)} Protocol classes")
if json_output:
_output([ci.__dict__ for ci in results], True)
@app.command()
def abstract(
ast_path: str = _ast_path_opt,
json_output: bool = _json_opt,
) -> None:
"""List all abstract classes."""
ast = _get_ast(ast_path, None)
results = ast.abstract_classes()
for ci in results:
bases = ", ".join(ci.base_classes[:3]) if ci.base_classes else "none"
print(f" {ci.name:40s} {ci.module_path:50s} extends: {bases}")
print(f"\n Total: {len(results)} abstract classes")
if json_output:
_output([ci.__dict__ for ci in results], True)
@app.command()
def pydantic(
ast_path: str = _ast_path_opt,
json_output: bool = _json_opt,
) -> None:
"""List all Pydantic BaseModel classes."""
ast = _get_ast(ast_path, None)
results = ast.pydantic_models()
for ci in results:
print(f" {ci.name:40s} {ci.module_path}")
print(f"\n Total: {len(results)} Pydantic models")
if json_output:
_output([ci.__dict__ for ci in results], True)
@app.command()
def dataclasses(
ast_path: str = _ast_path_opt,
json_output: bool = _json_opt,
) -> None:
"""List all dataclass classes."""
ast = _get_ast(ast_path, None)
results = ast.dataclasses()
for ci in results:
print(f" {ci.name:40s} {ci.module_path}")
print(f"\n Total: {len(results)} dataclasses")
if json_output:
_output([ci.__dict__ for ci in results], True)
@app.command()
def enums(
ast_path: str = _ast_path_opt,
json_output: bool = _json_opt,
) -> None:
"""List all Enum classes."""
ast = _get_ast(ast_path, None)
results = ast.enums()
for ci in results:
print(f" {ci.name:40s} {ci.module_path}")
print(f"\n Total: {len(results)} Enum classes")
if json_output:
_output([ci.__dict__ for ci in results], True)
@app.command()
def interfaces(
ast_path: str = _ast_path_opt,
json_output: bool = _json_opt,
) -> None:
"""List all Interface declarations (JS/TS)."""
ast = _get_ast(ast_path, None)
results = ast.interfaces()
for ci in results:
print(f" {ci.name:40s} {ci.module_path}")
print(f"\n Total: {len(results)} interfaces")
if json_output:
_output([ci.__dict__ for ci in results], True)
@app.command()
def type_aliases(
ast_path: str = _ast_path_opt,
json_output: bool = _json_opt,
) -> None:
"""List all Type alias declarations (JS/TS)."""
ast = _get_ast(ast_path, None)
results = ast.type_aliases()
for ci in results:
print(f" {ci.name:40s} {ci.module_path}")
print(f"\n Total: {len(results)} type aliases")
if json_output:
_output([ci.__dict__ for ci in results], True)
@app.command()
def exported(
ast_path: str = _ast_path_opt,
json_output: bool = _json_opt,
) -> None:
"""List all exported classes/interfaces (JS/TS)."""
ast = _get_ast(ast_path, None)
results = ast.exported_classes()
for ci in results:
print(f" {ci.name:40s} {ci.module_path}")
print(f"\n Total: {len(results)} exported declarations")
if json_output:
_output([ci.__dict__ for ci in results], True)
@app.command()
def diff(
class_name: str = typer.Argument(..., help="Class name to diff"),
ast_path: str = _ast_path_opt,
compare_path: str = typer.Option(
..., "--compare", help="Path to deep-comparison JSON (required)"
),
json_output: bool = _json_opt,
) -> None:
"""Show method-level diff for a class (requires --compare)."""
ast = _get_ast(ast_path, compare_path)
result = ast.method_diff(class_name)
if result is None:
print(f"No comparison data for '{class_name}'.")
raise typer.Exit(1)
_output(result, json_output)
@app.command()
def unmapped(
ast_path: str = _ast_path_opt,
compare_path: str = typer.Option(
..., "--compare", help="Path to deep-comparison JSON (required)"
),
json_output: bool = _json_opt,
) -> None:
"""List unported orchestrata classes and ultimate-only classes (requires --compare)."""
ast = _get_ast(ast_path, compare_path)
result = ast.unmapped_classes()
if not result:
print("No comparison data available.")
return
orchestrata_only = result.get("orchestrata_only", [])
ultimate_only = result.get("ultimate_only", [])
print(f"Orchestrata-only (not yet ported): {len(orchestrata_only)}")
for c in orchestrata_only[:30]:
print(f" - {c}")
if len(orchestrata_only) > 30:
print(f" ... and {len(orchestrata_only) - 30} more")
print(f"\nUltimate-only (new additions): {len(ultimate_only)}")
for c in ultimate_only[:30]:
print(f" - {c}")
if len(ultimate_only) > 30:
print(f" ... and {len(ultimate_only) - 30} more")
if json_output:
_output(result, True)
@app.command()
def coverage(
ast_path: str = _ast_path_opt,
compare_path: str = typer.Option(
..., "--compare", help="Path to deep-comparison JSON (required)"
),
json_output: bool = _json_opt,
) -> None:
"""Show porting coverage by tier (requires --compare)."""
ast = _get_ast(ast_path, compare_path)
result = ast.coverage_by_tier()
if not result:
print("No tier coverage data. Does the comparison file contain 'tier_summary'?")
return
print(
f"{'Tier':<6} {'Packages':<10} {'Orch LOC':<12} {'Ult LOC':<12} {'LOC %':<8} {'Class %':<8}"
)
print("-" * 58)
for tier, data in sorted(result.items()):
print(
f" {tier:<4} {data.get('packages', 0):<8} "
f"{data.get('orchestrata_loc', 0):<10} "
f"{data.get('ultimate_loc', 0):<10} "
f"{data.get('coverage', 0):<6.1f} "
f"{data.get('class_coverage', 0):<6.1f}"
)
if json_output:
_output(result, True)
@app.command()
def stats(
ast_path: str = _ast_path_opt,
json_output: bool = _json_opt,
) -> None:
"""Show internal index statistics."""
ast = _get_ast(ast_path, None)
_warn_if_stale(ast)
_output(ast.index_stats(), json_output)
def main() -> None:
app()
if __name__ == "__main__":
main()