diff --git a/src/aggregate.py b/src/aggregate.py index d96942e..035136e 100644 --- a/src/aggregate.py +++ b/src/aggregate.py @@ -145,16 +145,21 @@ def build_file_items(base_dir: Path, files: list[str | dict[str, Any]]) -> list[ tier : int | None (optional tier for context management) auto_aggregate : bool force_full : bool + view_mode : str (summary, full, skeleton, outline, none) [C: src/app_controller.py:AppController._bg_task, src/orchestrator_pm.py:module, tests/test_aggregate_flags.py:test_auto_aggregate_skip, tests/test_aggregate_flags.py:test_force_full, tests/test_tiered_context.py:test_build_file_items_with_tiers] """ with get_monitor().scope("build_file_items"): items: list[dict[str, Any]] = [] + parser = None for entry_raw in files: if isinstance(entry_raw, dict): entry = cast(str, entry_raw.get("path", "")) tier = entry_raw.get("tier") auto_aggregate = entry_raw.get("auto_aggregate", True) force_full = entry_raw.get("force_full", False) + view_mode = entry_raw.get("view_mode", "summary") + if force_full: + view_mode = "full" ast_signatures = entry_raw.get("ast_signatures", False) ast_definitions = entry_raw.get("ast_definitions", False) ast_mask = entry_raw.get("ast_mask", {}) @@ -164,6 +169,9 @@ def build_file_items(base_dir: Path, files: list[str | dict[str, Any]]) -> list[ tier = getattr(entry_raw, "tier", None) auto_aggregate = getattr(entry_raw, "auto_aggregate", True) force_full = getattr(entry_raw, "force_full", False) + view_mode = getattr(entry_raw, "view_mode", "summary") + if force_full: + view_mode = "full" ast_signatures = getattr(entry_raw, "ast_signatures", False) ast_definitions = getattr(entry_raw, "ast_definitions", False) ast_mask = getattr(entry_raw, "ast_mask", {}) @@ -173,6 +181,7 @@ def build_file_items(base_dir: Path, files: list[str | dict[str, Any]]) -> list[ tier = None auto_aggregate = True force_full = False + view_mode = "summary" ast_signatures = False ast_definitions = False ast_mask = {} @@ -181,13 +190,30 @@ def build_file_items(base_dir: Path, files: list[str | dict[str, Any]]) -> list[ continue paths = resolve_paths(base_dir, entry) if not paths: - items.append({"path": None, "entry": entry, "content": f"ERROR: no files matched: {entry}", "error": True, "mtime": 0.0, "tier": tier, "auto_aggregate": auto_aggregate, "force_full": force_full, "ast_signatures": ast_signatures, "ast_definitions": ast_definitions, "ast_mask": ast_mask, "custom_slices": custom_slices}) + items.append({"path": None, "entry": entry, "content": f"ERROR: no files matched: {entry}", "error": True, "mtime": 0.0, "tier": tier, "auto_aggregate": auto_aggregate, "force_full": force_full, "view_mode": view_mode, "ast_signatures": ast_signatures, "ast_definitions": ast_definitions, "ast_mask": ast_mask, "custom_slices": custom_slices}) continue for path in paths: try: content = path.read_text(encoding="utf-8") mtime = path.stat().st_mtime error = False + if not error and view_mode != "full": + if view_mode == "summary": + content = summarize.summarise_file(path, content) + elif view_mode == "skeleton": + if path.suffix == ".py": + if not parser: parser = ASTParser("python") + content = parser.get_skeleton(content, path=str(path)) + else: + content = summarize.summarise_file(path, content) + elif view_mode == "outline": + if path.suffix == ".py": + if not parser: parser = ASTParser("python") + content = parser.get_code_outline(content, path=str(path)) + else: + content = summarize.summarise_file(path, content) + elif view_mode == "none": + content = "(context excluded)" except FileNotFoundError: content = f"ERROR: file not found: {path}" mtime = 0.0 @@ -196,7 +222,7 @@ def build_file_items(base_dir: Path, files: list[str | dict[str, Any]]) -> list[ content = f"ERROR: {e}" mtime = 0.0 error = True - items.append({"path": path, "entry": entry, "content": content, "error": error, "mtime": mtime, "tier": tier, "auto_aggregate": auto_aggregate, "force_full": force_full, "ast_signatures": ast_signatures, "ast_definitions": ast_definitions, "ast_mask": ast_mask, "custom_slices": custom_slices}) + items.append({"path": path, "entry": entry, "content": content, "error": error, "mtime": mtime, "tier": tier, "auto_aggregate": auto_aggregate, "force_full": force_full, "view_mode": view_mode, "ast_signatures": ast_signatures, "ast_definitions": ast_definitions, "ast_mask": ast_mask, "custom_slices": custom_slices}) return items @@ -213,13 +239,20 @@ def _build_files_section_from_items(file_items: list[dict[str, Any]]) -> str: path = item.get("path") entry = item.get("entry", "unknown") content = item.get("content", "") + view_mode = item.get("view_mode", "full") if path is None: - sections.append(f"### `{entry}`\n\n```text\n{content}\n```") + if view_mode == "summary": + sections.append(f"### `{entry}`\n\n{content}") + else: + sections.append(f"### `{entry}`\n\n```text\n{content}\n```") else: path_obj = Path(path) if isinstance(path, str) else path suffix = path_obj.suffix.lstrip(".") if path_obj.suffix else "text" original = entry if "*" not in entry else str(path) - sections.append(f"### `{original}`\n\n```{suffix}\n{content}\n```") + if view_mode == "summary": + sections.append(f"### `{original}`\n\n{content}") + else: + sections.append(f"### `{original}`\n\n```{suffix}\n{content}\n```") return "\n\n---\n\n".join(sections) def build_beads_section(base_dir: Path) -> str: diff --git a/tests/test_context_composition_decoupled.py b/tests/test_context_composition_decoupled.py index d69af0a..9db7709 100644 --- a/tests/test_context_composition_decoupled.py +++ b/tests/test_context_composition_decoupled.py @@ -1,4 +1,5 @@ import pytest +from pathlib import Path from src.app_controller import AppController from src.models import FileItem @@ -35,7 +36,7 @@ def test_do_generate_uses_context_files(monkeypatch): def mock_aggregate_run(flat, **kwargs): assert flat["files"]["paths"] == controller.context_files - return ("md", "path", [], "stable_md", "disc_text") + return ("md", Path("path"), []) monkeypatch.setattr(pm, "flat_config", mock_flat_config) monkeypatch.setattr(pm, "save_project", lambda *args: None) diff --git a/tests/test_context_composition_phase6.py b/tests/test_context_composition_phase6.py new file mode 100644 index 0000000..839a2bc --- /dev/null +++ b/tests/test_context_composition_phase6.py @@ -0,0 +1,126 @@ +import pytest +from pathlib import Path +from src import aggregate + +def test_view_mode_summary(tmp_path): + base_dir = tmp_path / "project" + base_dir.mkdir() + test_file = base_dir / "test.py" + test_file.write_text("def hello():\n print('world')\n", encoding="utf-8") + + files = [{"path": "test.py", "view_mode": "summary"}] + items = aggregate.build_file_items(base_dir, files) + + assert len(items) == 1 + assert items[0]["view_mode"] == "summary" + # Content should be a summary, which typically starts with **Python** or similar + assert "**Python**" in items[0]["content"] + assert "functions: hello" in items[0]["content"] + +def test_view_mode_full(tmp_path): + base_dir = tmp_path / "project" + base_dir.mkdir() + test_file = base_dir / "test.py" + content = "def hello():\n print('world')\n" + test_file.write_text(content, encoding="utf-8") + + files = [{"path": "test.py", "view_mode": "full"}] + items = aggregate.build_file_items(base_dir, files) + + assert len(items) == 1 + assert items[0]["view_mode"] == "full" + assert items[0]["content"] == content + +def test_view_mode_skeleton(tmp_path): + base_dir = tmp_path / "project" + base_dir.mkdir() + test_file = base_dir / "test.py" + content = "def hello():\n \"\"\"Docstring.\"\"\"\n print('world')\n" + test_file.write_text(content, encoding="utf-8") + + files = [{"path": "test.py", "view_mode": "skeleton"}] + items = aggregate.build_file_items(base_dir, files) + + assert len(items) == 1 + assert items[0]["view_mode"] == "skeleton" + # Skeleton should have '...' instead of 'print' + assert "def hello():" in items[0]["content"] + assert "Docstring" in items[0]["content"] + assert "..." in items[0]["content"] + assert "print('world')" not in items[0]["content"] + +def test_view_mode_outline(tmp_path): + base_dir = tmp_path / "project" + base_dir.mkdir() + test_file = base_dir / "test.py" + content = "def hello():\n \"\"\"Docstring.\"\"\"\n print('world')\n" + test_file.write_text(content, encoding="utf-8") + + files = [{"path": "test.py", "view_mode": "outline"}] + items = aggregate.build_file_items(base_dir, files) + + assert len(items) == 1 + assert items[0]["view_mode"] == "outline" + # Outline should have [Func] hello (Lines 1-3) + assert "[Func] hello (Lines 1-3)" in items[0]["content"] + +def test_view_mode_none(tmp_path): + base_dir = tmp_path / "project" + base_dir.mkdir() + test_file = base_dir / "test.py" + test_file.write_text("def hello():\n print('world')\n", encoding="utf-8") + + files = [{"path": "test.py", "view_mode": "none"}] + items = aggregate.build_file_items(base_dir, files) + + assert len(items) == 1 + assert items[0]["view_mode"] == "none" + assert items[0]["content"] == "(context excluded)" + +def test_view_mode_default_summary(tmp_path): + base_dir = tmp_path / "project" + base_dir.mkdir() + test_file = base_dir / "test.py" + test_file.write_text("def hello():\n print('world')\n", encoding="utf-8") + + # Test with simple string path + files = ["test.py"] + items = aggregate.build_file_items(base_dir, files) + + assert len(items) == 1 + assert items[0]["view_mode"] == "summary" + assert "**Python**" in items[0]["content"] + +def test_files_section_rendering(tmp_path): + base_dir = tmp_path / "project" + base_dir.mkdir() + + # Full item + full_item = { + "path": base_dir / "full.txt", + "entry": "full.txt", + "content": "Full content", + "view_mode": "full", + "auto_aggregate": True + } + + # Summary item + summary_item = { + "path": base_dir / "summary.txt", + "entry": "summary.txt", + "content": "**Summary** content", + "view_mode": "summary", + "auto_aggregate": True + } + + sections = aggregate._build_files_section_from_items([full_item, summary_item]) + + # Full should be in backticks + assert "### `full.txt`" in sections + assert "```txt\nFull content\n```" in sections + + # Summary should NOT be in backticks + assert "### `summary.txt`" in sections + assert "```txt\n**Summary** content\n```" not in sections + assert "**Summary** content" in sections +