From dc6f5c117e216fab2bb0e2c3659584b066ece7ca Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Nov 2025 08:39:05 +0000 Subject: [PATCH] feat: Add Progressive Reveal CLI tool and Claude Code guide Add the Progressive Reveal CLI tool from gist to tools/progressive-reveal-cli/. This tool allows incremental exploration of files at 4 progressive levels: - Level 0: Metadata (size, type, hash, line count) - Level 1: Structure (keys, imports, classes, functions, headings) - Level 2: Preview (condensed content view) - Level 3: Full content (paginated) Features: - Smart file type detection for YAML, JSON, Markdown, Python - Regex-based grep filtering with context support - Type-aware analysis for different file formats - Safe handling of large and binary files Also add claude.md documentation to guide Claude Code usage with: - Progressive Reveal CLI installation and usage instructions - Common workflows for exploring configs, code, and docs - Tips for working with the GenesisGraph codebase - Repository structure overview This tool is particularly useful for Claude Code to efficiently explore and understand large files without needing to read entire contents. --- claude.md | 134 ++++++++++++ tools/progressive-reveal-cli/README.md | 101 +++++++++ tools/progressive-reveal-cli/pyproject.toml | 56 +++++ .../progressive-reveal-cli/reveal/__init__.py | 4 + .../reveal/analyzers/__init__.py | 17 ++ .../reveal/analyzers/base.py | 37 ++++ .../reveal/analyzers/json_analyzer.py | 130 +++++++++++ .../reveal/analyzers/markdown_analyzer.py | 114 ++++++++++ .../reveal/analyzers/python_analyzer.py | 116 ++++++++++ .../reveal/analyzers/text_analyzer.py | 35 +++ .../reveal/analyzers/yaml_analyzer.py | 114 ++++++++++ tools/progressive-reveal-cli/reveal/cli.py | 201 ++++++++++++++++++ tools/progressive-reveal-cli/reveal/core.py | 114 ++++++++++ .../reveal/detectors.py | 27 +++ .../reveal/formatters.py | 187 ++++++++++++++++ .../reveal/grep_filter.py | 85 ++++++++ tools/progressive-reveal-cli/setup.py | 46 ++++ 17 files changed, 1518 insertions(+) create mode 100644 claude.md create mode 100644 tools/progressive-reveal-cli/README.md create mode 100644 tools/progressive-reveal-cli/pyproject.toml create mode 100644 tools/progressive-reveal-cli/reveal/__init__.py create mode 100644 tools/progressive-reveal-cli/reveal/analyzers/__init__.py create mode 100644 tools/progressive-reveal-cli/reveal/analyzers/base.py create mode 100644 tools/progressive-reveal-cli/reveal/analyzers/json_analyzer.py create mode 100644 tools/progressive-reveal-cli/reveal/analyzers/markdown_analyzer.py create mode 100644 tools/progressive-reveal-cli/reveal/analyzers/python_analyzer.py create mode 100644 tools/progressive-reveal-cli/reveal/analyzers/text_analyzer.py create mode 100644 tools/progressive-reveal-cli/reveal/analyzers/yaml_analyzer.py create mode 100644 tools/progressive-reveal-cli/reveal/cli.py create mode 100644 tools/progressive-reveal-cli/reveal/core.py create mode 100644 tools/progressive-reveal-cli/reveal/detectors.py create mode 100644 tools/progressive-reveal-cli/reveal/formatters.py create mode 100644 tools/progressive-reveal-cli/reveal/grep_filter.py create mode 100644 tools/progressive-reveal-cli/setup.py diff --git a/claude.md b/claude.md new file mode 100644 index 0000000..5c58848 --- /dev/null +++ b/claude.md @@ -0,0 +1,134 @@ +# Claude Code Guide for GenesisGraph + +This document provides guidance for working with the GenesisGraph codebase using Claude Code. + +## Available Tools + +### Progressive Reveal CLI + +Located in `tools/progressive-reveal-cli/`, this is a powerful command-line tool for exploring files at different levels of detail. It's particularly useful when analyzing large configuration files, data files, or complex code files in the GenesisGraph repository. + +#### Installation + +```bash +cd tools/progressive-reveal-cli +pip install -e . +``` + +#### Usage + +The tool provides 4 progressive levels of file exploration: + +**Level 0: Metadata** - Quick overview of file characteristics +```bash +reveal path/to/file.yaml +``` +Shows: filename, type, size, modification date, line count, SHA256 hash + +**Level 1: Structure** - Structural synopsis without content +```bash +reveal path/to/file.yaml --level 1 +``` +For different file types, shows: +- YAML/JSON: Top-level keys, nesting depth, object/array counts +- Python: Imports, classes, functions, top-level assignments +- Markdown: Headings hierarchy, paragraph/code block/list counts +- Text: Line count, word count, estimated type + +**Level 2: Preview** - Content preview with key sections +```bash +reveal path/to/file.py --level 2 +``` +Shows a condensed view of important content (e.g., class/function signatures, docstrings) + +**Level 3: Full Content** - Complete file content with pagination +```bash +reveal path/to/file.md --level 3 --page-size 50 +``` +Full file content with configurable page size + +#### Filtering with Grep + +All levels support regex-based filtering: + +```bash +# Find specific patterns in structure +reveal config.yaml --level 1 --grep "database" + +# Preview with context around matches +reveal app.py --level 2 --grep "class" --context 2 + +# Full content filtered by pattern +reveal README.md --level 3 --grep "installation" --context 5 +``` + +#### Common Workflows + +**Exploring configuration files:** +```bash +# Start with metadata to understand size/type +reveal pyproject.toml + +# Check structure to see all configuration sections +reveal pyproject.toml --level 1 + +# Preview specific sections +reveal pyproject.toml --level 2 --grep "dependencies" + +# View full content when needed +reveal pyproject.toml --level 3 +``` + +**Understanding Python modules:** +```bash +# See module structure (imports, classes, functions) +reveal genesisgraph/core.py --level 1 + +# Preview class/function signatures with docstrings +reveal genesisgraph/core.py --level 2 + +# Find specific implementations +reveal genesisgraph/core.py --level 3 --grep "def process" --context 10 +``` + +**Analyzing documentation:** +```bash +# See document structure (headings, sections) +reveal README.md --level 1 + +# Preview introduction and key sections +reveal README.md --level 2 + +# Find specific topics +reveal README.md --level 3 --grep "API" --context 3 +``` + +## Tips for Working with GenesisGraph + +1. **Start Small**: Use `reveal` with level 0 or 1 to understand file structure before diving into full content +2. **Use Grep**: Filter large files with `--grep` to focus on relevant sections +3. **Progressive Exploration**: Follow the natural progression (metadata → structure → preview → full content) +4. **Context Lines**: Use `--context` to see code around matches for better understanding + +## Repository Structure + +``` +genesisgraph/ +├── genesisgraph/ # Main package source code +├── tests/ # Test suite +├── docs/ # Documentation +├── tools/ # Development tools (including progressive-reveal-cli) +├── examples/ # Usage examples +├── scripts/ # Build and utility scripts +└── pyproject.toml # Project configuration +``` + +## Working with Claude Code + +When Claude Code is exploring the GenesisGraph codebase, it can use the Progressive Reveal CLI to: +- Quickly understand file structure without reading entire files +- Filter large configuration or data files for specific sections +- Get file metadata to make informed decisions about what to analyze +- Preview code structure before making modifications + +This is especially useful for large files where full content might be overwhelming or unnecessary for the task at hand. diff --git a/tools/progressive-reveal-cli/README.md b/tools/progressive-reveal-cli/README.md new file mode 100644 index 0000000..63cda4e --- /dev/null +++ b/tools/progressive-reveal-cli/README.md @@ -0,0 +1,101 @@ +# Progressive Reveal CLI + +A powerful command-line tool for exploring files at different levels of detail. Progressive Reveal CLI allows you to understand file contents incrementally, from high-level metadata to detailed content, with smart type-aware analysis for YAML, JSON, Markdown, and Python files. + +## Features + +- **4 Progressive Levels**: Metadata → Structure → Preview → Full Content +- **Smart File Type Detection**: Automatic detection and specialized handling for YAML, JSON, Markdown, Python +- **Grep Filtering**: Regex-based filtering with context support across all levels +- **Type-Aware Analysis**: + - YAML: Top-level keys, nesting depth, anchors/aliases + - JSON: Object/array counts, max depth, value types + - Markdown: Headings, paragraphs, code blocks + - Python: Imports, classes, functions, docstrings +- **Paged Output**: Configurable page size for large files +- **Binary Detection**: Safe handling of binary files +- **UTF-8 Support**: Proper Unicode handling throughout + +## Installation + +```bash +# Install from the tools directory +cd tools/progressive-reveal-cli +pip install -e . +``` + +## Quick Start + +```bash +# View metadata +reveal myfile.yaml + +# View structure +reveal myfile.json --level 1 + +# View preview with filtering +reveal myfile.py --level 2 --grep "class" + +# View full content with paging +reveal myfile.md --level 3 --page-size 50 +``` + +## Usage with Claude Code + +This tool is particularly useful when working with Claude Code to explore large codebases or configuration files incrementally: + +```bash +# Start with metadata to understand file size and type +reveal config.yaml + +# Then view structure to see top-level keys +reveal config.yaml --level 1 + +# Preview specific sections with grep +reveal config.yaml --level 2 --grep "database" + +# Finally view full content when needed +reveal config.yaml --level 3 +``` + +## Command Line Options + +``` +reveal [options] + +Options: + --level <0-3> Revelation level (default: 0) + 0 = metadata + 1 = structural synopsis + 2 = content preview + 3 = full content (paged) + + --grep, -m Filter pattern (regex) + --context, -C Context lines around matches (default: 0) + --grep-case-sensitive Use case-sensitive grep matching + --page-size Page size for level 3 (default: 120) + --force Force read of large or binary files +``` + +## Examples + +```bash +# View file metadata +reveal config.yaml + +# View YAML structure +reveal config.yaml --level 1 + +# Preview Python file +reveal app.py --level 2 + +# View full content with grep +reveal app.py --level 3 --grep "class" --context 2 + +# Force read large file +reveal large.log --level 3 --force +``` + +## License + +MIT License diff --git a/tools/progressive-reveal-cli/pyproject.toml b/tools/progressive-reveal-cli/pyproject.toml new file mode 100644 index 0000000..7bd8888 --- /dev/null +++ b/tools/progressive-reveal-cli/pyproject.toml @@ -0,0 +1,56 @@ +[build-system] +requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2"] +build-backend = "setuptools.build_meta" + +[project] +name = "progressive-reveal-cli" +version = "0.1.0" +description = "A CLI tool for exploring files at different levels of detail" +readme = "README.md" +requires-python = ">=3.8" +license = {text = "MIT"} +authors = [ + {name = "Progressive Reveal Team"} +] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "pyyaml>=6.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0", + "pytest-cov>=4.0", +] + +[project.scripts] +reveal = "reveal.cli:main" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] + +[tool.coverage.run] +source = ["reveal"] +omit = ["*/tests/*", "*/test_*.py"] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", +] diff --git a/tools/progressive-reveal-cli/reveal/__init__.py b/tools/progressive-reveal-cli/reveal/__init__.py new file mode 100644 index 0000000..2042b53 --- /dev/null +++ b/tools/progressive-reveal-cli/reveal/__init__.py @@ -0,0 +1,4 @@ +"""Progressive Reveal CLI - A tool for exploring files at different levels of detail.""" + +__version__ = "0.1.0" +__author__ = "Progressive Reveal Team" diff --git a/tools/progressive-reveal-cli/reveal/analyzers/__init__.py b/tools/progressive-reveal-cli/reveal/analyzers/__init__.py new file mode 100644 index 0000000..b6e981e --- /dev/null +++ b/tools/progressive-reveal-cli/reveal/analyzers/__init__.py @@ -0,0 +1,17 @@ +"""Analyzers for different file types.""" + +from .base import BaseAnalyzer +from .yaml_analyzer import YAMLAnalyzer +from .json_analyzer import JSONAnalyzer +from .markdown_analyzer import MarkdownAnalyzer +from .python_analyzer import PythonAnalyzer +from .text_analyzer import TextAnalyzer + +__all__ = [ + 'BaseAnalyzer', + 'YAMLAnalyzer', + 'JSONAnalyzer', + 'MarkdownAnalyzer', + 'PythonAnalyzer', + 'TextAnalyzer', +] diff --git a/tools/progressive-reveal-cli/reveal/analyzers/base.py b/tools/progressive-reveal-cli/reveal/analyzers/base.py new file mode 100644 index 0000000..b2d8832 --- /dev/null +++ b/tools/progressive-reveal-cli/reveal/analyzers/base.py @@ -0,0 +1,37 @@ +"""Base analyzer interface.""" + +from abc import ABC, abstractmethod +from typing import Dict, Any, List + + +class BaseAnalyzer(ABC): + """Base class for file type analyzers.""" + + def __init__(self, lines: List[str]): + """ + Initialize analyzer with file lines. + + Args: + lines: List of file lines + """ + self.lines = lines + + @abstractmethod + def analyze_structure(self) -> Dict[str, Any]: + """ + Analyze file structure for Level 1. + + Returns: + Dictionary with structural information + """ + pass + + @abstractmethod + def generate_preview(self) -> List[tuple[int, str]]: + """ + Generate preview for Level 2. + + Returns: + List of (line_number, content) tuples + """ + pass diff --git a/tools/progressive-reveal-cli/reveal/analyzers/json_analyzer.py b/tools/progressive-reveal-cli/reveal/analyzers/json_analyzer.py new file mode 100644 index 0000000..55f0f7b --- /dev/null +++ b/tools/progressive-reveal-cli/reveal/analyzers/json_analyzer.py @@ -0,0 +1,130 @@ +"""JSON file analyzer.""" + +import json +from typing import Dict, Any, List +from .base import BaseAnalyzer + + +class JSONAnalyzer(BaseAnalyzer): + """Analyzer for JSON files.""" + + def __init__(self, lines: List[str]): + super().__init__(lines) + self.parse_error = None + self.parsed_data = None + + try: + content = '\n'.join(lines) + self.parsed_data = json.loads(content) + except Exception as e: + self.parse_error = str(e) + + def analyze_structure(self) -> Dict[str, Any]: + """Analyze JSON structure.""" + if self.parse_error: + return { + 'error': self.parse_error, + 'top_level_keys': [], + 'object_count': 0, + 'array_count': 0, + 'max_depth': 0, + 'value_types': {} + } + + # Get top-level keys + top_level_keys = [] + if isinstance(self.parsed_data, dict): + top_level_keys = list(self.parsed_data.keys()) + + # Count objects and arrays + object_count, array_count = self._count_structures(self.parsed_data) + + # Calculate max depth + max_depth = self._calculate_depth(self.parsed_data) + + # Count value types + value_types = self._count_value_types(self.parsed_data) + + return { + 'top_level_keys': top_level_keys, + 'object_count': object_count, + 'array_count': array_count, + 'max_depth': max_depth, + 'value_types': value_types + } + + def _count_structures(self, obj: Any) -> tuple[int, int]: + """Count objects and arrays recursively.""" + object_count = 0 + array_count = 0 + + if isinstance(obj, dict): + object_count += 1 + for value in obj.values(): + obj_c, arr_c = self._count_structures(value) + object_count += obj_c + array_count += arr_c + elif isinstance(obj, list): + array_count += 1 + for item in obj: + obj_c, arr_c = self._count_structures(item) + object_count += obj_c + array_count += arr_c + + return object_count, array_count + + def _calculate_depth(self, obj: Any, current_depth: int = 0) -> int: + """Calculate maximum nesting depth.""" + if isinstance(obj, dict): + if not obj: + return current_depth + return max(self._calculate_depth(v, current_depth + 1) for v in obj.values()) + elif isinstance(obj, list): + if not obj: + return current_depth + return max(self._calculate_depth(item, current_depth + 1) for item in obj) + else: + return current_depth + + def _count_value_types(self, obj: Any) -> Dict[str, int]: + """Count value types in JSON.""" + types = {} + + def count_type(value): + type_name = type(value).__name__ + if type_name == 'NoneType': + type_name = 'null' + elif type_name == 'bool': + type_name = 'boolean' + elif type_name == 'int' or type_name == 'float': + type_name = 'number' + types[type_name] = types.get(type_name, 0) + 1 + + def traverse(item): + if isinstance(item, dict): + for value in item.values(): + traverse(value) + elif isinstance(item, list): + for element in item: + traverse(element) + else: + count_type(item) + + traverse(obj) + return types + + def generate_preview(self) -> List[tuple[int, str]]: + """Generate JSON preview (first 10 key/value pairs or 20 lines).""" + preview = [] + + if self.parse_error: + # Fallback to first 20 lines + for i, line in enumerate(self.lines[:20], 1): + preview.append((i, line)) + return preview + + # Show first 20 lines + for i, line in enumerate(self.lines[:20], 1): + preview.append((i, line)) + + return preview diff --git a/tools/progressive-reveal-cli/reveal/analyzers/markdown_analyzer.py b/tools/progressive-reveal-cli/reveal/analyzers/markdown_analyzer.py new file mode 100644 index 0000000..5bedd90 --- /dev/null +++ b/tools/progressive-reveal-cli/reveal/analyzers/markdown_analyzer.py @@ -0,0 +1,114 @@ +"""Markdown file analyzer.""" + +import re +from typing import Dict, Any, List +from .base import BaseAnalyzer + + +class MarkdownAnalyzer(BaseAnalyzer): + """Analyzer for Markdown files.""" + + def analyze_structure(self) -> Dict[str, Any]: + """Analyze Markdown structure.""" + headings = [] + paragraph_count = 0 + code_block_count = 0 + list_count = 0 + + in_code_block = False + current_paragraph = False + + for line in self.lines: + stripped = line.strip() + + # Code blocks + if stripped.startswith('```'): + in_code_block = not in_code_block + if in_code_block: + code_block_count += 1 + continue + + if in_code_block: + continue + + # Headings + if stripped.startswith('#'): + match = re.match(r'^(#{1,3})\s+(.+)$', stripped) + if match: + level = len(match.group(1)) + title = match.group(2) + headings.append((level, title)) + current_paragraph = False + continue + + # Lists + if re.match(r'^\s*[-*+]\s+', line) or re.match(r'^\s*\d+\.\s+', line): + list_count += 1 + current_paragraph = False + continue + + # Paragraphs + if stripped and not current_paragraph: + paragraph_count += 1 + current_paragraph = True + elif not stripped: + current_paragraph = False + + return { + 'headings': headings, + 'paragraph_count': paragraph_count, + 'code_block_count': code_block_count, + 'list_count': list_count + } + + def generate_preview(self) -> List[tuple[int, str]]: + """Generate Markdown preview.""" + preview = [] + in_frontmatter = False + frontmatter_start = False + first_heading_found = False + lines_after_heading = 0 + in_code_block = False + + for i, line in enumerate(self.lines, 1): + stripped = line.strip() + + # Frontmatter detection + if i == 1 and stripped == '---': + in_frontmatter = True + frontmatter_start = True + preview.append((i, line)) + continue + + if in_frontmatter: + preview.append((i, line)) + if stripped == '---': + in_frontmatter = False + continue + + # Code blocks + if stripped.startswith('```'): + in_code_block = not in_code_block + if in_code_block: + preview.append((i, f"[Code block: {stripped[3:] or 'unknown'}]")) + continue + + if in_code_block: + continue + + # First heading + paragraph + if stripped.startswith('#'): + preview.append((i, line)) + first_heading_found = True + lines_after_heading = 0 + continue + + if first_heading_found and lines_after_heading < 5: + if stripped: + preview.append((i, line)) + lines_after_heading += 1 + + if len(preview) >= 20: + break + + return preview[:30] diff --git a/tools/progressive-reveal-cli/reveal/analyzers/python_analyzer.py b/tools/progressive-reveal-cli/reveal/analyzers/python_analyzer.py new file mode 100644 index 0000000..cf2d1f1 --- /dev/null +++ b/tools/progressive-reveal-cli/reveal/analyzers/python_analyzer.py @@ -0,0 +1,116 @@ +"""Python file analyzer.""" + +import ast +from typing import Dict, Any, List +from .base import BaseAnalyzer + + +class PythonAnalyzer(BaseAnalyzer): + """Analyzer for Python files.""" + + def __init__(self, lines: List[str]): + super().__init__(lines) + self.parse_error = None + self.tree = None + + try: + content = '\n'.join(lines) + self.tree = ast.parse(content) + except Exception as e: + self.parse_error = str(e) + + def analyze_structure(self) -> Dict[str, Any]: + """Analyze Python structure.""" + if self.parse_error: + return { + 'error': self.parse_error, + 'imports': [], + 'classes': [], + 'functions': [], + 'assignments': [] + } + + imports = [] + classes = [] + functions = [] + assignments = [] + + for node in ast.walk(self.tree): + if isinstance(node, ast.Import): + for alias in node.names: + imports.append(alias.name) + elif isinstance(node, ast.ImportFrom): + module = node.module or '' + for alias in node.names: + imports.append(f"{module}.{alias.name}" if module else alias.name) + + # Only top-level elements + for node in self.tree.body: + if isinstance(node, ast.ClassDef): + classes.append(node.name) + elif isinstance(node, ast.FunctionDef) or isinstance(node, ast.AsyncFunctionDef): + functions.append(node.name) + elif isinstance(node, ast.Assign): + for target in node.targets: + if isinstance(target, ast.Name): + assignments.append(target.id) + + return { + 'imports': imports, + 'classes': classes, + 'functions': functions, + 'assignments': assignments + } + + def generate_preview(self) -> List[tuple[int, str]]: + """Generate Python preview.""" + preview = [] + + # Module docstring + if self.tree and isinstance(self.tree.body[0], ast.Expr) and isinstance(self.tree.body[0].value, ast.Constant): + docstring_node = self.tree.body[0] + # Find docstring in lines + for i, line in enumerate(self.lines[:20], 1): + if '"""' in line or "'''" in line: + preview.append((i, line)) + # Add next few lines if multiline docstring + if line.count('"""') == 1 or line.count("'''") == 1: + for j in range(i, min(i + 10, len(self.lines) + 1)): + if j > i: + preview.append((j, self.lines[j - 1])) + if '"""' in self.lines[j - 1][1:] or "'''" in self.lines[j - 1][1:]: + break + break + + if self.parse_error: + # Fallback to first 20 lines + for i, line in enumerate(self.lines[:20], 1): + if (i, line) not in preview: + preview.append((i, line)) + return preview + + # Class signatures + first docstring line + for node in self.tree.body: + if isinstance(node, ast.ClassDef): + class_line = node.lineno + preview.append((class_line, self.lines[class_line - 1])) + + # Get class docstring + if node.body and isinstance(node.body[0], ast.Expr) and isinstance(node.body[0].value, ast.Constant): + doc_line = node.body[0].lineno + preview.append((doc_line, self.lines[doc_line - 1])) + + # Function signatures + first docstring line + for node in self.tree.body: + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + func_line = node.lineno + preview.append((func_line, self.lines[func_line - 1])) + + # Get function docstring + if node.body and isinstance(node.body[0], ast.Expr) and isinstance(node.body[0].value, ast.Constant): + doc_line = node.body[0].lineno + preview.append((doc_line, self.lines[doc_line - 1])) + + # Sort by line number and limit + preview = sorted(list(set(preview)), key=lambda x: x[0]) + return preview[:30] diff --git a/tools/progressive-reveal-cli/reveal/analyzers/text_analyzer.py b/tools/progressive-reveal-cli/reveal/analyzers/text_analyzer.py new file mode 100644 index 0000000..2378623 --- /dev/null +++ b/tools/progressive-reveal-cli/reveal/analyzers/text_analyzer.py @@ -0,0 +1,35 @@ +"""Text file analyzer.""" + +from typing import Dict, Any, List +from .base import BaseAnalyzer + + +class TextAnalyzer(BaseAnalyzer): + """Analyzer for plain text files.""" + + def analyze_structure(self) -> Dict[str, Any]: + """Analyze text structure.""" + line_count = len(self.lines) + word_count = sum(len(line.split()) for line in self.lines) + + # Try to guess file type + estimated_type = 'text' + content = '\n'.join(self.lines) + + if ' List[tuple[int, str]]: + """Generate text preview (first 20 lines).""" + preview = [] + for i, line in enumerate(self.lines[:20], 1): + preview.append((i, line)) + return preview diff --git a/tools/progressive-reveal-cli/reveal/analyzers/yaml_analyzer.py b/tools/progressive-reveal-cli/reveal/analyzers/yaml_analyzer.py new file mode 100644 index 0000000..d0dc9a9 --- /dev/null +++ b/tools/progressive-reveal-cli/reveal/analyzers/yaml_analyzer.py @@ -0,0 +1,114 @@ +"""YAML file analyzer.""" + +import re +from typing import Dict, Any, List +from .base import BaseAnalyzer + +try: + import yaml + HAS_YAML = True +except ImportError: + HAS_YAML = False + + +class YAMLAnalyzer(BaseAnalyzer): + """Analyzer for YAML files.""" + + def __init__(self, lines: List[str]): + super().__init__(lines) + self.parse_error = None + self.parsed_data = None + + if HAS_YAML: + try: + content = '\n'.join(lines) + self.parsed_data = yaml.safe_load(content) + except Exception as e: + self.parse_error = str(e) + + def analyze_structure(self) -> Dict[str, Any]: + """Analyze YAML structure.""" + if self.parse_error: + return { + 'error': self.parse_error, + 'top_level_keys': [], + 'nesting_depth': 0, + 'anchor_count': 0, + 'alias_count': 0 + } + + if not HAS_YAML: + return { + 'error': 'PyYAML not installed. Install with: pip install pyyaml', + 'top_level_keys': [], + 'nesting_depth': 0, + 'anchor_count': 0, + 'alias_count': 0 + } + + # Get top-level keys + top_level_keys = [] + if isinstance(self.parsed_data, dict): + top_level_keys = list(self.parsed_data.keys()) + + # Calculate nesting depth + nesting_depth = self._calculate_depth(self.parsed_data) + + # Count anchors and aliases + content = '\n'.join(self.lines) + anchor_count = len(re.findall(r'&\w+', content)) + alias_count = len(re.findall(r'\*\w+', content)) + + return { + 'top_level_keys': top_level_keys, + 'nesting_depth': nesting_depth, + 'anchor_count': anchor_count, + 'alias_count': alias_count + } + + def _calculate_depth(self, obj: Any, current_depth: int = 0) -> int: + """Calculate maximum nesting depth.""" + if isinstance(obj, dict): + if not obj: + return current_depth + return max(self._calculate_depth(v, current_depth + 1) for v in obj.values()) + elif isinstance(obj, list): + if not obj: + return current_depth + return max(self._calculate_depth(item, current_depth + 1) for item in obj) + else: + return current_depth + + def generate_preview(self) -> List[tuple[int, str]]: + """Generate YAML preview (first 10 key/value pairs or 20 lines).""" + preview = [] + + if self.parse_error or not HAS_YAML: + # Fallback to first 20 lines + for i, line in enumerate(self.lines[:20], 1): + preview.append((i, line)) + return preview + + # Show first 10 key/value pairs or first 20 logical lines + lines_shown = 0 + pairs_shown = 0 + max_pairs = 10 + max_lines = 20 + + for i, line in enumerate(self.lines, 1): + if lines_shown >= max_lines: + break + + stripped = line.strip() + + # Count top-level key/value pairs + if stripped and not stripped.startswith('#') and ':' in stripped: + if not line.startswith((' ', '\t')): # Top-level key + pairs_shown += 1 + if pairs_shown > max_pairs: + break + + preview.append((i, line)) + lines_shown += 1 + + return preview diff --git a/tools/progressive-reveal-cli/reveal/cli.py b/tools/progressive-reveal-cli/reveal/cli.py new file mode 100644 index 0000000..6a5b23e --- /dev/null +++ b/tools/progressive-reveal-cli/reveal/cli.py @@ -0,0 +1,201 @@ +"""Command-line interface for Progressive Reveal.""" + +import sys +import argparse +from pathlib import Path +from typing import List, Optional, Dict, Any + +from .core import FileSummary, create_file_summary +from .analyzers import YAMLAnalyzer, JSONAnalyzer, MarkdownAnalyzer, PythonAnalyzer, TextAnalyzer +from .formatters import format_metadata, format_structure, format_preview, format_full_content +from .grep_filter import apply_grep_filter + + +def get_analyzer(file_type: str, lines: List[str]): + """Get appropriate analyzer for file type.""" + analyzers = { + 'yaml': YAMLAnalyzer, + 'json': JSONAnalyzer, + 'markdown': MarkdownAnalyzer, + 'python': PythonAnalyzer, + 'text': TextAnalyzer, + } + + analyzer_class = analyzers.get(file_type, TextAnalyzer) + return analyzer_class(lines) + + +def reveal_level_0(summary: FileSummary) -> List[str]: + """Generate Level 0 (metadata) output.""" + return format_metadata(summary) + + +def reveal_level_1( + summary: FileSummary, + grep_pattern: Optional[str] = None, + case_sensitive: bool = False +) -> List[str]: + """Generate Level 1 (structure) output.""" + analyzer = get_analyzer(summary.type, summary.lines) + structure = analyzer.analyze_structure() + + lines = format_structure(summary, structure, grep_pattern) + + # Apply grep filter if specified + if grep_pattern: + from .grep_filter import filter_structure_output + lines = filter_structure_output(lines, grep_pattern, case_sensitive) + + return lines + + +def reveal_level_2( + summary: FileSummary, + grep_pattern: Optional[str] = None, + case_sensitive: bool = False, + context: int = 0 +) -> List[str]: + """Generate Level 2 (preview) output.""" + analyzer = get_analyzer(summary.type, summary.lines) + preview = analyzer.generate_preview() + + # Apply grep filter if specified + if grep_pattern: + preview = apply_grep_filter(preview, grep_pattern, case_sensitive, context) + + return format_preview(summary, preview, grep_pattern) + + +def reveal_level_3( + summary: FileSummary, + page_size: int = 120, + grep_pattern: Optional[str] = None, + case_sensitive: bool = False, + context: int = 0 +) -> List[str]: + """Generate Level 3 (full content) output.""" + # Create line tuples + lines_with_numbers = [(i + 1, line) for i, line in enumerate(summary.lines)] + + # Apply grep filter if specified + if grep_pattern: + lines_with_numbers = apply_grep_filter( + lines_with_numbers, + grep_pattern, + case_sensitive, + context + ) + + # Apply paging + total_lines = len(lines_with_numbers) + if total_lines <= page_size: + return format_full_content(summary, lines_with_numbers, grep_pattern, is_end=True) + + # For simplicity, show first page + # In a real implementation, this would be interactive + page_lines = lines_with_numbers[:page_size] + return format_full_content(summary, page_lines, grep_pattern, is_end=False) + + +def main(): + """Main CLI entry point.""" + parser = argparse.ArgumentParser( + description='Progressive Reveal CLI - Explore files at different levels of detail', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Levels: + 0 = metadata + 1 = structural synopsis + 2 = content preview + 3 = full content (paged) + +Examples: + reveal myfile.yaml + reveal myfile.json --level 1 + reveal myfile.py --level 2 --grep "class" + reveal myfile.md --level 3 --page-size 50 + """ + ) + + parser.add_argument('file', type=str, help='File to reveal') + parser.add_argument('--level', type=int, default=0, choices=[0, 1, 2, 3], + help='Revelation level (default: 0)') + parser.add_argument('--grep', '-m', type=str, dest='grep_pattern', + help='Filter pattern (regex)') + parser.add_argument('--context', '-C', type=int, default=0, + help='Context lines around matches (default: 0)') + parser.add_argument('--grep-case-sensitive', action='store_true', + help='Use case-sensitive grep matching') + parser.add_argument('--page-size', type=int, default=120, + help='Page size for level 3 (default: 120)') + parser.add_argument('--force', action='store_true', + help='Force read of large or binary files') + + args = parser.parse_args() + + try: + # Create file path + file_path = Path(args.file).resolve() + + # Create file summary + summary = create_file_summary(file_path, force=args.force) + + # Handle errors + if summary.parse_error and summary.is_binary and not args.force: + print(f"Error: {summary.parse_error}", file=sys.stderr) + sys.exit(1) + + # Generate output based on level + if args.level == 0: + output = reveal_level_0(summary) + elif args.level == 1: + if summary.parse_error and summary.is_binary: + print(f"Error: {summary.parse_error}", file=sys.stderr) + sys.exit(1) + output = reveal_level_1( + summary, + grep_pattern=args.grep_pattern, + case_sensitive=args.grep_case_sensitive + ) + elif args.level == 2: + if summary.parse_error and summary.is_binary: + print(f"Error: {summary.parse_error}", file=sys.stderr) + sys.exit(1) + output = reveal_level_2( + summary, + grep_pattern=args.grep_pattern, + case_sensitive=args.grep_case_sensitive, + context=args.context + ) + elif args.level == 3: + if summary.parse_error and summary.is_binary: + print(f"Error: {summary.parse_error}", file=sys.stderr) + sys.exit(1) + output = reveal_level_3( + summary, + page_size=args.page_size, + grep_pattern=args.grep_pattern, + case_sensitive=args.grep_case_sensitive, + context=args.context + ) + + # Print output + for line in output: + print(line) + + except FileNotFoundError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + except ValueError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + except KeyboardInterrupt: + print("\nInterrupted", file=sys.stderr) + sys.exit(130) + except Exception as e: + print(f"Unexpected error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/tools/progressive-reveal-cli/reveal/core.py b/tools/progressive-reveal-cli/reveal/core.py new file mode 100644 index 0000000..3f1c97b --- /dev/null +++ b/tools/progressive-reveal-cli/reveal/core.py @@ -0,0 +1,114 @@ +"""Core data models and utilities for Progressive Reveal CLI.""" + +import hashlib +import os +from dataclasses import dataclass, field +from datetime import datetime +from pathlib import Path +from typing import List, Optional, Dict, Any + + +@dataclass +class FileSummary: + """Internal data model for file analysis.""" + + path: Path + type: str + size: int + modified: datetime + linecount: int + sha256: str + metadata: Dict[str, Any] = field(default_factory=dict) + structure: Dict[str, Any] = field(default_factory=dict) + preview: str = "" + lines: List[str] = field(default_factory=list) + is_binary: bool = False + parse_error: Optional[str] = None + + +def read_file_safe(file_path: Path, max_bytes: int = 2 * 1024 * 1024, force: bool = False) -> tuple[bool, str, List[str]]: + """ + Safely read a file with size and binary checks. + + Returns: + tuple: (success, error_message, lines) + """ + if not file_path.exists(): + return False, f"File not found: {file_path}", [] + + file_size = file_path.stat().st_size + + if file_size > max_bytes and not force: + return False, f"File too large ({file_size} bytes > {max_bytes} bytes). Use --force to override.", [] + + try: + # Try reading as UTF-8 + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + lines = content.splitlines() + return True, "", lines + except UnicodeDecodeError: + return False, "Binary file detected. Use --force for hexdump.", [] + except Exception as e: + return False, f"Error reading file: {str(e)}", [] + + +def compute_sha256(file_path: Path) -> str: + """Compute SHA256 hash of a file.""" + sha256_hash = hashlib.sha256() + try: + with open(file_path, "rb") as f: + for byte_block in iter(lambda: f.read(4096), b""): + sha256_hash.update(byte_block) + return sha256_hash.hexdigest() + except Exception: + return "ERROR" + + +def create_file_summary(file_path: Path, force: bool = False) -> FileSummary: + """ + Create a FileSummary object from a file path. + + Args: + file_path: Path to the file + force: Whether to force read large/binary files + + Returns: + FileSummary object + """ + from .detectors import detect_file_type + + if not file_path.exists(): + raise FileNotFoundError(f"File not found: {file_path}") + + stat = file_path.stat() + modified = datetime.fromtimestamp(stat.st_mtime) + + # Read file content + success, error, lines = read_file_safe(file_path, force=force) + + if not success: + # Create minimal summary with error + return FileSummary( + path=file_path, + type="unknown", + size=stat.st_size, + modified=modified, + linecount=0, + sha256=compute_sha256(file_path), + is_binary="Binary file" in error, + parse_error=error, + lines=[] + ) + + file_type = detect_file_type(file_path) + + return FileSummary( + path=file_path, + type=file_type, + size=stat.st_size, + modified=modified, + linecount=len(lines), + sha256=compute_sha256(file_path), + lines=lines + ) diff --git a/tools/progressive-reveal-cli/reveal/detectors.py b/tools/progressive-reveal-cli/reveal/detectors.py new file mode 100644 index 0000000..62337e6 --- /dev/null +++ b/tools/progressive-reveal-cli/reveal/detectors.py @@ -0,0 +1,27 @@ +"""File type detection utilities.""" + +from pathlib import Path + + +FILE_TYPE_MAP = { + '.yaml': 'yaml', + '.yml': 'yaml', + '.json': 'json', + '.md': 'markdown', + '.markdown': 'markdown', + '.py': 'python', +} + + +def detect_file_type(file_path: Path) -> str: + """ + Detect file type based on extension. + + Args: + file_path: Path to the file + + Returns: + File type string (e.g., 'yaml', 'json', 'markdown', 'python', 'text') + """ + suffix = file_path.suffix.lower() + return FILE_TYPE_MAP.get(suffix, 'text') diff --git a/tools/progressive-reveal-cli/reveal/formatters.py b/tools/progressive-reveal-cli/reveal/formatters.py new file mode 100644 index 0000000..65d8279 --- /dev/null +++ b/tools/progressive-reveal-cli/reveal/formatters.py @@ -0,0 +1,187 @@ +"""Output formatting utilities.""" + +from typing import List, Optional, Dict, Any +from .core import FileSummary + + +def format_header(section: str, level: int, grep_pattern: Optional[str] = None) -> str: + """Format section header.""" + header = f"=== {section.upper()} (Level {level})" + if grep_pattern: + header += f", Filtered: \"{grep_pattern}\"" + header += " ===" + return header + + +def format_line_number(line_num: int, max_num: int = 9999) -> str: + """Format line number with left padding.""" + width = len(str(max_num)) + return f"{line_num:0{width}d}" + + +def format_breadcrumb(level: int, is_end: bool = False) -> str: + """Format breadcrumb suggestion.""" + if is_end: + return "→ (end) ← Back to --level 2" + + suggestions = { + 0: "→ Try --level 1 for structure", + 1: "→ Try --level 2 for preview", + 2: "→ Try --level 3 for full content" + } + + return suggestions.get(level, "") + + +def format_metadata(summary: FileSummary) -> List[str]: + """Format Level 0 metadata output.""" + lines = [] + lines.append(format_header("METADATA", 0)) + lines.append("") + lines.append(f"File name: {summary.path.name}") + lines.append(f"Detected type: {summary.type}") + lines.append(f"Size (bytes): {summary.size:,}") + lines.append(f"Modified: {summary.modified.strftime('%Y-%m-%d %H:%M:%S')}") + lines.append(f"Line count: {summary.linecount:,}") + lines.append(f"SHA256: {summary.sha256}") + + if summary.parse_error: + lines.append("") + lines.append(f"Note: {summary.parse_error}") + + lines.append("") + lines.append(format_breadcrumb(0)) + + return lines + + +def format_structure(summary: FileSummary, structure: Dict[str, Any], grep_pattern: Optional[str] = None) -> List[str]: + """Format Level 1 structure output.""" + lines = [] + lines.append(format_header("STRUCTURE", 1, grep_pattern)) + lines.append("") + + if 'error' in structure: + lines.append(f"Parse error: {structure['error']}") + lines.append("") + lines.append("→ Try --level 3 for raw content") + return lines + + if summary.type == 'yaml': + lines.append("Top-level keys:") + for i, key in enumerate(structure.get('top_level_keys', []), 1): + lines.append(f" {format_line_number(i, len(structure.get('top_level_keys', [])))} {key}") + + lines.append("") + lines.append(f"Nesting depth: {structure.get('nesting_depth', 0)}") + lines.append(f"Anchors: {structure.get('anchor_count', 0)}") + lines.append(f"Aliases: {structure.get('alias_count', 0)}") + + elif summary.type == 'json': + lines.append("Top-level keys:") + for i, key in enumerate(structure.get('top_level_keys', []), 1): + lines.append(f" {format_line_number(i, len(structure.get('top_level_keys', [])))} {key}") + + lines.append("") + lines.append(f"Object count: {structure.get('object_count', 0)}") + lines.append(f"Array count: {structure.get('array_count', 0)}") + lines.append(f"Max depth: {structure.get('max_depth', 0)}") + + value_types = structure.get('value_types', {}) + if value_types: + lines.append("") + lines.append("Value types:") + for vtype, count in value_types.items(): + lines.append(f" {vtype}: {count}") + + elif summary.type == 'markdown': + headings = structure.get('headings', []) + lines.append(f"Headings (H1-H3): {len(headings)}") + for level, title in headings: + indent = " " * level + lines.append(f" {indent}H{level}: {title}") + + lines.append("") + lines.append(f"Paragraphs: {structure.get('paragraph_count', 0)}") + lines.append(f"Code blocks: {structure.get('code_block_count', 0)}") + lines.append(f"Lists: {structure.get('list_count', 0)}") + + elif summary.type == 'python': + imports = structure.get('imports', []) + if imports: + lines.append(f"Imports ({len(imports)}):") + for imp in imports[:10]: # Limit to first 10 + lines.append(f" {imp}") + if len(imports) > 10: + lines.append(f" ... and {len(imports) - 10} more") + + classes = structure.get('classes', []) + if classes: + lines.append("") + lines.append(f"Classes ({len(classes)}):") + for cls in classes: + lines.append(f" {cls}") + + functions = structure.get('functions', []) + if functions: + lines.append("") + lines.append(f"Functions ({len(functions)}):") + for func in functions: + lines.append(f" {func}") + + assignments = structure.get('assignments', []) + if assignments: + lines.append("") + lines.append(f"Top-level assignments ({len(assignments)}):") + for assign in assignments[:10]: + lines.append(f" {assign}") + + elif summary.type == 'text': + lines.append(f"Line count: {structure.get('line_count', 0)}") + lines.append(f"Word count: {structure.get('word_count', 0)}") + lines.append(f"Estimated type: {structure.get('estimated_type', 'text')}") + + lines.append("") + lines.append(format_breadcrumb(1)) + + return lines + + +def format_preview(summary: FileSummary, preview: List[tuple[int, str]], grep_pattern: Optional[str] = None) -> List[str]: + """Format Level 2 preview output.""" + lines = [] + lines.append(format_header("PREVIEW", 2, grep_pattern)) + lines.append("") + + max_line_num = max((ln for ln, _ in preview), default=1) + + for line_num, content in preview: + lines.append(f"{format_line_number(line_num, max_line_num)} {content}") + + lines.append("") + lines.append(format_breadcrumb(2)) + + return lines + + +def format_full_content(summary: FileSummary, lines_to_show: List[tuple[int, str]], grep_pattern: Optional[str] = None, is_end: bool = False) -> List[str]: + """Format Level 3 full content output.""" + output = [] + output.append(format_header("FULL CONTENT", 3, grep_pattern)) + output.append("") + + if not lines_to_show: + output.append("(no content)") + output.append("") + output.append(format_breadcrumb(3, is_end=True)) + return output + + max_line_num = max((ln for ln, _ in lines_to_show), default=1) + + for line_num, content in lines_to_show: + output.append(f"{format_line_number(line_num, max_line_num)} {content}") + + output.append("") + output.append(format_breadcrumb(3, is_end=is_end)) + + return output diff --git a/tools/progressive-reveal-cli/reveal/grep_filter.py b/tools/progressive-reveal-cli/reveal/grep_filter.py new file mode 100644 index 0000000..a3e8abc --- /dev/null +++ b/tools/progressive-reveal-cli/reveal/grep_filter.py @@ -0,0 +1,85 @@ +"""Grep filtering utilities.""" + +import re +from typing import List, Optional + + +def apply_grep_filter( + lines: List[tuple[int, str]], + pattern: str, + case_sensitive: bool = False, + context: int = 0 +) -> List[tuple[int, str]]: + """ + Apply grep-style filtering to lines. + + Args: + lines: List of (line_number, content) tuples + pattern: Regex pattern to match + case_sensitive: Whether to use case-sensitive matching + context: Number of context lines before/after matches + + Returns: + Filtered list of (line_number, content) tuples + """ + if not pattern: + return lines + + try: + flags = 0 if case_sensitive else re.IGNORECASE + regex = re.compile(pattern, flags) + except re.error as e: + raise ValueError(f"Invalid regex pattern: {e}") + + matching_indices = set() + + # Find matching lines + for i, (line_num, content) in enumerate(lines): + if regex.search(content): + matching_indices.add(i) + + # Add context + if context > 0: + expanded_indices = set() + for idx in matching_indices: + for i in range(max(0, idx - context), min(len(lines), idx + context + 1)): + expanded_indices.add(i) + matching_indices = expanded_indices + + # Return filtered lines in order + result = [lines[i] for i in sorted(matching_indices)] + + return result + + +def filter_structure_output( + structure_lines: List[str], + pattern: str, + case_sensitive: bool = False +) -> List[str]: + """ + Filter structure output lines by pattern. + + Args: + structure_lines: List of formatted structure output lines + pattern: Regex pattern to match + case_sensitive: Whether to use case-sensitive matching + + Returns: + Filtered list of lines + """ + if not pattern: + return structure_lines + + try: + flags = 0 if case_sensitive else re.IGNORECASE + regex = re.compile(pattern, flags) + except re.error as e: + raise ValueError(f"Invalid regex pattern: {e}") + + filtered = [] + for line in structure_lines: + if regex.search(line): + filtered.append(line) + + return filtered diff --git a/tools/progressive-reveal-cli/setup.py b/tools/progressive-reveal-cli/setup.py new file mode 100644 index 0000000..cc813c1 --- /dev/null +++ b/tools/progressive-reveal-cli/setup.py @@ -0,0 +1,46 @@ +"""Setup script for Progressive Reveal CLI.""" + +from setuptools import setup, find_packages +from pathlib import Path + +# Read README +readme_file = Path(__file__).parent / "README.md" +long_description = readme_file.read_text(encoding="utf-8") if readme_file.exists() else "" + +setup( + name="progressive-reveal-cli", + version="0.1.0", + author="Progressive Reveal Team", + description="A CLI tool for exploring files at different levels of detail", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/yourusername/progressive-reveal-cli", + packages=find_packages(), + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Topic :: Software Development :: Build Tools", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + ], + python_requires=">=3.8", + install_requires=[ + "pyyaml>=6.0", + ], + extras_require={ + "dev": [ + "pytest>=7.0", + "pytest-cov>=4.0", + ], + }, + entry_points={ + "console_scripts": [ + "reveal=reveal.cli:main", + ], + }, +)