Skip to content

Workspace

workspace

Workspace detection and multi-package analysis.

Detects uv workspaces via [tool.uv.workspace] in pyproject.toml and aggregates PackageInfo across all member packages.

analyze_workspace(path)

Analyze all packages in a uv workspace.

Discovers workspace members, analyzes each with analyze_package(), and builds inter-package dependency edges.

Parameters:

Name Type Description Default
path Path

Path to workspace root.

required

Returns:

Type Description
WorkspaceInfo

WorkspaceInfo with all packages and dependency edges.

Raises:

Type Description
ValueError

If path is not a workspace root.

Example

ws = analyze_workspace(Path("/path/to/workspace")) len(ws.packages) > 0 True

Source code in packages/axm-ast/src/axm_ast/core/workspace.py
def analyze_workspace(path: Path) -> WorkspaceInfo:
    """Analyze all packages in a uv workspace.

    Discovers workspace members, analyzes each with ``analyze_package()``,
    and builds inter-package dependency edges.

    Args:
        path: Path to workspace root.

    Returns:
        WorkspaceInfo with all packages and dependency edges.

    Raises:
        ValueError: If path is not a workspace root.

    Example:
        >>> ws = analyze_workspace(Path("/path/to/workspace"))
        >>> len(ws.packages) > 0
        True
    """
    path = Path(path).resolve()
    ws = detect_workspace(path)
    if ws is None:
        msg = f"{path} is not a uv workspace"
        raise ValueError(msg)

    pyproject_text = (path / "pyproject.toml").read_text()
    members = _parse_workspace_members(pyproject_text)
    member_names: set[str] = set(members)

    packages: list[PackageInfo] = []
    for member in members:
        member_path = path / member
        if not member_path.is_dir():
            logger.warning("Workspace member %s not found, skipping", member)
            continue

        pkg_src = _find_package_source(member_path)
        if pkg_src is None:
            logger.warning("No source package found in %s, skipping", member)
            continue

        try:
            pkg = get_package(pkg_src)
            packages.append(pkg)
        except (OSError, ValueError):
            logger.warning("Failed to analyze %s, skipping", member, exc_info=True)

    # Build inter-package dependency edges
    package_edges = _build_package_edges(path, members, member_names)

    ws.packages = packages
    ws.package_edges = package_edges
    return ws

build_workspace_context(path)

Build complete workspace context in one call.

Lists all packages, their mutual dependencies, per-package stats, and the workspace-level dependency graph.

Parameters:

Name Type Description Default
path Path

Path to workspace root.

required

Returns:

Type Description
dict[str, Any]

Workspace context dict.

Source code in packages/axm-ast/src/axm_ast/core/workspace.py
def build_workspace_context(path: Path) -> dict[str, Any]:
    """Build complete workspace context in one call.

    Lists all packages, their mutual dependencies, per-package stats,
    and the workspace-level dependency graph.

    Args:
        path: Path to workspace root.

    Returns:
        Workspace context dict.
    """
    ws = analyze_workspace(path)
    graph = build_workspace_dep_graph(ws)

    pkg_summaries = []
    for pkg in ws.packages:
        fn_count = sum(len(m.functions) for m in pkg.modules)
        cls_count = sum(len(m.classes) for m in pkg.modules)
        pkg_summaries.append(
            {
                "name": pkg.name,
                "root": str(pkg.root),
                "module_count": len(pkg.modules),
                "function_count": fn_count,
                "class_count": cls_count,
            }
        )

    return {
        "workspace": ws.name,
        "root": str(ws.root),
        "package_count": len(ws.packages),
        "packages": pkg_summaries,
        "package_graph": graph,
    }

build_workspace_dep_graph(ws)

Build an adjacency-list dependency graph between packages.

Parameters:

Name Type Description Default
ws WorkspaceInfo

Analyzed workspace info.

required

Returns:

Type Description
dict[str, list[str]]

Dict mapping package dir name to list of packages it depends on.

Example

graph = build_workspace_dep_graph(ws) graph["axm-mcp"]['axm']

Source code in packages/axm-ast/src/axm_ast/core/workspace.py
def build_workspace_dep_graph(ws: WorkspaceInfo) -> dict[str, list[str]]:
    """Build an adjacency-list dependency graph between packages.

    Args:
        ws: Analyzed workspace info.

    Returns:
        Dict mapping package dir name to list of packages it depends on.

    Example:
        >>> graph = build_workspace_dep_graph(ws)
        >>> graph["axm-mcp"]
        ['axm']
    """
    graph: dict[str, list[str]] = {}
    for src, target in ws.package_edges:
        graph.setdefault(src, []).append(target)
    return graph

detect_workspace(path)

Detect a uv workspace at the given path.

Reads pyproject.toml looking for [tool.uv.workspace] members. If found, resolves each member's source package directory and returns a populated WorkspaceInfo. Returns None if not a workspace.

Parameters:

Name Type Description Default
path Path

Path to potential workspace root.

required

Returns:

Type Description
WorkspaceInfo | None

WorkspaceInfo if workspace detected, None otherwise.

Example

ws = detect_workspace(Path("/path/to/workspace")) ws is not None True

Source code in packages/axm-ast/src/axm_ast/core/workspace.py
def detect_workspace(path: Path) -> WorkspaceInfo | None:
    """Detect a uv workspace at the given path.

    Reads ``pyproject.toml`` looking for ``[tool.uv.workspace]`` members.
    If found, resolves each member's source package directory and returns
    a populated ``WorkspaceInfo``. Returns ``None`` if not a workspace.

    Args:
        path: Path to potential workspace root.

    Returns:
        WorkspaceInfo if workspace detected, None otherwise.

    Example:
        >>> ws = detect_workspace(Path("/path/to/workspace"))
        >>> ws is not None
        True
    """
    path = Path(path).resolve()
    pyproject = path / "pyproject.toml"
    if not pyproject.exists():
        return None

    text = pyproject.read_text()
    members = _parse_workspace_members(text)
    if not members:
        return None

    ws_name = _parse_project_name(text) or path.name

    return WorkspaceInfo(
        name=ws_name,
        root=path,
        packages=[],
        package_edges=[],
    )

format_workspace_graph_mermaid(ws)

Format the inter-package dependency graph as Mermaid.

Parameters:

Name Type Description Default
ws WorkspaceInfo

Analyzed workspace info.

required

Returns:

Type Description
str

Mermaid diagram string.

Source code in packages/axm-ast/src/axm_ast/core/workspace.py
def format_workspace_graph_mermaid(ws: WorkspaceInfo) -> str:
    """Format the inter-package dependency graph as Mermaid.

    Args:
        ws: Analyzed workspace info.

    Returns:
        Mermaid diagram string.
    """
    graph = build_workspace_dep_graph(ws)
    lines = ["graph TD"]

    # Add package nodes
    pkg_names = {e[0] for e in ws.package_edges} | {e[1] for e in ws.package_edges}
    for pkg in ws.packages:
        name = pkg.name
        if name not in pkg_names:
            # Also include packages without edges
            safe = name.replace("-", "_").replace(".", "_")
            lines.append(f'    {safe}["{name}"]')

    for src, targets in graph.items():
        safe_src = src.replace("-", "_").replace(".", "_")
        for target in targets:
            safe_target = target.replace("-", "_").replace(".", "_")
            lines.append(f"    {safe_src} --> {safe_target}")

    return "\n".join(lines)