Claude Skill

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

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download oaustegard-claude-skills-plugins_code-intelligence_skills_assessing-impact-e39c726.zip · 9 KB
Part of oaustegard/claude-skills — 39 skills

Install

skills CLI npx skills add https://github.com/oaustegard/claude-skills/tree/main/plugins/code-intelligence/skills/assessing-impact
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install oaustegard-claude-skills@llmmart
Git 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 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 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-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.
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.

No comments yet.

Reviews (0)

No reviews yet.

Related