| name | tsa-refactor-queue |
| description | | Use when this capability is needed. |
tsa-refactor-queue — Top-N prioritized refactor slices
Three signals, one ranked list. Health × churn × dead-code → the five files
you'd refactor first if you had a week. Each row carries a target symbol, a
blast radius, and a concrete action.
When to use
- "What should we refactor next?" — daily/weekly engineering triage
- Post-feature cleanup pass — find rot that accumulated while shipping
- Pre-sprint planning — turn "we should clean up" into 5 concrete tickets
- After a CI grade-drop alert from
tsa-health-watch — re-rank with churn
Don't use when:
- You already know the file (one-file deep dive →
health action=file or tsa-edit-safety)
- The codebase is brand-new (<2 weeks of git history) — churn signal is noise
- You want to optimize one hot function — use
tsa-graph + edit action=refactor directly
- Shallow clone in CI —
git_state=shallow makes mod_count_30d unreliable
Procedure
Step 1 — Single fan-out (parallel, 3 MCP calls)
Call these in ONE message:
health action=project with min_grade: "D" and max_files: 20 — F/D files + per-file weakest_dimension
health action=dead with max_dead: 200 — symbol-level dead candidates, grouped by file
health action=heatmap with top_n: 20 — complexity-weighted file list (covers structural smell)
The three responses overlap on file path. Joining on file_path gives a
3-signal table per candidate.
Step 2 — Score and rank (deterministic, no LLM needed)
For each file that appears in health action=project worst_files, compute:
priority = (1 - health_score/100) # how bad is the grade
* log(1 + mod_count_30d_for_file) # how hot is the file
* (dead_symbol_count / total_symbols + 0.1)
Where:
health_score ∈ [0,100] from health action=project (lower → worse → bigger weight)
mod_count_30d_for_file = sum of mod_count_30d across the file's symbols
(read from ast_symbol_activation — see tsa-temporal). log(1+x) damps
pathological churn so a single 50× file doesn't dominate.
dead_symbol_count / total_symbols = fraction of symbols health action=dead
flagged. The + 0.1 floor ensures non-dead files can still rank if churn+grade
alone justify it.
Sort descending, keep top 5.
Step 3 — Enrich top 5 with target symbol + blast radius
For each of the top 5 files, fan out one more parallel batch:
structure action=analyze file_path=<f> format_type="compact"
→ list of symbols + complexity + line ranges → pick worst symbol
nav action=callers function_name=<worst_symbol> include_activation=true limit=50
→ blast_radius = len(callers) where callee_resolution in {local, project}
edit action=refactor file_path=<f>
→ action_hint: split | extract | delete | rename
Step 4 — Emit the queue
For each row in the top 5, produce:
- file: tree_sitter_analyzer/api.py
rank: 1
health_grade: F
health_score: 42
weakest_dimension: complexity
mod_count_30d: 18
dead_symbols: 4 / 67
target_symbol: api.analyze
blast_radius: 23
action: split
estimated_token_savings: ~8k
verification_command: uv run pytest tests/unit/test_api.py -q
Worked example — this very repo
Run on /Users/aisheng.yu/git-private/tree-sitter-analyzer:
uv run tree-sitter-analyzer --project-health --max-files 20 --output-format json > /tmp/health.json
uv run tree-sitter-analyzer --dead-code --output-format json > /tmp/dead.json
uv run python -c "
import sqlite3
sql = '''
SELECT s.file_path, SUM(a.mod_count_30d) AS churn
FROM ast_symbol_activation a
JOIN ast_symbol_rows s ON s.id = a.symbol_id
GROUP BY s.file_path ORDER BY churn DESC LIMIT 50'''
for r in sqlite3.connect('.ast-cache/index.db').execute(sql):
print(*r, sep='\t')
" > /tmp/churn.tsv
Expected shape after joining (truncated, illustrative — actual numbers vary
with the working tree state — last fixture run on feat/consolidated produced
something like this for the top three rows):
top_5_refactor_queue:
- rank: 1
file: tree_sitter_analyzer/api.py
health_grade: F
weakest_dimension: complexity
mod_count_30d: 18
dead_symbols: 3
target_symbol: api.analyze
blast_radius: 23
action: split
note: "called from CLI + MCP + tests — split into api_facade + api_core first"
- rank: 2
file: tree_sitter_analyzer/languages/python_plugin.py
health_grade: F
weakest_dimension: size
mod_count_30d: 11
dead_symbols: 7
target_symbol: PythonElementExtractor.extract
blast_radius: 6
action: extract
note: "650+ lines — gap report calls out Language Plugin extractors specifically"
- rank: 3
file: tree_sitter_analyzer/mcp/server.py
health_grade: D
weakest_dimension: dependencies
Read the queue right:
- Row 1 (api.py) matches the gap-report §5 callout — handle first.
- Row 2 (python_plugin.py) is a known "negative fixture" file (see CLAUDE.md
"Test Fixture Files") — extracting helpers is OK but renaming risks breaking
test_python_detects_deep_nesting. Run that test before/after.
- Row 3 (server.py) touches
BaseMCPTool adjacency — solo commit, macOS gate
check (see CLAUDE.md design-decision §2).
Choosing the action per row
The action column should drive how you ticket the refactor:
| action | when to pick | typical PR shape |
|---|
| split | weakest_dimension ∈ {size, complexity} and blast_radius >= 5 | extract helpers, no public-API change |
| extract | weakest_dimension = duplication or repeated patterns across files | new shared util module |
| delete | dead_symbols / total_symbols > 15% and weakest_dimension != git_hotspot | pure prune PR |
| rename | only if edit action=refactor returns rename_advised: true AND blast_radius < 10 | tiny solo PR |
If two actions tie, prefer delete first — it's the cheapest, lowest-risk PR
and shrinks the next queue automatically.
Anti-patterns
- DON'T act on the queue without re-checking each row with
tsa-edit-safety
before editing — the queue is prioritization, not a green light.
- DON'T bundle row 1 + row 3 in one PR if either touches
BaseMCPTool,
PathResolver, or SecurityValidator (CLAUDE.md "Foundational changes")
— solo commits only.
- DON'T re-rank every turn — cache the queue in memory and only refresh
after a commit that touches a file already in the queue, or weekly.
- DON'T trust the
mod_count_30d column when git_state=shallow (CI). The
CLI returns git_state per symbol — bail out of churn ranking and fall
back to health-only ranking if any row in the top 20 is shallow.
- DON'T re-add
.md files to the source set hoping for markdown smells — see
CLAUDE.md design-decision §4. Build a separate markdown_health tool instead.
CLI equivalents (if MCP unavailable)
uv run tree-sitter-analyzer --project-health --max-files 20 --output-format json
uv run tree-sitter-analyzer --dead-code --output-format json
uv run tree-sitter-analyzer --overview --output-format json
uv run python -c "
import sqlite3
sql = '''
SELECT s.file_path, SUM(a.mod_count_30d) AS churn_30d
FROM ast_symbol_activation a
JOIN ast_symbol_rows s ON s.id = a.symbol_id
WHERE a.git_state = 'tracked'
GROUP BY s.file_path
ORDER BY churn_30d DESC LIMIT 50'''
for r in sqlite3.connect('.ast-cache/index.db').execute(sql):
print(*r, sep='\t')
"
uv run tree-sitter-analyzer <file> --table --output-format json
uv run tree-sitter-analyzer --callers <SYMBOL> --output-format json
uv run tree-sitter-analyzer <file> --refactor --output-format json
The MCP-side ranking script (PRIORITY × log(churn+1) × dead_ratio) is
deterministic — implement it as a small Python helper if you run the CLI
flow repeatedly. Don't ask an LLM to do arithmetic over 20 rows.
Decision surface returned
verdict: INFO
queue_at: <iso-8601 timestamp>
queue_size: 5
top_5:
- rank: 1..5
file: <abs path>
health_grade: A | B | C | D | F
health_score: 0-100
weakest_dimension: complexity | structure | dependencies | duplication | size | git_hotspot
mod_count_30d: <int>
dead_symbols: <int>
total_symbols: <int>
target_symbol: <name>
blast_radius: <int>
action: split | extract
[, ]
Source: aimasteracc/tree-sitter-analyzer — distributed by TomeVault.