diff --git a/CHANGELOG.md b/CHANGELOG.md index 32de57c..d41dc1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- **post-commit hook no longer overwrites scan-all's hierarchical READMEs** + (GH #160). The hook re-rendered each affected directory via a per-dir + `codeindex scan` subprocess, which hardcodes `level="detailed"` and + `child_dirs=[]` — every commit touching a hub directory (e.g. + `src/codeindex/`) overwrote its 64-line navigation aggregate with a + 350-line full-subtree symbol dump, and the next manual `scan-all` flipped + it back (README_AI.md oscillated between two shapes). The hook now builds + one `DirectoryTree` and renders affected dirs through the same + tree-aware seam as `scan-all` (`_process_directory_with_smartwriter`), + making hook output byte-consistent with it — correct levels, the 0-symbol + skip, and stale-README cleanup (GH #158) included. Side benefits: newly + added source dirs now get a README from the hook (the old + `readme_path.exists()` guard skipped them), and the per-dir subprocess + spawn + 120s timeout is gone. + ### Added - **0-symbol directories no longer generate a README_AI.md** (GH #158 diff --git a/src/codeindex/README_AI.md b/src/codeindex/README_AI.md index 90c4573..c8e501b 100644 --- a/src/codeindex/README_AI.md +++ b/src/codeindex/README_AI.md @@ -1,5 +1,5 @@ - + # codeindex @@ -9,342 +9,63 @@ - **Files**: 93 - **Symbols**: 627 +- **Subdirectories**: 4 -## Files - -### __init__.py -_codeindex - AI-native code indexing tool for large codebases - -Usage: - codeindex scan # Scan a directory and generate README_AI.md - co_ - -**Functions:** -- `def _source_version(pyproject_path: Path | None = None) -> str | None` -- `def _resolve_version() -> str` - -### adaptive_config.py -_Adaptive symbols configuration. - -This module defines the configuration structure for adaptive symbol extraction, -which allows dynamically adjusting th_ - -**class** `class AdaptiveSymbolsConfig` -> Configuration for adaptive symbol extraction. - - Adaptive symbol extraction adjusts the number of - -### adaptive_selector.py -_Adaptive symbol selector for dynamic symbol limit calculation. - -This module implements the core algorithm for adaptive symbol extraction, -which adjust_ - -**class** `class AdaptiveSymbolSelector` -> Selects appropriate symbol limit based on file size. - - This selector implements a tiered approach - -**Methods:** -- `def calculate_limit(self, file_lines: int, total_symbols: int) -> int` -- `def _determine_size_category(self, lines: int) -> str` -- `def _apply_constraints(self, limit: int, total_symbols: int) -> int` - -### ai_helper.py -_AI enhancement helper functions (Epic 4 Story 4.1). - -This module provides reusable functions for AI enhancement operations, -eliminating code duplicati_ - -**Functions:** -- `def aggregate_parse_results( - parse_results: list[ParseResult], - path: Path, -) -> ParseResult` - -### claude_md.py -_CLAUDE.md management for codeindex. - -Handles injection, update, and version checking of codeindex sections -in project-level CLAUDE.md files. Uses mark_ - -**Functions:** -- `def _is_cjk(ch: str) -> bool` -- `def detect_locale(content: str) -> str` -- `def _get_current_version() -> str` -- `def _load_template(version: str, lang: str = "en") -> str` -- `def build_section(version: Optional[str] = None, lang: str = "en") -> str` -- `def extract_version(file_path: Path) -> Optional[str]` -- `def inject( - file_path: Path, - version: Optional[str] = None, - lang: Optional[str] = None, -) -> bool` -- `def check_outdated(project_dir: Optional[Path] = None) -> Optional[str]` - -### cli.py -_CLI entry point for codeindex. - -This module serves as the main entry point for the codeindex CLI tool. -It imports and registers commands from speciali_ - -**Functions:** -- `def main()` - -### cli_claude_md.py -_CLI commands for CLAUDE.md management._ - -**Functions:** -- `def claude_md()` -- `def update(project_dir: Path, lang: str)` -- `def status(project_dir: Path)` -- `def print_outdated_warning()` - -### cli_common.py -_Common utilities for CLI modules. - -This module provides shared resources used across all CLI command modules, -such as the Rich console instance for fo_ - -### cli_config.py -_CLI commands for configuration and project status. - -This module provides commands for initializing configuration files, -checking indexing status, and _ - -**Functions:** -- `def _update_gitignore(project_dir: Path) -> bool` -- `def _collect_init_targets(project_dir: Path) -> list[tuple[str, str, str]]` -- `def _print_post_init_message()` -- `def init(force: bool, yes: bool, quiet: bool, help_config: bool, lang: str, dry_run: bool = False)` -- `def status(root: Path)` -- `def list_dirs(root: Path)` -- `def doctor()` - -### cli_config_commands.py -_CLI commands for configuration help and explanation (Epic 15 Story 15.3). - -This module provides commands for: -- Explaining individual configuration pa_ - -**Functions:** -- `def config()` -- `def explain(parameter: str)` - -### cli_docs.py -_Documentation CLI commands for codeindex._ +## Subdirectories -**Functions:** -- `def docs()` -- `def show_ai_guide()` +- **extractors/** - Route extractor for Spring Framework. +- **parsers/** - 多语言代码解析 +- **templates/** - Module directory +- **writers/** - Result of writing a README file. -### cli_graph_export.py -_CLI graph-export command — write-once graph artifact for loomgraph (GH #102). -Standalone (Path A): does its own clean whole-tree parse and dumps a -wr_ - -**Functions:** -- `def graph_export(root: Path, output: str, quiet: bool)` - -### cli_hooks.py -_Git Hooks management module for codeindex. - -Epic 6, P3.1: Automate Git Hooks installation and management. - -This module provides: -- HookManager: Manage_ - -**class** `class HookStatus(Enum)` -> Status of a Git hook. - -**class** `class HookManager` -> Manage Git hooks for codeindex. - -**Methods:** -- `def _find_git_repo(self) -> Path` -- `def install_hook( - self, hook_name: str, backup: bool = True, force: bool = False - ) -> bool` -- `def _ensure_hook_common(self) -> None` -- `def uninstall_hook( - self, hook_name: str, restore_backup: bool = True - ) -> bool` -- `def list_all_hooks(self) -> dict[str, HookStatus]` - -**Functions:** -- `def generate_hook_script( - hook_name: str, config: Optional[dict] = None -) -> str` -- `def _generate_pre_commit_script(config: dict) -> str` -- `def _generate_post_commit_script(config: dict) -> str` -- `def _generate_pre_push_script(config: dict) -> str` -- `def backup_existing_hook(hook_path: Path) -> Path` -- `def detect_existing_hooks(hooks_dir: Path) -> list[str]` -- `def install_hook(hook_name: str, repo_path: Optional[Path] = None) -> bool` -- `def uninstall_hook(hook_name: str, repo_path: Optional[Path] = None) -> bool` - -_... and 8 more symbols_ - -### cli_parse.py -_CLI parse command - Parse a single source file and output JSON. - -Epic 12, Story 12.1: Single File Parse Command -This module provides the 'parse' comma_ - -**Functions:** -- `def parse(file_path: str)` - -### cli_scan.py -_CLI commands for scanning directories and generating README files. - -This module provides the core scanning functionality, including single directory -s_ - -**Functions:** -- `def _validate_scan_args(fallback: bool, dry_run: bool, ai: bool, quiet: bool) -> None` -- `def _validate_and_resolve_path(path: Path, output: str) -> Path` -- `def _load_and_prepare_config( - ai: bool, - parallel: int | None, - docstring_mode: str | None, -) -> tuple[Config, DocstringProcessor | None]` -- `def _scan_and_parse_directory( - path: Path, config: Config, quiet: bool, output: str -) -> list | None` -- `def _output_scan_json(parse_results: list) -> None` -- `def _generate_structural_readme( - path: Path, - parse_results: list, - config: Config, - docstring_processor: DocstringProcessor | None, - quiet: bool, - show_cost: bool, -) -> None` -- `def _generate_ai_readme( - path: Path, - parse_results: list, - config: Config, - dry_run: bool, - quiet: bool, - timeout: int, -) -> None` -- `def _validate_scanall_args(fallback: bool, quiet: bool) -> None` -- `def _load_scanall_config( - root: Path, - output: str, - parallel: int | None, - docstring_mode: str | None, -) -> tuple[Config, DocstringProcessor | None]` -- `def _output_scanall_json(root: Path, config: Config) -> None` -- `def _build_and_print_tree(root: Path, config: Config, quiet: bool) -> DirectoryTree` -- `def _process_directories_parallel( - dirs: list[Path], - tree: DirectoryTree, - config: Config, - docstring_processor: DocstringProcessor | None, - quiet: bool, - show_cost: bool, - ai: bool = False, - timeout: int = 120, - retry_all: bool = False, -) -> None` -- `def _build_enrich_summary( - dir_path: Path, - child_dirs: list[Path], - config: Config, -) -> str` -- `def _enrich_directories_with_ai( - dirs: list[Path], - tree: DirectoryTree, - config: Config, - quiet: bool, - timeout: int, - retry_all: bool = False, - enrichment_cache: dict[Path, str] | None = None, -) -> None` -- `def _print_enrichment_failure_hint(failed_dirs: list[str]) -> None` - -_... and 3 more symbols_ - -### cli_symbols.py -_CLI commands for symbol indexing and dependency analysis. - -This module provides commands for generating project-wide indices -and analyzing code depend_ - -**Functions:** -- `def extract_module_purpose( - dir_path: Path, - config: Config, - output_file: str = "README_AI.md" -) -> str` -- `def index(root: Path, output: str)` -- `def symbols(root: Path, output: str, quiet: bool)` -- `def affected(since: str, until: str, as_json: bool)` - -### cli_tech_debt.py -_CLI commands for technical debt analysis. - -This module provides the tech-debt command for analyzing technical debt -in a directory, including file size_ - -**Functions:** -- `def _find_source_files( - path: Path, recursive: bool, languages: list[str] | None = None -) -> list[Path]` -- `def _analyze_files( - files: list[Path], - detector: TechDebtDetector, - reporter: TechDebtReporter, - show_progress: bool, -) -> list[dict]` -- `def _get_file_type(file_path: Path) -> str` -- `def _create_scorer(parse_result, file_type: str) -> SymbolImportanceScorer` -- `def _analyze_single_file( - file_path: Path, - parse_result, - detector: TechDebtDetector, - reporter: TechDebtReporter, - test_smell_detector, -) -> list[dict]` -- `def _collect_test_smells(file_path: Path, parse_result, test_smell_detector) -> list[dict]` -- `def _handle_parse_error(file_path: Path, error: str, show_progress: bool)` -- `def _handle_analysis_error(file_path: Path, error: Exception, show_progress: bool)` -- `def _format_and_output( - report: TechDebtReport, - format: str, - output: Path | None, - quiet: bool, - test_smells: list[dict] | None = None, - target_path: Path | None = None, -) -> None` -- `def tech_debt(path: Path, format: str, output: Path | None, recursive: bool, quiet: bool)` - -### config.py -_Configuration management for codeindex._ - -**class** `class SymbolsConfig` -> Configuration for symbol extraction. - -**class** `class GroupingConfig` -> Configuration for file grouping. - -**class** `class SemanticConfig` -> Configuration for semantic extraction. - -**class** `class AIConfig` -> Direct HTTP AI backend (OpenAI-compatible ``/chat/completions``). - - Default provider is DeepSeek. - -**class** `class IndexingConfig` -> Configuration for smart indexing. - -**class** `class IncrementalConfig` -> Configuration for incremental updates. - -**class** `class +## Files ---- -_Content truncated due to size limit. See individual module README files for details._ +- **__init__.py** - _source_version, _resolve_version +- **adaptive_config.py** - AdaptiveSymbolsConfig +- **adaptive_selector.py** - AdaptiveSymbolSelector +- **ai_helper.py** - aggregate_parse_results +- **claude_md.py** - _is_cjk, detect_locale, _get_current_version +- **cli.py** - main +- **cli_claude_md.py** - claude_md, update, status +- cli_common.py +- **cli_config.py** - _update_gitignore, _collect_init_targets, _print_post_init_message +- **cli_config_commands.py** - config, explain +- **cli_docs.py** - docs, show_ai_guide +- **cli_graph_export.py** - graph_export +- **cli_hooks.py** - HookStatus, HookManager, generate_hook_script +- **cli_parse.py** - parse +- **cli_scan.py** - _validate_scan_args, _validate_and_resolve_path, _load_and_prepare_config +- **cli_symbols.py** - extract_module_purpose, index, symbols +- **cli_tech_debt.py** - _find_source_files, _analyze_files, _get_file_type +- **config.py** - SymbolsConfig, GroupingConfig, SemanticConfig +- **config_help.py** - show_full_config_help, _show_param_section, explain_parameter +- **directory_tree.py** - DirectoryNode, DirectoryTree +- **docstring_processor.py** - DocstringProcessor +- **doctor.py** - Finding, check_cli, check_project +- **enricher.py** - looks_like_refusal, extract_symbol_summary, extract_summary_from_readme +- **errors.py** - ErrorCode, ErrorInfo, create_error_response +- **file_classifier.py** - FileSizeCategory, FileSizeAnalysis, FileSizeClassifier +- **framework_detect.py** - RouteInfo, ModelInfo, FrameworkInfo +- **graph_buffer.py** - DirNode, GraphBuffer, render_directory +- **graph_export.py** - Entity, Edge, ExportModel +- **hierarchical.py** - DirectoryInfo, build_directory_hierarchy, create_processing_batches +- hooks.py +- **incremental.py** - UpdateLevel, FileChange, ChangeAnalysis +- **init_wizard.py** - WizardResult, check_parser_installed, get_parser_install_guidance +- **invoker.py** - InvokeResult, _is_transient, _retry_transient +- **objc_association.py** - ObjCFilePair, find_objc_pairs, parse_objc_pair +- **parallel.py** - BatchResult, parse_files_parallel, scan_directories_parallel +- **parser.py** - CallType, Call, Symbol +- **route_extractor.py** - ExtractionContext, RouteExtractor +- **route_registry.py** - RouteExtractorRegistry +- **scanner.py** - ScanResult, get_language_extensions, is_pass_through +- **semantic_extractor.py** - DirectoryContext, BusinessSemantic, SimpleDescriptionGenerator +- **skill_helpers.py** - detect_project_languages, detect_codeindex_config, detect_loomgraph_integration +- smart_writer.py +- **symbol_index.py** - SymbolEntry, GlobalSymbolIndex +- **symbol_scorer.py** - ScoringContext, SymbolImportanceScorer +- **tech_debt.py** - DebtSeverity, DebtIssue, DebtAnalysisResult +- **tech_debt_formatters.py** - ReportFormatter, ConsoleFormatter, MarkdownFormatter +- **test_smells.py** - SmellType, Smell, TestSmellDetector +- **writer.py** - WriteResult, format_symbols_for_prompt, format_imports_for_prompt diff --git a/src/codeindex/cli_hooks.py b/src/codeindex/cli_hooks.py index f7db95b..553b7f1 100644 --- a/src/codeindex/cli_hooks.py +++ b/src/codeindex/cli_hooks.py @@ -457,6 +457,13 @@ def run_post_commit_hook() -> int: `codeindex hooks run post-commit`. All logic lives here so that `pipx upgrade ai-codeindex` automatically updates the behavior. + Affected directories are re-rendered through the same tree-aware seam + as ``scan-all`` (GH #160): one writer, one world-view. The previous + per-dir ``codeindex scan`` subprocess hardcoded ``level="detailed"`` + and overwrote scan-all's overview/navigation READMEs, oscillating hub + directories between a 350-line symbol dump and a 64-line navigation + aggregate on every commit. + Returns: Exit code (0 = success) """ @@ -479,24 +486,33 @@ def run_post_commit_hook() -> int: if level == "skip" or not affected_dirs: return 0 - # Step 2: Run codeindex scan for each affected directory + # Step 2: Re-render affected directories through the tree-aware seam + # (same code path as scan-all, so hook output is byte-consistent with + # it — correct levels, 0-symbol skip, stale-README cleanup). repo_root = Path.cwd() - updated_readmes: list[str] = [] + from .cli_scan import _process_directory_with_smartwriter + from .config import Config + from .directory_tree import DirectoryTree + + try: + config = Config.load() + tree = DirectoryTree(repo_root, config) + except Exception: + return 0 + updated_readmes: list[str] = [] for dir_path in affected_dirs: - readme_path = repo_root / dir_path / "README_AI.md" - if not readme_path.exists(): + target = repo_root / dir_path + if not target.is_dir(): continue - try: - scan_result = subprocess.run( - ["codeindex", "scan", dir_path, "--quiet"], - capture_output=True, text=True, timeout=120, - ) - if scan_result.returncode == 0: - updated_readmes.append(str(readme_path)) - except (subprocess.TimeoutExpired, FileNotFoundError): - continue + # The seam isolates per-dir failures (try/except → result tuple). + _, success, _, _ = _process_directory_with_smartwriter( + target, tree, config + ) + readme_path = target / config.output_file + if success and readme_path.exists(): + updated_readmes.append(str(readme_path)) if not updated_readmes: return 0 diff --git a/tests/README_AI.md b/tests/README_AI.md index 668e24c..0432d37 100644 --- a/tests/README_AI.md +++ b/tests/README_AI.md @@ -1,5 +1,5 @@ - + # tests @@ -9,358 +9,120 @@ - **Files**: 149 - **Symbols**: 2438 +- **Subdirectories**: 5 -## Files - -### __init__.py - -### test_graphbuffer_baseline.py -_Characterization net for the GraphBuffer IR refactor (GH #101). - -This is the **safety net** for a refactor-under-net: it pins the current -(master) beh_ - -**Functions:** -- `def _normalize(text: str, root: Path) -> str` -- `def _collect_readmes(root: Path) -> str` -- `def _copy_fixture(tmp_path: Path) -> Path` -- `def _assert_or_update(name: str, actual: str) -> None` -- `def _run(args: list[str], cwd: Path) -> None` -- `def test_structural_readmes(tmp_path: Path) -> None` -- `def test_project_symbols(tmp_path: Path) -> None` -- `def test_enrich_prompts_and_frozen_ai_readmes(tmp_path: Path, monkeypatch) -> None` - -### test_graphbuffer_equivalence.py -_Run-both-diff equivalence proof for the GraphBuffer seam (GH #101). - -Phase 1 of the strangler refactor introduces ``GraphBuffer`` as an in-memory -IR b_ - -**Functions:** -- `def _load(root: Path) -> Config` -- `def _readmes(root: Path) -> dict[str, str]` -- `def _scan_parse(dir_path: Path, tree: DirectoryTree, config: Config)` -- `def _run_in(root: Path, fn) -> None` -- `def test_buffer_render_matches_direct_path(tmp_path: Path) -> None` - -### conftest.py -_Shared test fixtures and utilities for codeindex tests._ - -**Functions:** -- `def create_mock_symbol( - name: str = "test_function", - kind: str = "function", - signature: str = "def test_function():", - docstring: str = "A test function", - line_start: int = 1, - line_end: int = 10, -) -> Symbol` -- `def create_mock_parse_result( - file_path: str = "test.php", - file_lines: int = 300, - symbol_count: int = 15, - class_name: str | None = None, - methods_per_class: int = 0, - imports: list[Import] | None = None, - functions_count: int | None = None, - method_lines: int = 15, -) -> ParseResult` -- `def mock_config()` -- `def symbol_scorer(mock_config)` - -### __init__.py -_Tests for route extractors._ - -### test_spring.py -_Tests for Spring Framework route extractor. - -Story 7.2: Spring Route Extraction -Tests extraction of Spring REST routes from controllers: -- @RestContro_ - -**class** `class TestBasicRouteExtraction` -> Test basic Spring route extraction. - -**class** `class TestMultipleRoutes` -> Test multiple routes in one controller. - -**class** `class TestControllerAnnotation` -> Test @Controller vs @RestController. - -**class** `class TestEdgeCases` -> Test edge cases. - -**class** `class TestLineNumbers` -> Test route line number extraction. - -**Methods:** -- `def test_get_mapping(self, tmp_path)` -- `def test_post_mapping(self, tmp_path)` -- `def test_put_mapping(self, tmp_path)` -- `def test_delete_mapping(self, tmp_path)` -- `def test_crud_controller(self, tmp_path)` -- `def test_rest_controller(self, tmp_path)` -- `def test_controller_annotation(self, tmp_path)` -- `def test_no_controller_annotation(self, tmp_path)` -- `def test_empty_controller(self, tmp_path)` -- `def test_method_without_mapping(self, tmp_path)` - -_... and 1 more symbols_ - -### test_thinkphp.py -_Tests for ThinkPHP route extractor (Epic 6, Task 2.3)._ - -**class** `class TestThinkPHPRouteExtractor` -> Test ThinkPHP route extractor with new architecture. - -**Methods:** -- `def test_framework_name(self)` -- `def test_can_extract_in_controller_directory(self)` -- `def test_can_extract_in_non_controller_directory(self)` -- `def test_extract_routes_with_line_numbers(self)` -- `def test_extract_routes_multiple_controllers(self)` -- `def test_extract_routes_only_public_methods(self)` -- `def test_extract_routes_skip_magic_methods(self)` -- `def test_extract_routes_no_controller_class(self)` -- `def test_extract_routes_with_parse_error(self)` - -### test_thinkphp_description.py -_Tests for ThinkPHP route extractor description extraction (Epic 6, P2, Task 3.3)._ - -**class** `class TestThinkPHPDescriptionExtraction` -> Test ThinkPHP route extractor extracts descriptions from docstrings. - -**Methods:** -- `def test_extract_description_from_method_docstring(self)` -- `def test_extract_description_truncates_long_text(self)` -- `def test_extract_description_empty_for_no_docstring(self)` -- `def test_extract_description_from_multiple_methods(self)` - -### models.py -_Domain models._ - -**class** `class User` -> An application user. - -**Methods:** -- `def check_password(self, password: str) -> bool` - -### service.py -_User authentication service. - -Handles login, session creation and rate limiting._ - -**class** `class Session` -> A user session with an expiry timestamp. - -**class** `class AuthService` -> Authenticate users and issue sessions. - -**Methods:** -- `def authenticate(self, user: User, password: str) -> bool` -- `def create_session(self, user: User) -> Session` -- `def _mint(self, user: User) -> str` - -**Functions:** -- `def login(service: AuthService, user: User, password: str) -> Session | None` - -### helpers.py -_Small stateless helpers._ - -**Functions:** -- `def now_ts() -> int` -- `def clamp(value: int, low: int, high: int) -> int` - -### broken.py -_Parse error: Syntax error in source file_ - -### complete.py - -**class** `class Parent` -> Parent class - -**class** `class Child(Parent)` -> Child class - -**Methods:** -- `def method(self)` - -**Functions:** -- `def add(x, y)` - -### simple.py - -**class** `class Calculator` -> Simple calculator +## Subdirectories -**Methods:** -- `def multiply(self, a: int, b: int) -> int` +- **characterization/** - 2 files | 13 symbols +- **extractors/** - Test basic Spring route extraction. +- **fixtures/** - Test fixtures and sample data +- **legacy/** - Hierarchical structure testing +- **writers/** - 3 files | 63 symbols | classes: TestOverviewGenerator, TestNavigationGenerator, TestOverviewStatsAggregateFromChildren, TestDetailedGenerator, TestCollectRecursiveStats +1 more -**Functions:** -- `def add(a: int, b: int) -> int` -### service.py -_Auth service — exercises intra-file, cross-file, external, and INHERITS edges._ - -**class** `class AuthService` -> Authenticates users. - -**class** `class AdminService(AuthService)` -> Admin auth with extra capability. - -**Methods:** -- `def authenticate(self, token: str) -> bool` -- `def login(self, token: str) -> bool` -- `def cwd(self) -> str` - -### validators.py -_Standalone validators — cross-file CALLS target._ - -**Functions:** -- `def validate(token: str) -> bool` - -### workers.py -_Two same-named methods — exercises both AMBIGUOUS and UNRESOLVED resolution. - -``kickoff`` calls a BARE ``run()``: last-segment matches both ``Builder._ - -**class** `class Builder` - -**class** `class Packer` - -**Methods:** -- `def run(self) -> None` -- `def run(self) -> None` - -**Functions:** -- `def kickoff() -> None` -- `def dispatch(obj) -> None` - -### file1.py - -**Functions:** -- `def func1()` - -### file2.py - -**Functions:** -- `def func2()` - -### file4.py - -**Functions:** -- `def func4()` - -### file3.py - -**Functions:** -- `def func3()` - -### test_hierarchy_simple.py -_Test fixture generator for hierarchical processing. - -This script creates a simple directory hierarchy with Python files for testing -the hierarchical s_ - -### test_adaptive_config.py -_Tests for adaptive symbols configuration._ - -**class** `class TestAdaptiveSymbolsConfig` -> Test AdaptiveSymbolsConfig data class. - -**class** `class TestConfigurationValidation` -> Test configuration validation logic. - -**Methods:** -- `def test_default_config_exists(self)` -- `def test_default_config_disabled_by_default(self)` -- `def test_default_config_has_thresholds(self)` -- `def test_default_config_has_limits(self)` -- `def test_default_thresholds_are_increasing(self)` -- `def test_default_limits_are_increasing(self)` -- `def test_default_min_max_symbols(self)` -- `def test_custom_config_initialization(self)` -- `def test_config_with_partial_overrides(self)` -- `def test_thresholds_should_be_positive(self)` -- `def test_limits_should_be_positive(self)` -- `def test_min_symbols_should_be_positive(self)` -- `def test_max_symbols_should_be_reasonable(self)` - -_... and 6 more symbols_ - -### test_adaptive_selector.py -_Tests for AdaptiveSymbolSelector._ - -**class** `class TestAdaptiveSymbolSelectorBase` -> Test basic AdaptiveSymbolSelector functionality. - -**class** `class TestSizeCategoryDetermination` -> Test file size category determination. - -**Methods:** -- `def test_selector_initialization_with_default_config(self)` -- `def test_selector_initialization_with_custom_config(self)` -- `def test_calculate_limit_returns_int(self)` -- `def test_calculate_limit_returns_positive(self)` -- `def test_calculate_limit_not_exceed_total_symbols(self)` -- `def test_tiny_file_category(self)` -- `def test_tiny_boundary_99_lines(self)` -- `def test_small_file_category(self)` -- `def test_medium_file_category(self)` -- `def test_large_file_category(self)` -- `def test_xlarge_file_category(self)` -- `def test_huge_file_category(self)` -- `def test_mega_file_category(self)` - -_... and 22 more symbols_ - -### test_ai_helper.py -_Unit tests for AI enhancement helper functions (Epic 4 Story 4.1)._ - -**Functions:** -- `def test_aggregate_multiple_parse_results()` -- `def test_aggregate_single_parse_result()` -- `def test_aggregate_empty_parse_results()` -- `def test_aggregate_preserves_symbol_order()` - -### test_backward_compatibility.py -_Backward compatibility tests for Epic 6 refactoring (Task 2.5). - -These tests verify that the new route extractor architecture maintains -100% compatibi_ - -**class** `class TestBackwardCompatibility` -> Verify new architecture is 100% compatible with old implementation. - -**Methods:** -- `def test_extract_thinkphp_routes_still_works(self)` -- `def test_smart_writer_generates_same_output_structure(self, tmp_path)` -- `def test_route_table_format_unchanged(self, tmp_path)` - -### test_call_integration.py -_Story 11.4: Integration & JSON Output Tests - -Tests for call relationship extraction integration with ParseResult, -JSON serialization, and CLI integrat_ - -**class** `class TestJSONSerialization` -> AC1: JSON Output Format (3 tests) - -**class** `class TestParseResultIntegration` -> AC2: ParseResult Integration (3 tests) - -**class** `class TestBackwardCompatibility` -> AC3: Backward Compatibility (2 tests) - -**class** `class TestJSONRoundTrip` -> AC4: JSON Round-Trip (2 tests) - -**class** `class TestLanguageConsistency` -> AC5: Cross-Language Consistency (2 tests) +## Files -**Methods:** -- `def test_basic_json_structure(self, tmp_path)` -- `def te +- __init__.py +- **conftest.py** - create_mock_symbol, create_mock_parse_result, mock_config +- **test_adaptive_config.py** - TestAdaptiveSymbolsConfig, TestConfigurationValidation, TestExpectedDefaults +- **test_adaptive_selector.py** - TestAdaptiveSymbolSelectorBase, TestSizeCategoryDetermination, TestConstraintApplication +- **test_ai_helper.py** - test_aggregate_multiple_parse_results, test_aggregate_single_parse_result, test_aggregate_empty_parse_results +- **test_backward_compatibility.py** - TestBackwardCompatibility +- **test_call_integration.py** - TestJSONSerialization, TestParseResultIntegration, TestBackwardCompatibility +- **test_claude_md.py** - TestExtractVersion, TestBuildSection, TestInject +- **test_claude_md_injection.py** - TestInjectClaudeMd, TestHasClaudeMdInjection, project_dir +- **test_claude_md_locale.py** - TestDetectLocale, TestBuildSection, TestInitCliWiring +- **test_cli_config_gitignore.py** - TestUpdateGitignore +- **test_cli_debt_scan.py** - TestDebtScanCLI, TestDebtScanEdgeCases, TestDebtScanIntegrationWithRealProject +- **test_cli_docstring_options.py** - TestDocstringCLIOptions, TestDocstringHelp +- **test_cli_hooks.py** - TestHookManager, TestHookGeneration, TestBackupAndRestore +- **test_cli_json.py** - TestScanJSONOutput, TestScanAllJSONOutput, TestJSONErrorHandling +- **test_cli_parse.py** - TestCliParse +- **test_cli_scan_defaults_bdd.py** - InProjectDir, cli_runner, scan_context +- **test_cli_tech_debt.py** - TestTechDebtCommand, TestTechDebtIntegration, sample_files +- **test_config_adaptive.py** - TestSymbolsConfigAdaptive, TestConfigLoadingAdaptive, TestConfigurationMerging +- **test_config_ai_section.py** - TestAIConfig, TestConfigLoadsAISection +- **test_dataclass_structure.py** - TestInheritanceDataclass, TestImportWithAlias, TestParseResultWithInheritances +- **test_directory_tree.py** - _create_test_structure, test_directory_tree_build, test_directory_tree_levels +- **test_docstring_config.py** - TestDocstringConfig, TestBackwardCompatibility +- **test_docstring_processor.py** - TestDocstringProcessor +- **test_doctor.py** - TestCheckCli, TestCheckProject, TestCheckClaudeMd +- **test_enricher.py** - TestExtractSymbolSummary, TestExtractSummaryFromReadme, TestBuildEnrichPrompt +- **test_enricher_integration.py** - TestEnrichmentIntegration +- **test_error_handling.py** - TestCommandLevelErrors, TestErrorObjectStructure, TestFileLevelErrors +- **test_file_classifier.py** - test_classify_tiny_file, test_classify_small_file, test_classify_medium_file +- **test_graph_export.py** - TestContentHash, TestLanguageMismatchWarning, TestUnresolvedBreakdown +- **test_help_system_bdd.py** - help_context, cli_available, config_exists_with_param +- **test_hook_post_commit.py** - TestThinWrapperScript, TestRunPostCommitHook +- **test_hooks.py** - TestLegacyImports +- **test_hooks_config.py** - TestHooksConfig, TestPostCommitConfig +- **test_hooks_integration.py** - TestUpgradeScenario, TestMultipleUpgrades, TestCLICommand +- **test_hooks_rerun.py** - TestHooksRerun, _advertised +- **test_hooks_run_hidden.py** - TestHooksRunHidden, _advertised_subcommands +- **test_init_ai_section.py** - TestInitSeedsAISection, _minimal_result +- **test_init_dry_run.py** - TestInitDryRun +- **test_init_minimal_scope.py** - TestInitMinimalScope +- **test_init_non_tty.py** - TestInitNonTty +- **test_init_wizard_bdd.py** - wizard_context, project_directory, no_config_exists +- **test_integration_swift_objc.py** - TestMixedProjectParsing, TestFileAssociationAccuracy, TestRealisticProjectStructure +- **test_invoker_api.py** - TestInvokeAiApi, TestResolveAIBackend, TestInvokeAiDispatch +- **test_invoker_retry.py** - TestIsTransient, TestRetry, _ok +- **test_java_annotations.py** - TestClassAnnotations, TestMethodAnnotations, TestFieldAnnotations +- **test_java_calls.py** - TestBasicMethodCalls, TestConstructorCalls, TestStaticImportResolution +- **test_java_edge_cases.py** - TestNestedClasses, TestComplexGenerics, TestLongSignatures +- **test_java_error_recovery.py** - TestSyntaxErrors, TestIncompleteDeclarations, TestMalformedGenerics +- **test_java_generic_bounds.py** - TestSingleExtendsBound, TestMultipleBounds, TestWildcardBounds +- **test_java_inheritance.py** - TestBasicInheritance, TestGenericTypes, TestImportResolution +- **test_java_lambda.py** - TestSimpleLambda, TestLambdaWithParameters, TestBlockLambda +- **test_java_lombok.py** - TestBasicLombokAnnotations, TestConstructorAnnotations, TestBuilderAnnotation +- **test_java_module.py** - TestBasicModuleDeclaration, TestRequiresDirective, TestExportsDirective +- **test_java_parser.py** - TestJavaParserBasics, TestJavaSymbolExtraction, TestJavaImports +- **test_java_spring.py** - TestSpringControllerLayer, TestSpringServiceLayer, TestSpringRepositoryLayer +- **test_java_throws.py** - TestSingleThrows, TestMultipleThrows, TestGenericThrows +- **test_json_output.py** - TestSymbolSerialization, TestImportSerialization, TestParseResultSerialization +- **test_language_detect_dedup.py** - TestExtensionToLanguage, TestDetectProjectLanguagesDeduped +- **test_language_mismatch_hint.py** - TestLanguageMismatchHint, TestScanAllSurfacesMismatch, TestDiagnosticAccuracy +- **test_lazy_loading.py** - test_parser_module_does_not_import_all_languages, test_get_parser_lazy_loads_python_only, test_get_parser_caches_parsers +- **test_list_dirs_diagnostic.py** - TestListDirsLanguageMismatch, _write_config +- **test_loomgraph_integration.py** - TestLoomGraphJSONFormat, TestLoomGraphDataMapping, TestLoomGraphRealWorldExample +- **test_objc_association_utils.py** - TestFindObjCPairs, TestParseObjCPair, TestMergeObjCResults +- **test_parallel_scan.py** - TestParallelScanning, TestParallelPerformance, TestParallelCorrectness +- **test_parser.py** - test_parse_simple_function, test_parse_class_with_methods, test_parse_imports +- **test_parser_detection.py** - TestCheckParserInstalled, TestParserInstallGuidance, TestInitWizardPostMessage +- **test_parser_objc_association.py** - TestBasicAssociation, TestSymbolMerging, TestMissingPairs +- **test_parser_objc_basic.py** - TestInterfaceDeclarations, TestImplementationParsing, TestImportStatements +- **test_parser_objc_bridging.py** - TestBridgingHeaderDetection, TestBridgingHeaderImports, TestBridgingHeaderClasses +- **test_parser_objc_categories.py** - TestProtocolDeclarations, TestCategoryDeclarations, TestCategoryAssociation +- **test_parser_partial_recovery.py** - test_partial_recovery_keeps_valid_symbol, test_clean_file_not_partial +- **test_parser_swift_docstrings.py** - TestSwiftSingleLineDocComments, TestSwiftMultiLineDocComments, TestSwiftDocstringAssociation +- **test_parser_swift_extensions.py** - TestBasicExtensions, TestProtocolConformanceExtensions, TestConstrainedExtensions +- **test_parser_swift_generics.py** - TestGenericTypeParameters, TestGenericConstraints, TestAssociatedTypes +- **test_parser_swift_inheritance.py** - TestSwiftClassInheritance, TestSwiftProtocolConformance, TestSwiftMixedInheritance +- **test_parser_swift_integration.py** - TestSwiftIntegration, TestSwiftPerformance, TestSwiftEndToEnd +- **test_parser_swift_poc.py** - TestSwiftParserPOC, TestSwiftParserRealWorld +- **test_parser_swift_properties.py** - TestSwiftStoredProperties, TestSwiftComputedProperties, TestSwiftPropertyWrappers +- **test_parser_swift_property_wrappers.py** - TestCommonPropertyWrappers, TestParameterizedWrappers, TestMultipleWrappers +- **test_parser_swift_protocols.py** - TestSwiftProtocolDeclarations, TestSwiftProtocolInheritance, TestSwiftClassProtocolConformance +- **test_parser_swift_signatures.py** - TestAccessModifiers, TestGenericParameters, TestFunctionParameters +- **test_php_calls.py** - TestBasicFunctionCalls, TestMethodCalls, TestConstructorCalls +- **test_php_comment_extraction.py** - TestPHPCommentExtraction +- **test_php_docstring_extraction.py** - TestPHPDocstringExtraction, TestSmartWriterIntegration, TestPHPParserEdgeCases +- **test_php_import_alias.py** - TestPHPImportAliasBasic, TestPHPImportAliasGroupImports, TestPHPImportAliasNamespace +- **test_php_inheritance.py** - TestPHPInheritanceBasic, TestPHPInheritanceNamespace, TestPHPInheritanceModifiers +- **test_php_loomgraph_integration.py** - TestPHPLoomGraphJSONFormat, TestPHPLoomGraphRealWorld, TestPHPLoomGraphEdgeCases +- **test_project_index_semantic.py** - TestExtractModulePurpose, TestProjectIndexGeneration, TestBDDProjectIndex +- **test_python_calls.py** - TestBasicFunctionCalls, TestMethodCalls, TestConstructorCalls +- **test_python_docstring_description.py** - TestPythonDocstringDescription +- **test_python_import_alias.py** - TestImportAsBasic, TestFromImportAs, TestComplexScenarios +- **test_python_inheritance.py** - TestSingleInheritance, TestMultipleInheritance, TestNoInheritance +- **test_route_extractor.py** - TestExtractionContext, TestRouteExtractor +- **test_route_info.py** - TestRouteInfoLineNumber +- **test_route_registry.py** - TestFrameworkExtractor, AnotherFrameworkExtractor, TestRouteExtractorRegistry +- **test_route_table_description.py** - TestRouteTableDescription +- **test_route_table_display.py** - TestRouteTableDisplay +- **test_scanall_auto_ai.py** - TestScanAllAutoAI, cli_runner, project_with_ai_command +- **test_scanner_passthrough.py** - TestIsPassThrough, TestDirectoryTreePassthrough +- **test_scanner_swift_objc_extensions.py** - TestScannerKnow --- _Content truncated due to size limit. See individual module README files for details._ diff --git a/tests/test_hook_post_commit.py b/tests/test_hook_post_commit.py index c8131bc..3418b44 100644 --- a/tests/test_hook_post_commit.py +++ b/tests/test_hook_post_commit.py @@ -47,11 +47,28 @@ def test_has_codeindex_marker(self): class TestRunPostCommitHook: - """Python-side post-commit logic.""" + """Python-side post-commit logic (tree-aware since GH #160).""" + + @staticmethod + def _affected_json(dirs): + import json as _json + return _json.dumps({"level": "affected", "affected_dirs": dirs}) + + @staticmethod + def _fake_subprocess(affected_dirs, staged_changes=False): + """Dispatch subprocess.run: affected query answered, git ops succeed.""" + def fake_run(cmd, *args, **kwargs): + if "affected" in cmd: + return MagicMock(returncode=0, + stdout=TestRunPostCommitHook._affected_json(affected_dirs)) + if "diff" in cmd: # git diff --cached --quiet: 1 = has changes + return MagicMock(returncode=1 if staged_changes else 0, stdout="") + return MagicMock(returncode=0, stdout="") + return fake_run @patch("codeindex.cli_hooks.subprocess.run") def test_skips_when_no_affected_dirs(self, mock_run): - """No affected dirs → no scan, no commit.""" + """No affected dirs → no render, no commit.""" mock_run.return_value = MagicMock( returncode=0, stdout='{"level": "skip", "affected_dirs": []}', @@ -61,48 +78,65 @@ def test_skips_when_no_affected_dirs(self, mock_run): @patch("codeindex.cli_hooks.Path.cwd") @patch("codeindex.cli_hooks.subprocess.run") - def test_scans_affected_directories(self, mock_run, mock_cwd, tmp_path): - """Affected dirs → codeindex scan for each.""" - # Create the README_AI.md so the check passes - auth_dir = tmp_path / "src" / "auth" - auth_dir.mkdir(parents=True) - (auth_dir / "README_AI.md").write_text("# Auth\n") + def test_hub_dir_renders_navigation_not_detailed(self, mock_run, mock_cwd, tmp_path): + """GH #160 regression: a dir with indexed children must keep its + navigation-level README — the old per-dir `codeindex scan` subprocess + hardcoded detailed and overwrote scan-all's hierarchy.""" + (tmp_path / "src" / "auth" / "sub").mkdir(parents=True) + (tmp_path / "src" / "auth" / "__init__.py").write_text("def a():\n pass\n") + (tmp_path / "src" / "auth" / "sub" / "mod.py").write_text("def b():\n pass\n") mock_cwd.return_value = tmp_path + mock_run.side_effect = self._fake_subprocess(["src/auth"]) - diff_mock = MagicMock(returncode=1, stdout="") # has changes - mock_run.side_effect = [ - MagicMock( - returncode=0, - stdout='{"level": "minor", "affected_dirs": ["src/auth"]}', - ), - MagicMock(returncode=0, stdout=""), # scan - MagicMock(returncode=0, stdout=""), # git add - diff_mock, # git diff --cached --quiet (1 = has changes) - MagicMock(returncode=0, stdout="abc123"), # git rev-parse - MagicMock(returncode=0, stdout=""), # git commit - ] + run_post_commit_hook() + + content = (tmp_path / "src" / "auth" / "README_AI.md").read_text() + assert "(navigation)" in content + + @patch("codeindex.cli_hooks.Path.cwd") + @patch("codeindex.cli_hooks.subprocess.run") + def test_new_dir_without_readme_gets_one(self, mock_run, mock_cwd, tmp_path): + """New dirs (no prior README) are rendered, not skipped — the old + `readme_path.exists()` guard left freshly added source dirs unindexed.""" + pkg = tmp_path / "src" / "newpkg" + pkg.mkdir(parents=True) + (pkg / "mod.py").write_text("def f():\n pass\n") + mock_cwd.return_value = tmp_path + mock_run.side_effect = self._fake_subprocess(["src/newpkg"]) run_post_commit_hook() - # Should have called codeindex scan for the affected dir - scan_calls = [ - c for c in mock_run.call_args_list - if "scan" in str(c) - ] - assert len(scan_calls) >= 1 + assert (pkg / "README_AI.md").exists() + @patch("codeindex.cli_hooks.Path.cwd") @patch("codeindex.cli_hooks.subprocess.run") - def test_no_ai_prompt_in_scan(self, mock_run): - """Scan should use `codeindex scan`, not custom AI prompts.""" - mock_run.return_value = MagicMock( - returncode=0, - stdout='{"level": "minor", "affected_dirs": ["src/mod"]}', - ) + def test_zero_symbol_dir_stale_readme_removed(self, mock_run, mock_cwd, tmp_path): + """0-symbol skip (GH #158) is inherited from the shared seam.""" + empty = tmp_path / "src" / "empty" + empty.mkdir(parents=True) + (empty / "__init__.py").write_text("") + (empty / "README_AI.md").write_text("# stale\n") + mock_cwd.return_value = tmp_path + mock_run.side_effect = self._fake_subprocess(["src/empty"]) + + run_post_commit_hook() + + assert not (empty / "README_AI.md").exists() + + @patch("codeindex.cli_hooks.Path.cwd") + @patch("codeindex.cli_hooks.subprocess.run") + def test_no_codeindex_scan_subprocess(self, mock_run, mock_cwd, tmp_path): + """Rendering is in-process now — no per-dir `codeindex scan` spawn.""" + pkg = tmp_path / "src" / "pkg" + pkg.mkdir(parents=True) + (pkg / "mod.py").write_text("def f():\n pass\n") + mock_cwd.return_value = tmp_path + mock_run.side_effect = self._fake_subprocess(["src/pkg"]) run_post_commit_hook() - # Check no call contains AI prompt keywords for call in mock_run.call_args_list: - cmd = str(call) - assert "PROMPT" not in cmd - assert "Code Diff" not in cmd + cmd = call.args[0] + # tmp_path itself contains "scan" (pytest dir naming) — match the + # invocation shape, not a substring. + assert not (cmd[0].endswith("codeindex") and len(cmd) > 1 and cmd[1] == "scan")