Skip to content

feat(affected): transitive affected-files command for CI test selection (closes #62 phase-1)#142

Merged
Wolfvin merged 1 commit into
mainfrom
feat/issue-62-affected-command-phase1
Jul 3, 2026
Merged

feat(affected): transitive affected-files command for CI test selection (closes #62 phase-1)#142
Wolfvin merged 1 commit into
mainfrom
feat/issue-62-affected-command-phase1

Conversation

@Wolfvin

@Wolfvin Wolfvin commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Issue #62 Phase 1 — adds codelens affected command.

What

Given a list of changed source files (positional args, or piped via --stdin from git 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:

AFFECTED=$(git diff --name-only HEAD | codelens affected --stdin --quiet)
pytest $AFFECTED

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:
    • Added is_test_file(path) — conservative heuristic across 10+ ecosystems (Python, JS/TS, Go, Java, C#, etc.).
    • Added get_affected_files(changed_files, workspace, depth, filter, include_source) — depth-controlled BFS with 5000-node safety cap. Returns affected, affected_by_source, unresolved, stats.
    • Extended _resolve_python_module with 2 new fallbacks for CodeLens sys.path.insert(0, SCRIPT_DIR) convention — handles from utils import X from tests/ → scripts/utils.py, and from commands.scan import X → scripts/commands/scan.py. Without these, the reverse-dependency graph misses test→script edges.
  • scripts/codelens.py:
    • Fixed latent argparse conflict: when a command defines its own -f shortcut (like affected does for --filter), the global --format -f add would crash. Now checks existing_option_strings and skips the -f shortcut only for that command. --format long form always added.
    • Added affected to usage docstring.
  • README.md, SKILL-QUICK.md, SKILL.md, pyproject.toml, skill.json, scripts/graph_model.py — regenerated by sync_command_count.py --apply (70 commands, 68 MCP tools).

Smoke test

$ 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

Test results

  • New: 50/50 passing (tests/test_affected_command.py)
  • Full suite: 1413 passed, 87 skipped, 1 deselected — zero regressions
  • The _resolve_python_module extension 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
  • --stdin reads paths from stdin (pipe from git diff --name-only)
  • -d/--depth, -f/--filter, -j/--json, -q/--quiet flags all implemented
  • dependents_engine.py extended with transitive BFS
  • is_test_file(path) heuristic implemented (10+ ecosystems)
  • Cycle safety (5000-node cap)
  • CI pattern works: AFFECTED=$(git diff --name-only | codelens affected --stdin --quiet); pytest $AFFECTED

Future 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.

…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.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@sonarqubecloud

sonarqubecloud Bot commented Jul 1, 2026

Copy link
Copy Markdown

@Wolfvin Wolfvin merged commit bdcd805 into main Jul 3, 2026
5 of 11 checks passed
@Wolfvin Wolfvin deleted the feat/issue-62-affected-command-phase1 branch July 3, 2026 02:14
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant