Source code for codecrate.markdown

from __future__ import annotations

import json
from collections import defaultdict
from pathlib import Path
from typing import Any, Literal

from .analysis_metadata import build_repository_guide
from .fences import choose_backtick_fence, is_fence_close, parse_fence_open
from .focus import FocusSelectionResult
from .formats import (
    FENCE_AGENT_WORKFLOW,
    FENCE_MACHINE_HEADER,
    FENCE_MANIFEST,
    PACK_FORMAT_VERSION,
)
from .locators import (
    anchor_for_file_index,
    anchor_for_file_source,
    anchor_for_symbol,
)
from .manifest import machine_header, to_manifest
from .model import ClassRef, FilePack, PackResult
from .ordering import sort_paths
from .output_model import (
    LineRange,
    MarkdownUsageContext,
    RenderedMarkdown,
    RenderMetadata,
)
from .parse import parse_symbols
from .setup_metadata import detect_setup_metadata


def _file_range(line_count: int) -> str:
    return "(empty)" if line_count == 0 else f"(L1-{line_count})"


def _ensure_nl(s: str) -> str:
    return s if (not s or s.endswith("\n")) else (s + "\n")


def _append_fenced_block(lines: list[str], content: str, info: str) -> None:
    fence = choose_backtick_fence(content)
    lines.append(f"{fence}{info}\n")
    lines.append(_ensure_nl(content))
    lines.append(f"{fence}\n\n")


def _fence_lang_for(rel_path: str) -> str:
    ext = rel_path.rsplit(".", 1)[-1].lower() if "." in rel_path else ""
    return {
        "py": "python",
        "toml": "toml",
        "rst": "rst",
        "md": "markdown",
        "txt": "text",
        "ini": "ini",
        "cfg": "ini",
        "yaml": "yaml",
        "yml": "yaml",
        "json": "json",
    }.get(ext, "text")


def _range_token(kind: str, key: str) -> str:
    return f"<<CC:{kind}:{key}>>"


_SECTION_TITLES: tuple[str, ...] = (
    "Focus Selection",
    "Directory Tree",
    "Repository Guide",
    "Symbol Index",
    "Function Library",
    "Files",
)


def _format_range(start: int | None, end: int | None) -> str:
    if start is None or end is None or start > end:
        return "(empty)"
    return f"(L{start}-{end})"


def _extract_rel_path(line: str) -> str | None:
    if not line.startswith("### `"):
        return None
    start = line.find("`") + 1
    end = line.find("`", start)
    if start <= 0 or end <= start:
        return None
    return line[start:end]


def _next_non_empty_line_idx(lines: list[str], start: int) -> int | None:
    for idx in range(start, len(lines)):
        if lines[idx].strip():
            return idx
    return None


def _is_file_block_end(lines: list[str], fence_end_idx: int) -> bool:
    next_idx = _next_non_empty_line_idx(lines, fence_end_idx + 1)
    if next_idx is None:
        return True
    next_line = lines[next_idx]
    return (
        next_line.startswith("### `")
        or next_line.startswith("**Symbols**")
        or next_line.startswith("# Repository:")
    )


def _is_function_block_end(lines: list[str], fence_end_idx: int) -> bool:
    next_idx = _next_non_empty_line_idx(lines, fence_end_idx + 1)
    if next_idx is None:
        return True
    next_line = lines[next_idx]
    return (
        next_line.startswith('<a id="func-')
        or next_line.startswith("### ")
        or next_line.startswith("## Files")
        or next_line.startswith("# Repository:")
    )


def _scan_file_blocks(lines: list[str]) -> dict[str, tuple[int, int] | None]:
    ranges: dict[str, tuple[int, int] | None] = {}
    in_files = False
    i = 0
    while i < len(lines):
        line = lines[i]
        if line.strip() == "## Files":
            in_files = True
            i += 1
            continue
        if in_files and line.startswith("## ") and line.strip() != "## Files":
            break
        if in_files and line.startswith("### `"):
            rel = _extract_rel_path(line)
            if rel is None:
                i += 1
                continue
            j = i + 1
            fence = ""
            while j < len(lines):
                opened = parse_fence_open(lines[j])
                if opened is not None:
                    fence = opened[0]
                    break
                j += 1
            if j >= len(lines):
                ranges[rel] = None
                i = j
                continue
            start_line = j + 2
            k = j + 1
            while k < len(lines):
                if is_fence_close(lines[k], fence) and _is_file_block_end(lines, k):
                    break
                k += 1
            end_line = k
            if start_line > end_line:
                ranges[rel] = None
            else:
                ranges[rel] = (start_line, end_line)
            i = k + 1
            continue
        i += 1
    return ranges


def _scan_function_library(lines: list[str]) -> dict[str, tuple[int, int] | None]:
    ranges: dict[str, tuple[int, int] | None] = {}
    in_lib = False
    i = 0
    while i < len(lines):
        line = lines[i]
        if line.strip() == "## Function Library":
            in_lib = True
            i += 1
            continue
        if in_lib and line.startswith("## ") and line.strip() != "## Function Library":
            break
        if in_lib and line.startswith("### "):
            defn_id = line.replace("###", "").strip()
            j = i + 1
            fence = ""
            while j < len(lines):
                opened = parse_fence_open(lines[j])
                if opened is not None and opened[1] == "python":
                    fence = opened[0]
                    break
                j += 1
            if j >= len(lines):
                i += 1
                continue
            start_line = j + 2
            k = j + 1
            while k < len(lines):
                if is_fence_close(lines[k], fence) and _is_function_block_end(lines, k):
                    break
                k += 1
            end_line = k
            if start_line > end_line:
                ranges[defn_id] = None
            else:
                ranges[defn_id] = (start_line, end_line)
            i = k + 1
            continue
        i += 1
    return ranges


def _scan_section_ranges(lines: list[str]) -> dict[str, tuple[int, int] | None]:
    headings: list[tuple[str, int]] = []
    fence: str | None = None
    for idx, line in enumerate(lines, start=1):
        if fence is None:
            opened = parse_fence_open(line)
            if opened is not None:
                fence = opened[0]
                continue
        else:
            if is_fence_close(line, fence):
                fence = None
            continue
        if line.startswith("## "):
            headings.append((line[3:].strip(), idx))

    ranges: dict[str, tuple[int, int] | None] = {}
    for i, (title, start_line) in enumerate(headings):
        if title not in _SECTION_TITLES:
            continue
        end_line = headings[i + 1][1] - 1 if i + 1 < len(headings) else len(lines)
        ranges[title] = (start_line, end_line) if start_line <= end_line else None
    return ranges


def _scan_symbol_index_ranges(
    lines: list[str],
    defs_by_file: dict[str, list[str]],
) -> dict[str, tuple[int, int] | None]:
    ranges: dict[str, tuple[int, int] | None] = {}
    in_index = False
    current_file: str | None = None
    current_index = 0
    for idx, line in enumerate(lines, start=1):
        if line.strip() == "## Symbol Index":
            in_index = True
            continue
        if in_index and line.startswith("## ") and line.strip() != "## Symbol Index":
            break
        if not in_index:
            continue
        rel_path = _extract_rel_path(line)
        if rel_path is not None:
            current_file = rel_path
            current_index = 0
            continue
        if (
            current_file is None
            or not line.startswith("- `")
            or line.startswith("- `class ")
        ):
            continue
        defs = defs_by_file.get(current_file, [])
        if current_index >= len(defs):
            continue
        ranges[defs[current_index]] = (idx, idx)
        current_index += 1
    return ranges


def _scan_anchor_ids(lines: list[str]) -> set[str]:
    anchors: set[str] = set()
    for line in lines:
        if line.startswith('<a id="') and line.endswith('"></a>'):
            anchors.add(line[len('<a id="') : -len('"></a>')])
    return anchors


def _to_line_ranges(
    ranges: dict[str, tuple[int, int] | None],
) -> dict[str, LineRange]:
    out: dict[str, LineRange] = {}
    for key, value in ranges.items():
        if value is None:
            continue
        out[key] = LineRange(start_line=value[0], end_line=value[1])
    return out


