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.

PackageSummary

Bases: TypedDict

Per-package summary entry inside a :class:WorkspaceContext.

total=False so depth-0 outputs (name only) remain valid.

Source code in packages/axm-ast/src/axm_ast/core/workspace.py
Python
class PackageSummary(TypedDict, total=False):
    """Per-package summary entry inside a :class:`WorkspaceContext`.

    ``total=False`` so depth-0 outputs (name only) remain valid.
    """

    name: str
    root: str
    module_count: int
    function_count: int
    class_count: int

WorkspaceContext

Bases: TypedDict

Aggregate workspace context returned by :func:build_workspace_context.

total=False because the depth-0 view from :func:format_workspace_context drops package_graph.

Source code in packages/axm-ast/src/axm_ast/core/workspace.py
Python
class WorkspaceContext(TypedDict, total=False):
    """Aggregate workspace context returned by :func:`build_workspace_context`.

    ``total=False`` because the depth-0 view from
    :func:`format_workspace_context` drops ``package_graph``.
    """

    workspace: str
    root: str
    package_count: int
    packages: list[PackageSummary]
    package_graph: dict[str, list[str]]

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
Python
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()
    raw_members = parse_workspace_members(pyproject_text)
    members = _expand_workspace_members(path, raw_members)

    # Build member_names from project names in each member's pyproject.toml
    member_names: set[str] = set()
    for member in members:
        member_pyproject = path / member / "pyproject.toml"
        if member_pyproject.exists():
            name = _parse_project_name(member_pyproject.read_text())
            if name:
                member_names.add(name)
        member_names.add(Path(member).name)

    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
WorkspaceContext

Workspace context dict.

Source code in packages/axm-ast/src/axm_ast/core/workspace.py
Python
def build_workspace_context(path: Path) -> WorkspaceContext:
    """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: list[PackageSummary] = []
    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(
            PackageSummary(
                name=pkg.name,
                root=str(pkg.root),
                module_count=len(pkg.modules),
                function_count=fn_count,
                class_count=cls_count,
            )
        )

    return WorkspaceContext(
        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
Python
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
Python
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_context(ctx, *, depth=1)

Apply depth-based filtering to workspace context.

Parameters:

Name Type Description Default
ctx WorkspaceContext

Full workspace context from :func:build_workspace_context.

required
depth int

Detail level. 0 = compact (names only, no graph),

= 1 = full output.

1

Returns:

Type Description
WorkspaceContext

Filtered workspace context dict.

Source code in packages/axm-ast/src/axm_ast/core/workspace.py
Python
def format_workspace_context(
    ctx: WorkspaceContext, *, depth: int = 1
) -> WorkspaceContext:
    """Apply depth-based filtering to workspace context.

    Args:
        ctx: Full workspace context from :func:`build_workspace_context`.
        depth: Detail level. 0 = compact (names only, no graph),
            >= 1 = full output.

    Returns:
        Filtered workspace context dict.
    """
    if depth >= 1:
        return ctx

    return WorkspaceContext(
        workspace=ctx["workspace"],
        root=ctx["root"],
        package_count=ctx["package_count"],
        packages=[PackageSummary(name=pkg["name"]) for pkg in ctx["packages"]],
    )

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
Python
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
    for pkg in ws.packages:
        name = pkg.name
        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)

format_workspace_text(ctx)

Format workspace context as compact plain text for ToolResult.text.

Parameters:

Name Type Description Default
ctx WorkspaceContext

Workspace context dict from :func:build_workspace_context or :func:format_workspace_context.

required

Returns:

Type Description
str

Compact text string.

Source code in packages/axm-ast/src/axm_ast/core/workspace.py
Python
def format_workspace_text(ctx: WorkspaceContext) -> str:
    """Format workspace context as compact plain text for ToolResult.text.

    Args:
        ctx: Workspace context dict from :func:`build_workspace_context`
            or :func:`format_workspace_context`.

    Returns:
        Compact text string.
    """
    lines: list[str] = []
    lines.append(
        f"{ctx.get('workspace', '')} | workspace | "
        f"{ctx.get('package_count', 0)} packages"
    )

    packages = ctx.get("packages", [])
    if packages:
        lines.append("")
        lines.append("Packages:")
        for pkg in packages:
            mod_c = pkg.get("module_count")
            if mod_c is not None:
                fn_c = pkg.get("function_count", 0)
                cls_c = pkg.get("class_count", 0)
                lines.append(f"  {pkg['name']}: {mod_c} mod, {fn_c} fn, {cls_c} cls")
            else:
                lines.append(f"  {pkg['name']}")

    graph = ctx.get("package_graph", {})
    if graph:
        lines.append("")
        lines.append("Dependencies:")
        for src in sorted(graph):
            lines.append(f"  {src}{', '.join(graph[src])}")

    return "\n".join(lines)

parse_workspace_members(text)

Extract workspace member names from pyproject.toml text.

Parses the [tool.uv.workspace] members list. Pure-string helper promoted to a stable module-public surface so callers (and tests) can inspect the raw [tool.uv.workspace].members array without forcing a full :func:analyze_workspace round-trip.

Parameters:

Name Type Description Default
text str

Raw pyproject.toml content.

required

Returns:

Type Description
list[str]

List of member directory names, empty if not found.

Source code in packages/axm-ast/src/axm_ast/core/workspace.py
Python
def parse_workspace_members(text: str) -> list[str]:
    """Extract workspace member names from pyproject.toml text.

    Parses the ``[tool.uv.workspace]`` members list. Pure-string helper
    promoted to a stable module-public surface so callers (and tests) can
    inspect the raw ``[tool.uv.workspace].members`` array without forcing
    a full :func:`analyze_workspace` round-trip.

    Args:
        text: Raw pyproject.toml content.

    Returns:
        List of member directory names, empty if not found.
    """
    # Match [tool.uv.workspace] section
    match = re.search(
        r"\[tool\.uv\.workspace\]\s*\n\s*members\s*=\s*\[([^\]]*)\]",
        text,
        re.DOTALL,
    )
    if not match:
        return []

    raw = match.group(1)
    return [m.strip().strip("\"'") for m in raw.split(",") if m.strip().strip("\"'")]