assessing-impact
Pre-change blast-radius report for a symbol or file. Walks tree-sitting references, augments with a plain-text scan over non-parsed files (configs, plain docs), and clusters affected sites by feature (`_FEATURES.md`) or top-level package. Use when about to refactor, rename, or de
Install
npx skills add https://github.com/oaustegard/claude-skills/tree/main/plugins/code-intelligence/skills/assessing-impact
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install oaustegard-claude-skills@llmmart
git clone https://github.com/oaustegard/claude-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole oaustegard/claude-skills collection as a plugin from our marketplace. Git is the plain clone.
README
assessing-impact
Pre-change blast-radius reports for a symbol or file in a local codebase. Walks the tree-sitting AST cache for direct references, augments with a plain-text scan over file types tree-sitting doesn't parse, and clusters affected sites by package, tests, docs, and (when present) _FEATURES.md features.
Features
- Symbol or file targets — identify what breaks if you change
validateUseror refactorauth.py - AST-aware ref discovery — uses tree-sitting's parsed corpus, excluding definition lines so refs are uses, not declarations
- Non-AST text scan — picks up
Dockerfile,.ini,.env,.properties, plain.txt/.rstreferences that the AST scanner misses - Three-axis clustering — code-by-package, tests, and docs, with optional
_FEATURES.mdoverlay - Test surface suggestion — surfaces tests that already reference the target plus tests neighboring the definition
- No opinionated risk score — structured report is input for the LLM that writes the final summary
- Substring-fallback warning — flags when the target had no exact match so the resulting noise is visible
- Markdown or JSON output
Dependency
Requires the tree-sitting skill — imports engine.py directly and uses its bundled grammars.
Relationship to Other Skills
- exploring-codebases — divergent first-encounter EDA; run before
assessing-impactif the repo also needs an_FEATURES.md - searching-codebases — convergent "where is X" search; complementary, not a replacement for impact analysis
- tree-sitting — drill into specific call sites once impact has identified them
- featuring — produces the
_FEATURES.mdfiles thatassessing-impactoverlays for feature-area clustering
Honest Limits
Text-based ref discovery, no type/MRO resolution, no cross-language or cross-repo tracing, no persistent index. For deep ongoing impact analysis on a daily codebase, GitNexus or SourceGraph remain the right tool. This skill is for the ad-hoc case: "I'm about to refactor X in a repo I don't own, what should I be careful about?"
Skill manifest
Assessing Impact
Cheap, ad-hoc impact analysis for a single target. Not a graph database — a focused walk over an AST cache plus a complementary text scan, clustered into a report that's easy to summarize.
Use this when you're about to refactor / rename / delete a symbol in a repo you don't work in daily, and you want a single artifact that says: "these N files will need to change, in these M packages, with these tests likely affected."
Don't use this for deep ongoing impact analysis on your own codebase — stand up GitNexus, SourceGraph, or your IDE's index. This skill is for the one-shot case.
Setup
uv venv /home/claude/.venv 2>/dev/null
uv pip install --python /home/claude/.venv/bin/python tree-sitter
export PYTHON=/home/claude/.venv/bin/python
export IMPACT=/mnt/skills/user/assessing-impact/scripts/impact.py
The script depends on the tree-sitting skill — it imports engine.py
directly. The bundled grammars live with tree-sitting; no separate
language-pack install needed.
Workflow
1. Run the report
$PYTHON $IMPACT /path/to/repo SYMBOL_NAME
Or target a whole file:
$PYTHON $IMPACT /path/to/repo path/to/module.py
2. Read the data, write the summary
The script prints a structured markdown report. Treat it as input for your final summary, not the deliverable. It deliberately doesn't assign a "high/medium/low" risk label — that's your job, after weighing:
- Refs concentrated in one package (low blast) vs. fanned across many (high)
- Test refs present (good — the change has a verification surface) vs. absent
- Doc mentions (renames need to update docs too)
- Caveats listed at the bottom (what the script can't see)
3. Drill if needed
If a particular package looks suspicious, follow up with tree-sitting
to read the actual call sites:
TREESIT=/mnt/skills/user/tree-sitting/scripts/treesit.py
$PYTHON $TREESIT /path/to/repo --no-tree 'source:caller_function'
Options
| Flag | Default | Purpose |
|---|---|---|
--features PATH |
_FEATURES.md |
Root _FEATURES.md — when present, refs get clustered by feature in addition to by package. |
--skip DIRS |
(defaults from tree-sitting) | Extra comma-separated dirs to skip. |
--limit-per-name N |
500 | Cap refs per symbol name. Bump if you suspect truncation. |
--json |
off | Emit JSON instead of markdown — for downstream tooling. |
Output Sections
# Impact Report: <target>
## Target
Kind, definition sites with line ranges.
## Direct & Textual References (N total)
Top-line counts, then refs grouped by:
- Code references by package
- Test references
- Documentation mentions
## Affected Features (from _FEATURES.md) ← only if file present
Feature name → ref count + file count.
## Suggested Test Surfaces
Test files that already reference the target, plus tests neighboring
the definition. Likely the regression net for the change.
## Caveats
What the scan can't see (dynamic dispatch, cross-language, cross-repo).
Composition with Other Skills
- Run after
exploring-codebasesif the repo also has a freshly generated_FEATURES.md— the impact report will cluster refs by feature, which makes the blast radius story much more legible than raw package directories. - Use
tree-sittingto drill specific call sites once impact has identified them. - Use
searching-codebaseswhen you want regex/AST search over the same corpus rather than impact analysis on a known target.
Honest Limits
- Text-based ref discovery. Refs are matched by symbol name, not by
type-resolved call edges. Common names (
run,init,handler) will pick up unrelated symbols. Prefer running this on distinctive names; otherwise expect noise and read the snippets. - No type/MRO resolution. Dynamic dispatch (
getattr, duck-typed method calls, virtual dispatch in C++) is missed or over-matched. - No cross-language tracing. A TS frontend calling a Python backend handler over HTTP appears as zero refs — they're not in the same AST.
- No cross-repo tracing. Consumers in separate repos (downstream packages, sibling services) are invisible. For multi-repo impact, reach for GitNexus / SourceGraph.
- No persistent index. Each run re-scans. Fine for single-shot use; acceptable cost (~700ms scan + sub-ms queries) for a few hundred files.
- Diff input not yet supported. v0.1 takes a symbol or file path. Diff → affected-symbols extraction is a planned follow-up.
Files
scripts/impact.py— Single-entry CLI. Resolves target → walks AST refs → augments with text scan → clusters by package and (optionally) by feature → renders markdown or JSON.
Files (claude-skills)
-
scripts
-
impact.py 17 KB
#!/usr/bin/env python3 """ impact.py — Pre-change blast-radius report for a symbol or file. Given a target (symbol name or file path) in a local codebase, walks the tree-sitting AST cache to find direct references, augments with a plain-text scan of non-parsed file types (configs, plain docs), and clusters the affected sites by feature (using `_FEATURES.md`) or by top-level directory. Output: a structured markdown report intended as input to an LLM that will synthesize the final risk summary. No opinionated risk score — the data is presented; judgment is the LLM's job. Usage: python impact.py /path/to/repo SYMBOL python impact.py /path/to/repo path/to/file.py python impact.py /path/to/repo SYMBOL --features _FEATURES.md python impact.py /path/to/repo SYMBOL --skip tests,vendor --json """ import argparse import json import os import re import sys from collections import defaultdict from pathlib import Path # Plain-text extensions that tree-sitting doesn't parse but may mention symbols. NON_AST_EXTS = { '.txt', '.rst', '.cfg', '.ini', '.env', '.properties', '.dockerfile', '.makefile', '.mk', '.example', '.sample', } NON_AST_FILENAMES = { 'Dockerfile', 'Makefile', 'Procfile', 'Caddyfile', '.env', '.gitignore', '.dockerignore', } # Files that document but rarely "use" a symbol — separate them in the report. DOC_EXTS = {'.md', '.rst', '.txt', '.adoc'} # Test path heuristics — these refs matter for "what tests exercise this?" TEST_PATH_PATTERNS = re.compile(r'(^|/)(tests?|__tests__|spec|specs)(/|$)', re.IGNORECASE) TEST_FILE_PATTERN = re.compile(r'(^|/)(test_|_test\.|\.test\.|\.spec\.)', re.IGNORECASE) def _find_treesit_engine() -> Path | None: """Locate tree-sitting engine across known skill directories.""" candidates = [ Path(__file__).resolve().parent.parent.parent / 'tree-sitting' / 'scripts', Path('/mnt/skills/user/tree-sitting/scripts'), Path('/mnt/skills/public/tree-sitting/scripts'), ] for p in candidates: if (p / 'engine.py').exists(): return p return None def setup_engine(): """Import tree-sitting engine. Exits on failure.""" engine_path = _find_treesit_engine() if engine_path is None: print("ERROR: tree-sitting skill not found. Install it first.", file=sys.stderr) sys.exit(2) sys.path.insert(0, str(engine_path)) try: from engine import CodeCache return CodeCache() except ImportError as e: print(f"ERROR: tree-sitting engine import failed: {e}", file=sys.stderr) sys.exit(2) def is_test_path(relpath: str) -> bool: """Heuristic: does this path look like a test?""" return bool(TEST_PATH_PATTERNS.search(relpath) or TEST_FILE_PATTERN.search(relpath)) def is_doc_path(relpath: str) -> bool: return Path(relpath).suffix.lower() in DOC_EXTS def resolve_target(cache, target: str) -> dict: """Resolve a target string to symbols and their definition sites. Returns: { 'kind': 'symbol' | 'file', 'name': str, 'symbols': [Symbol, ...], # for kind='symbol': matches; for 'file': all symbols in file 'def_sites': [{'file', 'line_start', 'line_end', 'kind'}, ...] } """ target_path = Path(target) is_file = ( '/' in target or target_path.suffix or any(target == relpath or target == relpath.split('/')[-1] for relpath in cache.files) ) if is_file: symbols = cache.file_symbols(target) if not symbols: return {'kind': 'file', 'name': target, 'symbols': [], 'def_sites': []} def_sites = [ {'file': s.file, 'line_start': s.line, 'line_end': s.end_line, 'kind': s.kind, 'name': s.name} for s in symbols ] return {'kind': 'file', 'name': target, 'symbols': symbols, 'def_sites': def_sites} matches = cache.find_symbol(target, limit=50) exact = [s for s in matches if s.name == target] syms = exact if exact else matches def_sites = [ {'file': s.file, 'line_start': s.line, 'line_end': s.end_line, 'kind': s.kind, 'name': s.name} for s in syms ] return { 'kind': 'symbol', 'name': target, 'symbols': syms, 'def_sites': def_sites, 'exact_match': bool(exact), } def find_ast_refs(cache, names: list[str], def_sites: list[dict], limit_per_name: int = 500) -> list[dict]: """Use tree-sitting's text-scan refs over its parsed corpus. Excludes the definition lines themselves so refs are USES, not declarations. """ def_set = {(d['file'], line) for d in def_sites for line in range(d['line_start'], d['line_end'] + 1)} refs = [] seen = set() for name in names: for r in cache.references(name, limit=limit_per_name): key = (r['file'], r['line']) if key in def_set or key in seen: continue seen.add(key) refs.append({ 'file': r['file'], 'line': r['line'], 'text': r['text'], 'symbol': name, 'source': 'ast-cache', }) return refs def find_text_refs(repo: Path, names: list[str], skip: set[str], already_scanned: set[str], limit: int = 500) -> list[dict]: """Plain-text scan of files NOT in the AST cache (configs, plain docs). Looks for whole-word occurrences of each name. Returns matches outside `already_scanned` (the set of cache-known relpaths). """ if not names: return [] pattern = re.compile(r'\b(' + '|'.join(re.escape(n) for n in names) + r')\b') refs = [] for dirpath, dirnames, filenames in os.walk(repo): dirnames[:] = [d for d in dirnames if d not in skip and not d.startswith('.')] for fn in filenames: fp = Path(dirpath) / fn try: relpath = str(fp.relative_to(repo)) except ValueError: continue if relpath in already_scanned: continue ext = fp.suffix.lower() if not (ext in NON_AST_EXTS or ext in DOC_EXTS or fn in NON_AST_FILENAMES): continue try: text = fp.read_text(errors='replace') except (OSError, UnicodeDecodeError): continue for i, line in enumerate(text.split('\n'), 1): m = pattern.search(line) if m: refs.append({ 'file': relpath, 'line': i, 'text': line.strip()[:140], 'symbol': m.group(1), 'source': 'text-scan', }) if len(refs) >= limit: return refs return refs def cluster_refs(refs: list[dict]) -> dict: """Group references into buckets: tests, docs, code-by-package.""" buckets: dict = { 'code': defaultdict(list), # package/top-dir -> refs 'tests': [], 'docs': [], } for r in refs: relpath = r['file'] if is_test_path(relpath): buckets['tests'].append(r) elif is_doc_path(relpath): buckets['docs'].append(r) else: parts = relpath.split('/') pkg = parts[0] if len(parts) > 1 else '(root)' buckets['code'][pkg].append(r) return buckets def parse_features_file(path: Path) -> list[dict]: """Parse a _FEATURES.md and return [{'name', 'files': set[str]}, ...]. Files are extracted from `file#symbol` refs in each feature section. """ if not path.exists(): return [] text = path.read_text(errors='replace') ref_pattern = re.compile(r'`([^`]+?)#[^`]+?`') features = [] current = None for line in text.split('\n'): if line.startswith('## ') and not line.startswith('### '): if current: features.append(current) current = {'name': line[3:].strip(), 'files': set()} elif current: for m in ref_pattern.finditer(line): current['files'].add(m.group(1)) if current: features.append(current) return [f for f in features if f['files']] def map_refs_to_features(refs: list[dict], features: list[dict]) -> dict: """Return {feature_name: [refs]}. Refs that match no feature are excluded.""" out: dict = defaultdict(list) for r in refs: for f in features: if r['file'] in f['files']: out[f['name']].append(r) return dict(out) def find_test_files_for_target(cache, target_name: str, def_files: set[str]) -> list[str]: """Find test files that reference the target — likely test surfaces.""" test_files = set() for r in cache.references(target_name, limit=200): if is_test_path(r['file']): test_files.add(r['file']) # Also: tests living next to the def files for df in def_files: parent = str(Path(df).parent) for relpath in cache.files: if is_test_path(relpath) and relpath.startswith(parent): test_files.add(relpath) return sorted(test_files) def render_report(target: dict, refs_by_source: dict, features_map: dict, test_surfaces: list[str], stats: dict) -> str: """Render the markdown report.""" lines = [f"# Impact Report: {target['name']}\n"] lines.append("## Target") lines.append(f"- Kind: `{target['kind']}`") if target['kind'] == 'symbol' and not target.get('exact_match', True): lines.append(f"- ⚠ No exact match for `{target['name']}` — falling back " "to substring matches across multiple symbols. " "Refs below are the union; expect noise. Re-run with a " "more specific name to narrow.") if target['def_sites']: lines.append(f"- Definition sites ({len(target['def_sites'])}):") for d in target['def_sites'][:10]: lines.append(f" - `{d['file']}:{d['line_start']}-{d['line_end']}` " f"({d['kind']}: `{d['name']}`)") if len(target['def_sites']) > 10: lines.append(f" - ... and {len(target['def_sites']) - 10} more") else: lines.append("- No definition found in scanned corpus.") lines.append("") ast_refs = refs_by_source.get('ast-cache', []) text_refs = refs_by_source.get('text-scan', []) all_refs = ast_refs + text_refs clustered = cluster_refs(all_refs) n_code = sum(len(v) for v in clustered['code'].values()) lines.append(f"## Direct & Textual References ({len(all_refs)} total)") lines.append(f"- Code refs: **{n_code}** across **{len(clustered['code'])}** " f"top-level package(s)") lines.append(f"- Test refs: **{len(clustered['tests'])}**") lines.append(f"- Doc refs: **{len(clustered['docs'])}**") lines.append(f"- Of which from AST cache: {len(ast_refs)}; from plain-text scan: {len(text_refs)}") lines.append("") if clustered['code']: lines.append("### Code references by package") for pkg in sorted(clustered['code'].keys()): pkg_refs = clustered['code'][pkg] lines.append(f"\n**{pkg}/** — {len(pkg_refs)} refs in " f"{len(set(r['file'] for r in pkg_refs))} files") for r in pkg_refs[:15]: lines.append(f"- `{r['file']}:{r['line']}` — {r['text']}") if len(pkg_refs) > 15: lines.append(f"- ... and {len(pkg_refs) - 15} more") lines.append("") if clustered['tests']: lines.append("### Test references") for r in clustered['tests'][:20]: lines.append(f"- `{r['file']}:{r['line']}` — {r['text']}") if len(clustered['tests']) > 20: lines.append(f"- ... and {len(clustered['tests']) - 20} more") lines.append("") if clustered['docs']: lines.append("### Documentation mentions") for r in clustered['docs'][:15]: lines.append(f"- `{r['file']}:{r['line']}` — {r['text']}") if len(clustered['docs']) > 15: lines.append(f"- ... and {len(clustered['docs']) - 15} more") lines.append("") if features_map: lines.append("## Affected Features (from `_FEATURES.md`)") for name, frefs in sorted(features_map.items(), key=lambda kv: -len(kv[1])): files = sorted(set(r['file'] for r in frefs)) lines.append(f"- **{name}** — {len(frefs)} refs across {len(files)} file(s)") lines.append("") if test_surfaces: lines.append("## Suggested Test Surfaces") for tf in test_surfaces[:20]: lines.append(f"- `{tf}`") if len(test_surfaces) > 20: lines.append(f"- ... and {len(test_surfaces) - 20} more") lines.append("") lines.append("## Caveats") lines.append("- **Text-based ref discovery.** Matches are by symbol name. " "Common names (e.g. `run`, `init`) will produce noisy matches " "from unrelated symbols with the same identifier.") lines.append("- **No type/MRO resolution.** Method calls dispatched on a " "dynamic type (e.g. `obj.foo()` where `obj`'s class isn't " "knowable statically) may be missed or over-matched.") lines.append("- **No cross-language tracing.** A TS frontend calling a " "Python backend handler over HTTP won't be linked.") lines.append("- **Single-repo only.** Anything in a separate repo " "(consumer, vendored package) is invisible.") lines.append("- **Corpus**: " + (", ".join(stats.get('languages', [])) or "(none)") + f" — {stats.get('files', 0)} parsed files, " f"{stats.get('symbols', 0)} symbols.") return '\n'.join(lines) def main(): parser = argparse.ArgumentParser( description="Pre-change blast-radius report for a symbol or file.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__, ) parser.add_argument('repo', help="Path to the repository root.") parser.add_argument('target', help="Symbol name or file path.") parser.add_argument('--features', default='_FEATURES.md', help="Path to root _FEATURES.md (default: _FEATURES.md).") parser.add_argument('--skip', default='', help="Comma-separated extra dirs to skip during scan.") parser.add_argument('--limit-per-name', type=int, default=500, help="Max refs per symbol name (default: 500).") parser.add_argument('--json', action='store_true', help="Emit a JSON document instead of markdown.") args = parser.parse_args() repo = Path(args.repo).resolve() if not repo.is_dir(): print(f"ERROR: not a directory: {repo}", file=sys.stderr) sys.exit(2) cache = setup_engine() from engine import DEFAULT_SKIP skip_set = set(DEFAULT_SKIP) if args.skip: skip_set.update(s.strip() for s in args.skip.split(',') if s.strip()) stats = cache.scan(str(repo), skip=skip_set) target = resolve_target(cache, args.target) if not target['symbols']: print(f"ERROR: target '{args.target}' not found as symbol or file in " f"{stats.get('files', 0)} parsed files.", file=sys.stderr) sys.exit(1) names = sorted({s.name for s in target['symbols']}) ast_refs = find_ast_refs(cache, names, target['def_sites'], limit_per_name=args.limit_per_name) text_refs = find_text_refs(repo, names, skip_set, already_scanned=set(cache.files.keys()), limit=args.limit_per_name * len(names)) features_path = repo / args.features features = parse_features_file(features_path) features_map = map_refs_to_features(ast_refs + text_refs, features) if features else {} primary_name = target['name'] if target['kind'] == 'symbol' else names[0] def_files = {d['file'] for d in target['def_sites']} test_surfaces = find_test_files_for_target(cache, primary_name, def_files) refs_by_source = {'ast-cache': ast_refs, 'text-scan': text_refs} if args.json: out = { 'target': { 'kind': target['kind'], 'name': target['name'], 'def_sites': target['def_sites'], }, 'refs': { 'ast_cache': ast_refs, 'text_scan': text_refs, }, 'clusters': { pkg: refs for pkg, refs in cluster_refs(ast_refs + text_refs)['code'].items() }, 'features': {name: refs for name, refs in features_map.items()}, 'test_surfaces': test_surfaces, 'stats': stats, } print(json.dumps(out, indent=2, default=str)) else: print(render_report(target, refs_by_source, features_map, test_surfaces, stats)) if __name__ == '__main__': main()
-
-
CHANGELOG.md 407 B
# assessing-impact - Changelog All notable changes to the `assessing-impact` skill are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [0.1.1] - 2026-04-26 ### Other - assessing-impact: add README, bump to 0.1.1 (#582) ## [0.1.0] - 2026-04-26 ### Other - assessing-impact: new skill for pre-change blast-radius reports (closes #578) (#581) -
README.md 2.1 KB
# assessing-impact Pre-change blast-radius reports for a symbol or file in a local codebase. Walks the `tree-sitting` AST cache for direct references, augments with a plain-text scan over file types tree-sitting doesn't parse, and clusters affected sites by package, tests, docs, and (when present) `_FEATURES.md` features. ## Features - **Symbol or file targets** — identify what breaks if you change `validateUser` or refactor `auth.py` - **AST-aware ref discovery** — uses tree-sitting's parsed corpus, excluding definition lines so refs are uses, not declarations - **Non-AST text scan** — picks up `Dockerfile`, `.ini`, `.env`, `.properties`, plain `.txt`/`.rst` references that the AST scanner misses - **Three-axis clustering** — code-by-package, tests, and docs, with optional `_FEATURES.md` overlay - **Test surface suggestion** — surfaces tests that already reference the target plus tests neighboring the definition - **No opinionated risk score** — structured report is input for the LLM that writes the final summary - **Substring-fallback warning** — flags when the target had no exact match so the resulting noise is visible - **Markdown or JSON output** ## Dependency Requires the **tree-sitting** skill — imports `engine.py` directly and uses its bundled grammars. ## Relationship to Other Skills - **exploring-codebases** — divergent first-encounter EDA; run before `assessing-impact` if the repo also needs an `_FEATURES.md` - **searching-codebases** — convergent "where is X" search; complementary, not a replacement for impact analysis - **tree-sitting** — drill into specific call sites once impact has identified them - **featuring** — produces the `_FEATURES.md` files that `assessing-impact` overlays for feature-area clustering ## Honest Limits Text-based ref discovery, no type/MRO resolution, no cross-language or cross-repo tracing, no persistent index. For deep ongoing impact analysis on a daily codebase, GitNexus or SourceGraph remain the right tool. This skill is for the ad-hoc case: *"I'm about to refactor X in a repo I don't own, what should I be careful about?"* -
SKILL.md 5.4 KB
--- name: assessing-impact description: >- Pre-change blast-radius report for a symbol or file. Walks tree-sitting references, augments with a plain-text scan over non-parsed files (configs, plain docs), and clusters affected sites by feature (`_FEATURES.md`) or top-level package. Use when about to refactor, rename, or delete something in a repo you don't own — "what breaks if I change `validateUser`", "who calls this", "is this safe to remove", "where is this used", "blast radius", "impact analysis". This is the CONVERGENT pre-change risk skill — for "what is this repo?" use exploring-codebases; for "where is X?" use searching-codebases. metadata: version: 0.1.1 --- # Assessing Impact Cheap, ad-hoc impact analysis for a single target. Not a graph database — a focused walk over an AST cache plus a complementary text scan, clustered into a report that's easy to summarize. **Use this when** you're about to refactor / rename / delete a symbol in a repo you don't work in daily, and you want a single artifact that says: "these N files will need to change, in these M packages, with these tests likely affected." **Don't use this for** deep ongoing impact analysis on your own codebase — stand up GitNexus, SourceGraph, or your IDE's index. This skill is for the one-shot case. ## Setup ```bash uv venv /home/claude/.venv 2>/dev/null uv pip install --python /home/claude/.venv/bin/python tree-sitter export PYTHON=/home/claude/.venv/bin/python export IMPACT=/mnt/skills/user/assessing-impact/scripts/impact.py ``` The script depends on the `tree-sitting` skill — it imports `engine.py` directly. The bundled grammars live with `tree-sitting`; no separate language-pack install needed. ## Workflow ### 1. Run the report ```bash $PYTHON $IMPACT /path/to/repo SYMBOL_NAME ``` Or target a whole file: ```bash $PYTHON $IMPACT /path/to/repo path/to/module.py ``` ### 2. Read the data, write the summary The script prints a structured markdown report. Treat it as **input** for your final summary, not the deliverable. It deliberately doesn't assign a "high/medium/low" risk label — that's your job, after weighing: - Refs concentrated in one package (low blast) vs. fanned across many (high) - Test refs present (good — the change has a verification surface) vs. absent - Doc mentions (renames need to update docs too) - Caveats listed at the bottom (what the script can't see) ### 3. Drill if needed If a particular package looks suspicious, follow up with `tree-sitting` to read the actual call sites: ```bash TREESIT=/mnt/skills/user/tree-sitting/scripts/treesit.py $PYTHON $TREESIT /path/to/repo --no-tree 'source:caller_function' ``` ## Options | Flag | Default | Purpose | |------|---------|---------| | `--features PATH` | `_FEATURES.md` | Root `_FEATURES.md` — when present, refs get clustered by feature in addition to by package. | | `--skip DIRS` | (defaults from tree-sitting) | Extra comma-separated dirs to skip. | | `--limit-per-name N` | 500 | Cap refs per symbol name. Bump if you suspect truncation. | | `--json` | off | Emit JSON instead of markdown — for downstream tooling. | ## Output Sections ``` # Impact Report: <target> ## Target Kind, definition sites with line ranges. ## Direct & Textual References (N total) Top-line counts, then refs grouped by: - Code references by package - Test references - Documentation mentions ## Affected Features (from _FEATURES.md) ← only if file present Feature name → ref count + file count. ## Suggested Test Surfaces Test files that already reference the target, plus tests neighboring the definition. Likely the regression net for the change. ## Caveats What the scan can't see (dynamic dispatch, cross-language, cross-repo). ``` ## Composition with Other Skills - **Run after `exploring-codebases`** if the repo also has a freshly generated `_FEATURES.md` — the impact report will cluster refs by feature, which makes the blast radius story much more legible than raw package directories. - **Use `tree-sitting` to drill** specific call sites once impact has identified them. - **Use `searching-codebases`** when you want regex/AST search over the same corpus rather than impact analysis on a known target. ## Honest Limits - **Text-based ref discovery.** Refs are matched by symbol name, not by type-resolved call edges. Common names (`run`, `init`, `handler`) will pick up unrelated symbols. Prefer running this on distinctive names; otherwise expect noise and read the snippets. - **No type/MRO resolution.** Dynamic dispatch (`getattr`, duck-typed method calls, virtual dispatch in C++) is missed or over-matched. - **No cross-language tracing.** A TS frontend calling a Python backend handler over HTTP appears as zero refs — they're not in the same AST. - **No cross-repo tracing.** Consumers in separate repos (downstream packages, sibling services) are invisible. For multi-repo impact, reach for GitNexus / SourceGraph. - **No persistent index.** Each run re-scans. Fine for single-shot use; acceptable cost (~700ms scan + sub-ms queries) for a few hundred files. - **Diff input not yet supported.** v0.1 takes a symbol or file path. Diff → affected-symbols extraction is a planned follow-up. ## Files - `scripts/impact.py` — Single-entry CLI. Resolves target → walks AST refs → augments with text scan → clusters by package and (optionally) by feature → renders markdown or JSON.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.