def _apply_context_line_numbers(
    text: str,
    def_line_map: dict[str, tuple[int, int]],
    class_line_map: dict[str, tuple[int, int]],
    def_to_canon: dict[str, str],
    def_to_file: dict[str, str],
    class_to_file: dict[str, str],
    defs_by_file: dict[str, list[str]],
    use_stubs: bool,
) -> RenderedMarkdown:
    lines = text.splitlines()
    section_ranges = _scan_section_ranges(lines)
    file_ranges = _scan_file_blocks(lines)
    func_ranges = _scan_function_library(lines) if use_stubs else {}
    symbol_index_ranges = _scan_symbol_index_ranges(lines, defs_by_file)
    anchors_present = _scan_anchor_ids(lines)

    replacements: dict[str, str] = {}
    for title in _SECTION_TITLES:
        token = _range_token("SECTION", title)
        rng = section_ranges.get(title)
        if rng is None:
            replacements[token] = _format_range(None, None)
        else:
            replacements[token] = _format_range(rng[0], rng[1])

    for rel, rng in file_ranges.items():
        token = _range_token("FILE", rel)
        if rng is None:
            replacements[token] = "(empty)"
        else:
            replacements[token] = _format_range(rng[0], rng[1])

    for class_id, loc in class_line_map.items():
        rel_path = class_to_file.get(class_id)
        token = _range_token("CLASS", class_id)
        file_range = file_ranges.get(rel_path) if rel_path else None
        if file_range is None:
            replacements[token] = _format_range(None, None)
            continue
        start = file_range[0] + loc[0] - 1
        end = file_range[0] + loc[1] - 1
        replacements[token] = _format_range(start, end)

    for local_id, loc in def_line_map.items():
        token = _range_token("DEF", local_id)
        if use_stubs:
            canon_id = def_to_canon.get(local_id)
            canon_range = func_ranges.get(canon_id) if canon_id else None
            if canon_range is not None:
                replacements[token] = _format_range(canon_range[0], canon_range[1])
                continue
        rel_path = def_to_file.get(local_id)
        file_range = file_ranges.get(rel_path) if rel_path else None
        if file_range is None:
            replacements[token] = _format_range(None, None)
            continue
        start = file_range[0] + loc[0] - 1
        end = file_range[0] + loc[1] - 1
        replacements[token] = _format_range(start, end)

    for token, value in replacements.items():
        text = text.replace(token, value)
    metadata = RenderMetadata(
        section_ranges=_to_line_ranges(section_ranges),
        file_ranges=_to_line_ranges(file_ranges),
        symbol_index_ranges=_to_line_ranges(symbol_index_ranges),
        canonical_ranges=_to_line_ranges(func_ranges),
        anchors_present=frozenset(anchors_present),
    )
    return RenderedMarkdown(markdown=text, metadata=metadata)


def _has_dedupe_effect(pack: PackResult) -> bool:
    """
    True iff at least one definition has local_id != id (meaning dedupe actually
    collapsed identical bodies and rewrote canonical ids).
    """
    for fp in pack.files:
        for d in fp.defs:
            local_id = getattr(d, "local_id", d.id)
            if local_id != d.id:
                return True
    return False


def _read_full_text(fp: FilePack) -> str:
    """Return the packed file contents."""
    return fp.original_text


def _render_tree(paths: list[str]) -> str:
    root: dict[str, Any] = {}
    for p in paths:
        cur = root
        parts = [x for x in p.split("/") if x]
        for part in parts[:-1]:
            child = cur.setdefault(part, {})
            cur = child if isinstance(child, dict) else {}
        cur.setdefault(parts[-1], None)

    def walk(node: dict[str, Any], prefix: str = "") -> list[str]:
        items = sorted(node.items(), key=lambda kv: (kv[1] is None, kv[0].lower()))
        out: list[str] = []
        for i, (name, child) in enumerate(items):
            last = i == len(items) - 1
            branch = "└─ " if last else "├─ "
            out.append(prefix + branch + name)
            if isinstance(child, dict):
                ext = "   " if last else "│  "
                out.extend(walk(child, prefix + ext))
        return out

    return "\n".join(walk(root))


def _render_how_to_use_section(
    *,
    use_stubs: bool,
    include_repository_guide: bool,
    include_directory_tree: bool,
    include_symbol_index: bool,
    usage_context: MarkdownUsageContext | None = None,
) -> str:
    lines: list[str] = []
    lines.append("## How to Use This Pack\n\n")
    lines.append(
        "This pack is a read-only repository snapshot for analysis and patch "
        "proposals.\n\n"
    )
    if usage_context and usage_context.standalone_unpacker_filename:
        command = _reconstruct_command(usage_context)
        command_text = " ".join(command)
        lines.append("**Machine reconstruction**\n\n")
        lines.append(
            "This pack includes a generated standalone unpacker. Prefer "
            "reconstructing the repo before analysis:\n\n"
        )
        _append_fenced_block(lines, command_text + "\n", "bash")
        lines.append(
            "If `python3` is not available, try `/usr/bin/python3`, `python3`, "
            "or `python -S`. The standalone unpacker uses only the Python "
            "standard library.\n\n"
        )
        lines.append(
            "After reconstruction, inspect files under `reconstructed/`. Do not "
            "scrape file bodies from this markdown unless the unpacker fails "
            "with a Codecrate error.\n\n"
        )
        lines.append(
            "Fallback: if the generated unpacker fails with a Codecrate error, "
            "use `codecrate unpack PACK.md -o OUT` if Codecrate is installed. "
            "Do not use whole-file regex extraction; any fallback parser must "
            "copy the generated unpacker's line-by-line fence parsing, manifest "
            "and file hash verification, and path traversal rejection.\n\n"
        )

    lines.append("**Quick workflow**\n")
    step = 1
    if include_directory_tree:
        lines.append(
            f"{step}. **Directory Tree** {_range_token('SECTION', 'Directory Tree')}\n"
        )
        step += 1
    if include_repository_guide:
        lines.append(
            f"{step}. **Repository Guide** "
            f"{_range_token('SECTION', 'Repository Guide')}\n"
        )
        step += 1
    if include_symbol_index:
        lines.append(
            f"{step}. **Symbol Index** {_range_token('SECTION', 'Symbol Index')}\n"
        )
        step += 1
    if use_stubs:
        lines.append(
            f"{step}. **Function Library** "
            f"{_range_token('SECTION', 'Function Library')}\n"
        )
        step += 1
        lines.append(f"{step}. **Files** {_range_token('SECTION', 'Files')}\n")
        lines.append(
            f"{step + 1}. For stubbed functions (`...  # ↪ FUNC:v1:XXXXXXXX`), "
            "use **Function "
            "Library** to read full bodies by ID.\n"
        )
    else:
        lines.append(f"{step}. **Files** {_range_token('SECTION', 'Files')}\n")
    lines.append("\n")

    lines.append(
        "**Proposing changes**\n"
        "- Prefer minimal unified diffs (`--- a/...` / `+++ b/...`) with "
        "repo-relative paths.\n\n"
    )

    return "".join(lines)


def _reconstruct_command(usage_context: MarkdownUsageContext) -> list[str]:
    command = [
        "python3",
        "-S",
        usage_context.standalone_unpacker_filename or "context.unpack.py",
        usage_context.markdown_filename,
        "-o",
        "reconstructed",
    ]
    if usage_context.include_machine_header:
        command.append("--check-machine-header")
    command.extend(["--strict", "--fail-on-warning"])
    return command


def _render_agent_workflow_block(
    usage_context: MarkdownUsageContext | None,
) -> str:
    if usage_context is None or usage_context.standalone_unpacker_filename is None:
        return ""
    payload: dict[str, Any] = {
        "schema": "codecrate.agent-workflow.v1",
        "recommended_first_action": "reconstruct",
        "markdown": usage_context.markdown_filename,
        "standalone_unpacker": usage_context.standalone_unpacker_filename,
        "reconstruct_command": _reconstruct_command(usage_context),
        "fallback_interpreters": ["/usr/bin/python3", "python3", "python -S"],
        "inspect_after_reconstruction": True,
        "manual_markdown_scraping": "avoid-unless-unpacker-fails",
    }
    if usage_context.index_json_filename:
        payload["index_json"] = usage_context.index_json_filename
    if usage_context.manifest_json_filename:
        payload["manifest_json"] = usage_context.manifest_json_filename

    lines: list[str] = []
    _append_fenced_block(
        lines,
        json.dumps(payload, indent=2, sort_keys=False) + "\n",
        FENCE_AGENT_WORKFLOW,
    )
    return "".join(lines)


def _render_dependency_list(items: list[str]) -> str:
    return ", ".join(f"`{item}`" for item in items)


def _render_environment_setup_section(root: Path) -> str:
    setup = detect_setup_metadata(root)
    if setup is None:
        return ""

    lines: list[str] = []
    lines.append("## Environment Setup\n\n")
    lines.append(f"- Ecosystem: {setup.ecosystem}\n")
    lines.append(f"- Detected from: `{setup.source_file}`\n")
    lines.append(f"- Prepare command: `{setup.prepare_command}`\n")
    if setup.dev_prepare_command:
        lines.append(f"- Optional dev command: `{setup.dev_prepare_command}`\n")
    if setup.runtime_dependencies:
        lines.append(
            "- Runtime dependencies: "
            f"{_render_dependency_list(setup.runtime_dependencies)}\n"
        )
    if setup.dev_dependencies:
        lines.append(
            f"- Dev dependencies: {_render_dependency_list(setup.dev_dependencies)}\n"
        )
    lines.append(
        "- If dependencies or tools are missing at runtime, run the prepare command "
        "before executing project code or tests.\n\n"
    )
    return "".join(lines)


def _render_repository_guide_section(*, root: Path, pack: PackResult) -> str:
    guide = build_repository_guide(root=root, pack=pack)
    if not any(guide.values()):
        return ""

    label_map = {
        "entrypoints": "Entrypoints",
        "main_workflows": "Main workflows",
        "key_config_files": "Key config files",
        "central_modules": "Central modules",
        "test_clusters": "Primary test clusters",
    }
    lines = ["## Repository Guide\n\n"]
    for key in (
        "entrypoints",
        "main_workflows",
        "key_config_files",
        "central_modules",
        "test_clusters",
    ):
        values = guide.get(key, [])
        if not values:
            continue
        lines.append(f"- {label_map[key]}: {_render_dependency_list(values)}\n")
    lines.append("\n")
    return "".join(lines)


def _render_focus_selection_section(
    focus_selection: FocusSelectionResult | None,
) -> str:
    if focus_selection is None or not focus_selection.inclusion_reasons:
        return ""
    lines = ["## Focus Selection\n\n"]
    lines.append(f"- Selected files: {len(focus_selection.selected_paths)}\n")
    reasons = sorted(
        {
            reason
            for item in focus_selection.inclusion_reasons.values()
            for reason in item.selected_by
        }
    )
    if reasons:
        lines.append(
            "- Inclusion reasons: " + ", ".join(f"`{item}`" for item in reasons) + "\n"
        )
    preview = list(focus_selection.selected_paths[:8])
    if preview:
        lines.append("- Preview: " + ", ".join(f"`{item}`" for item in preview) + "\n")
    lines.append("\n")
    return "".join(lines)


