feat(affected): transitive affected-files command for CI test selection (closes #62 phase-1)#142
Merged
Merged
Conversation
…on (closes #62 phase-1) ## What Issue #62 Phase 1 — adds `codelens affected` command that identifies which test files are transitively affected by a set of changed source files. The classic CI pattern: AFFECTED=$(git diff --name-only HEAD | codelens affected --stdin --quiet) pytest $AFFECTED Walks the reverse import-dependency graph (depth-controlled BFS) from each changed file and returns every test file that transitively imports one of them. ## Files added * `scripts/commands/affected.py` — thin CLI wrapper that registers the `affected` command. Accepts files as positional args OR via `--stdin` (pipe from `git diff --name-only`). Flags: `-d/--depth` (default 5), `-f/--filter <glob>`, `-j/--json`, `-q/--quiet`, `--include-source`. * `tests/test_affected_command.py` — 50 unit tests covering: - `is_test_file` heuristic (38 parametrized cases across Python, JS/TS, Go, Java, C#, plus edge cases) - transitive BFS resolution (direct, transitive, unrelated) - depth cap (0, 1, 5) - `--include-source` flag - `--filter` glob - absolute path / bare basename / ambiguous basename resolution - unresolved file handling - cycle safety (a↔b cycle, 200-node chain with depth=-1) - `affected_by_source` mapping - stdin input (with comments and blank lines) - `--quiet` mode (prints paths only) - command registration via `COMMAND_REGISTRY` ## Files modified * `scripts/dependents_engine.py`: - Added `is_test_file(path)` — conservative heuristic across 10+ ecosystems. Patterns matched: `test_*`, `*_test`, `*Test`/`*Tests` (Java), `*.test.*`/`*.spec.*` (JS/TS), `*_test.go`, files inside `tests/`/`__tests__`/`spec/` directories. Requires a source-code extension (`.py`, `.js`, `.ts`, `.go`, `.rs`, etc.) — bare `test` or `tests` (likely directories) are rejected. - Added `get_affected_files(changed_files, workspace, depth, filter, include_source)` — the engine. Depth-controlled BFS with a 5000-node safety cap to prevent runaway traversal on cyclic graphs. Returns `affected` (sorted list), `affected_by_source` (per-source mapping for traceability), `unresolved` (files that couldn't be resolved), and `stats`. - Extended `_resolve_python_module` with two new fallbacks specific to the CodeLens `sys.path.insert(0, SCRIPT_DIR)` convention: 5. Try `scripts/<module>.py` — handles `from utils import X` from `tests/foo.py` → `scripts/utils.py`. 6. Try `scripts/<first_seg>/<rest>.py` — handles `from commands.scan import X` from `tests/foo.py` → `scripts/commands/scan.py`. Without these fallbacks, the reverse-dependency graph misses test→script edges and `codelens affected` returns false negatives. * `scripts/codelens.py`: - Fixed a pre-existing latent argparse conflict: when a command defines its own `-f` shortcut (like `affected` does for `--filter`), the global `--format -f` add would crash with "conflicting option string: -f". Now the global add checks `existing_option_strings` and only adds the `-f` shortcut if the command hasn't claimed it. The `--format` long form is always added (no conflict possible). - Added `affected` to the usage docstring. * `README.md`, `SKILL-QUICK.md`, `SKILL.md`, `pyproject.toml`, `skill.json`, `scripts/graph_model.py` — regenerated by `python3 scripts/sync_command_count.py --apply` to reflect the new command count (70 total, 68 MCP tools = 54 static + 14 dynamic). ## Smoke test on CodeLens itself $ PYTHONPATH=scripts python3 -c " from dependents_engine import get_affected_files r = get_affected_files(['scripts/utils.py'], '.', depth=5) print(r['stats']['affected_count'], 'test files affected') " 42 test files affected Changing `scripts/utils.py` correctly flags 42 test files (including `tests/test_affected_command.py` itself) for re-run. ## Test results * New: 50/50 passing (`tests/test_affected_command.py`) * Full suite: 1413 passed, 87 skipped, 1 deselected — zero regressions vs the pre-#62 baseline on this branch. * The `_resolve_python_module` extension is backward-compatible: every existing resolution path is tried first; the new fallbacks only kick in when the old paths returned None. ## Definition of Done (Phase 1) - [x] `codelens affected [files...]` accepts files as positional args - [x] `--stdin` reads paths from stdin (pipe from `git diff --name-only`) - [x] `-d/--depth` (default 5), `-f/--filter <glob>`, `-j/--json`, `-q/--quiet` flags all implemented - [x] `dependents_engine.py` extended with transitive BFS - [x] `is_test_file(path)` heuristic implemented (10+ ecosystems) - [x] Cycle safety (5000-node cap) - [x] CI/hook pattern works: `AFFECTED=$(git diff --name-only | codelens affected --stdin --quiet); pytest $AFFECTED` ## Future phases (not in this PR) Phase 2 (symbolic editing tools), Phase 3 (LSP-based rename), Phase 4 (overload resolution), Phase 5 (framework-aware routes), Phase 6 (dynamic-dispatch synthesizers), Phase 7 (dynamic-boundary detection) are explicitly deferred. Phases 2-3 depend on the native LSP server (PR #135, issue #48) which has merged to main.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
5 tasks
|
Wolfvin
added a commit
that referenced
this pull request
Jul 3, 2026
Issue #173: PR #166 was written when main had 70 commands. After rebasing on latest main (which merged orient #165, affected #142, and others), the runtime COMMAND_REGISTRY now reports 77 commands. Ran sync_command_count.py --apply to regenerate all count references: - README.md: 5 substitutions - SKILL-QUICK.md: 5 substitutions - SKILL.md: 2 substitutions - pyproject.toml: 1 substitution - scripts/graph_model.py: 1 substitution - skill.json: 1 substitution Also resolved rebase conflicts in: - scripts/mcp_server.py: merged Phase 1 (staleness) + Phase 4 (worktree) banner methods — both coexist on the MCP server - scripts/sync/__init__.py: merged worktree (Phase 4) + pending (Phase 1) exports into unified __all__ Pre-existing finding (NOT caused by this PR): the 'affected' command (PR #142, already on main) uses -f for --filter, which conflicts with the global --format/-f flag added by codelens.py. This breaks ALL CLI invocations that trigger argparse registration (e.g. 'codelens affected --help'). Flagged for BOS — separate issue needed.
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Issue #62 Phase 1 — adds
codelens affectedcommand.What
Given a list of changed source files (positional args, or piped via
--stdinfromgit diff --name-only), walks the reverse import-dependency graph (depth-controlled BFS) and returns every test file that transitively imports one of the changed files.Classic CI pattern:
Files added
scripts/commands/affected.py— thin CLI wrapper. Flags:-d/--depth(default 5),-f/--filter <glob>,-j/--json,-q/--quiet,--include-source,--stdin.tests/test_affected_command.py— 50 unit tests covering is_test_file heuristic, transitive BFS, depth caps, filter glob, cycle safety, stdin input, quiet mode, command registration.Files modified
scripts/dependents_engine.py:is_test_file(path)— conservative heuristic across 10+ ecosystems (Python, JS/TS, Go, Java, C#, etc.).get_affected_files(changed_files, workspace, depth, filter, include_source)— depth-controlled BFS with 5000-node safety cap. Returnsaffected,affected_by_source,unresolved,stats._resolve_python_modulewith 2 new fallbacks for CodeLenssys.path.insert(0, SCRIPT_DIR)convention — handlesfrom utils import Xfrom tests/ → scripts/utils.py, andfrom commands.scan import X→ scripts/commands/scan.py. Without these, the reverse-dependency graph misses test→script edges.scripts/codelens.py:-fshortcut (likeaffecteddoes for--filter), the global--format -fadd would crash. Now checksexisting_option_stringsand skips the-fshortcut only for that command.--formatlong form always added.affectedto usage docstring.README.md,SKILL-QUICK.md,SKILL.md,pyproject.toml,skill.json,scripts/graph_model.py— regenerated bysync_command_count.py --apply(70 commands, 68 MCP tools).Smoke test
Test results
tests/test_affected_command.py)_resolve_python_moduleextension is backward-compatible: every existing resolution path is tried first; new fallbacks only kick in when old paths returned None.Definition of Done (Phase 1)
codelens affected [files...]accepts files as positional args--stdinreads paths from stdin (pipe fromgit diff --name-only)-d/--depth,-f/--filter,-j/--json,-q/--quietflags all implementeddependents_engine.pyextended with transitive BFSis_test_file(path)heuristic implemented (10+ ecosystems)AFFECTED=$(git diff --name-only | codelens affected --stdin --quiet); pytest $AFFECTEDFuture phases (deferred)
Phases 2-7 (symbolic editing, LSP rename, overload resolution, framework routes, dynamic-dispatch synthesizers, dynamic-boundary detection) are not in this PR. Phases 2-3 depend on the native LSP server (PR #135, issue #48) which has merged to main.