[docs] def render_markdown_result( # noqa: C901 pack: PackResult, canonical_sources: dict[str, str], layout: str = "auto", nav_mode: Literal["compact", "full"] = "full", skipped_for_safety_count: int = 0, skipped_for_binary_count: int = 0, redacted_for_safety_count: int = 0, *, include_safety_report: bool = False, safety_report_entries: list[dict[str, str]] | None = None, include_manifest: bool = True, include_repository_guide: bool = True, include_symbol_index: bool = True, include_directory_tree: bool = True, include_environment_setup: bool = True, include_how_to_use: bool = True, manifest_data: dict[str, Any] | None = None, repo_label: str = "repo", repo_slug: str = "repo", focus_selection: FocusSelectionResult | None = None, usage_context: MarkdownUsageContext | None = None, ) -> RenderedMarkdown: lines: list[str] = [] lines.append("# Codecrate Context Pack\n\n") # Do not leak absolute local paths; keep the header root stable + relative. lines.append("Root: `.`\n\n") lines.append(f"Format: `{PACK_FORMAT_VERSION}`\n\n") layout_norm = (layout or "auto").strip().lower() if layout_norm not in {"auto", "stubs", "full"}: layout_norm = "auto" nav_mode_norm = nav_mode.strip().lower() if nav_mode_norm not in {"compact", "full"}: nav_mode_norm = "full" compact_nav = nav_mode_norm == "compact" use_stubs = layout_norm == "stubs" or ( layout_norm == "auto" and _has_dedupe_effect(pack) ) resolved_layout = "stubs" if use_stubs else "full" lines.append(f"Layout: `{resolved_layout}`\n\n") if skipped_for_safety_count > 0: lines.append(f"Skipped for safety: {skipped_for_safety_count} file(s)\n\n") if skipped_for_binary_count > 0: lines.append(f"Skipped as binary: {skipped_for_binary_count} file(s)\n\n") if redacted_for_safety_count > 0: lines.append(f"Redacted for safety: {redacted_for_safety_count} file(s)\n\n") if include_safety_report: lines.append("## Safety Report\n\n") entries = safety_report_entries or [] if not entries: lines.append("_No safety findings._\n\n") else: skipped_entries = [e for e in entries if e.get("action") == "skipped"] redacted_entries = [e for e in entries if e.get("action") == "redacted"] lines.append(f"- Skipped: {len(skipped_entries)}\n") lines.append(f"- Redacted: {len(redacted_entries)}\n\n") for item in entries: path = item.get("path", "") action = item.get("action", "") reason = item.get("reason", "") lines.append(f"- `{path}` - **{action}** ({reason})\n") lines.append("\n") def_line_map: dict[str, tuple[int, int]] = {} class_line_map: dict[str, tuple[int, int]] = {} def_to_canon: dict[str, str] = {} def_to_file: dict[str, str] = {} class_to_file: dict[str, str] = {} defs_by_file: dict[str, list[str]] = {} for fp in pack.files: rel = fp.path.relative_to(pack.root).as_posix() defs_by_file[rel] = [ d.local_id for d in sorted(fp.defs, key=lambda d: (d.def_line, d.qualname)) ] for d in fp.defs: def_line_map[d.local_id] = (d.def_line, d.end_line) def_to_canon[d.local_id] = d.id def_to_file[d.local_id] = rel for c in fp.classes: class_to_file[c.id] = rel if use_stubs: for fp in pack.files: by_qualname: dict[str, list[ClassRef]] = defaultdict(list) try: parsed_classes = parse_symbols( path=fp.path, root=pack.root, text=fp.stubbed_text ).classes except SyntaxError: parsed_classes = [] for c in parsed_classes: by_qualname[c.qualname].append(c) for c in sorted(fp.classes, key=lambda x: (x.class_line, x.qualname)): matches = by_qualname.get(c.qualname) if matches: match = matches.pop(0) class_line_map[c.id] = (match.class_line, match.end_line) else: class_line_map[c.id] = (c.class_line, c.end_line) else: for fp in pack.files: for c in fp.classes: class_line_map[c.id] = (c.class_line, c.end_line) guide_section = ( _render_repository_guide_section(root=pack.root, pack=pack) if include_repository_guide else "" ) if include_how_to_use: lines.append( _render_how_to_use_section( use_stubs=use_stubs, include_repository_guide=bool(guide_section), include_directory_tree=include_directory_tree, include_symbol_index=include_symbol_index, usage_context=usage_context, ) ) lines.append(_render_agent_workflow_block(usage_context)) if include_environment_setup: lines.append(_render_environment_setup_section(pack.root)) lines.append(guide_section) lines.append(_render_focus_selection_section(focus_selection)) if include_manifest: manifest_obj = manifest_data or to_manifest(pack, minimal=not use_stubs) header_obj = machine_header( manifest=manifest_obj, repo_label=repo_label, repo_slug=repo_slug, ) lines.append("## Machine Header\n\n") _append_fenced_block( lines, json.dumps(header_obj, sort_keys=True, separators=(",", ":")) + "\n", FENCE_MACHINE_HEADER, ) lines.append("## Manifest\n\n") _append_fenced_block( lines, json.dumps(manifest_obj, indent=2, sort_keys=False) + "\n", FENCE_MANIFEST, ) rel_paths = [ path.relative_to(pack.root).as_posix() for path in sort_paths([f.path for f in pack.files]) ] if include_directory_tree: lines.append("## Directory Tree\n\n") _append_fenced_block(lines, _render_tree(rel_paths) + "\n", "text") if include_symbol_index: lines.append("## Symbol Index\n\n") for fp in sorted(pack.files, key=lambda x: x.path.as_posix()): rel = fp.path.relative_to(pack.root).as_posix() file_range = _range_token("FILE", rel) fa = anchor_for_file_index(rel) sa = anchor_for_file_source(rel) if compact_nav: lines.append(f"### `{rel}` {file_range}\n") lines.append(f'<a id="{fa}"></a>\n') else: # Always provide a jump target to the file contents. lines.append(f"### `{rel}` {file_range} — [jump](#{sa})\n") lines.append(f'<a id="{fa}"></a>\n') for c in sorted(fp.classes, key=lambda x: (x.class_line, x.qualname)): class_loc = _range_token("CLASS", c.id) lines.append(f"- `class {c.qualname}` {class_loc}\n") for d in sorted(fp.defs, key=lambda d: (d.def_line, d.qualname)): loc = _range_token("DEF", d.local_id) has_canonical = d.id in canonical_sources link = "\n" if use_stubs and has_canonical: anchor = anchor_for_symbol(d.id) link = f" — [jump](#{anchor})\n" id_display = f"**{d.id}**" if getattr(d, "local_id", d.id) != d.id: id_display += f" (local **{d.local_id}**)" lines.append(f"- `{d.qualname}` → {id_display} {loc}{link}") else: lines.append(f"- `{d.qualname}` → {loc}\n") lines.append("\n") if use_stubs: lines.append("## Function Library\n\n") for defn_id, code in canonical_sources.items(): lines.append(f'<a id="{anchor_for_symbol(defn_id)}"></a>\n') lines.append(f"### {defn_id}\n") _append_fenced_block(lines, _ensure_nl(code), "python") lines.append("## Files\n\n") for fp in pack.files: rel = fp.path.relative_to(pack.root).as_posix() file_range = _range_token("FILE", rel) lines.append(f"### `{rel}` {file_range}\n") sa = anchor_for_file_source(rel) lines.append(f'<a id="{sa}"></a>\n') if compact_nav: lines.append("\n") else: fa = anchor_for_file_index(rel) lines.append(f"[jump to index](#{fa})\n\n") # Compact stubs are not line-count aligned, so render as a single block. if use_stubs: file_content = _ensure_nl(fp.stubbed_text) else: file_content = _ensure_nl(_read_full_text(fp)) _append_fenced_block(lines, file_content, _fence_lang_for(rel)) # Only emit the Symbols block when there are actually symbols. if use_stubs and fp.defs: lines.append("**Symbols**\n\n") if fp.module: lines.append(f"_Module_: `{fp.module}`\n\n") for d in sorted(fp.defs, key=lambda x: (x.def_line, x.qualname)): loc = _range_token("DEF", d.local_id) has_canonical = d.id in canonical_sources if has_canonical: anchor = anchor_for_symbol(d.id) link = f" — [jump](#{anchor})\n" id_display = f"**{d.id}**" if getattr(d, "local_id", d.id) != d.id: id_display += f" (local **{d.local_id}**)" lines.append(f"- `{d.qualname}` → {id_display} {loc}{link}") else: lines.append(f"- `{d.qualname}` → {loc}\n") lines.append("\n") text = "".join(lines) return _apply_context_line_numbers( text, def_line_map=def_line_map, class_line_map=class_line_map, def_to_canon=def_to_canon, def_to_file=def_to_file, class_to_file=class_to_file, defs_by_file=defs_by_file, use_stubs=use_stubs, )
[docs] def render_markdown( # noqa: C901 pack: PackResult, canonical_sources: dict[str, str], layout: str = "auto", nav_mode: Literal["compact", "full"] = "full", skipped_for_safety_count: int = 0, skipped_for_binary_count: int = 0, redacted_for_safety_count: int = 0, *, include_safety_report: bool = False, safety_report_entries: list[dict[str, str]] | None = None, include_manifest: bool = True, include_repository_guide: bool = True, include_symbol_index: bool = True, include_directory_tree: bool = True, include_environment_setup: bool = True, include_how_to_use: bool = True, manifest_data: dict[str, Any] | None = None, repo_label: str = "repo", repo_slug: str = "repo", usage_context: MarkdownUsageContext | None = None, ) -> str: return render_markdown_result( pack, canonical_sources, layout=layout, nav_mode=nav_mode, skipped_for_safety_count=skipped_for_safety_count, skipped_for_binary_count=skipped_for_binary_count, redacted_for_safety_count=redacted_for_safety_count, include_safety_report=include_safety_report, safety_report_entries=safety_report_entries, include_manifest=include_manifest, include_repository_guide=include_repository_guide, include_symbol_index=include_symbol_index, include_directory_tree=include_directory_tree, include_environment_setup=include_environment_setup, include_how_to_use=include_how_to_use, manifest_data=manifest_data, repo_label=repo_label, repo_slug=repo_slug, usage_context=usage_context, ).markdown