From hotspot-analysis
Use to produce a deterministic, reproducible, ranked prioritization of a Java codebase's REST API endpoints (and methods) for test generation — especially as the input that decides which RestAssured API tests to write first. Drives this repo's Java CLI over a local git working tree, combining recency-weighted git churn, SonarQube-style cognitive complexity, and JaCoCo coverage gap into a Composite Hotspot Score, and emits a machine-readable ranking (CSV/YAML/Markdown/HTML) plus a CI gating exit code. Method-level Java, hunk-accurate, no LLM guesswork. Based on Adam Tornhill's "Your Code as a Crime Scene".
How this skill is triggered — by the user, by Claude, or both
Slash command
/hotspot-analysis:hotspot-analysisThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill is a **thin wrapper around a Java CLI**. The jar does the analysis
This skill is a thin wrapper around a Java CLI. The jar does the analysis deterministically; the skill's job is to build it, configure it, run it, and hand the ranked, machine-readable output to whatever generates tests (typically RestAssured API tests, or unit tests for the top methods).
What makes it worth a separate step (vs. asking a model to "guess the risky endpoints"):
--strict returns a non-zero exit code on empty results.The primary deliverable is a priority queue of REST API endpoints: for each endpoint you get its HTTP method + route, the aggregated risk over its whole call graph, the call graph itself, and a coverage signal — i.e. exactly what an agent needs to decide which endpoint to test first and which under-tested path to target.
Not for: non-Java repos; deciding test content (it ranks what to test, the test generator decides how).
Target must be a directory containing a real .git/ folder. Phase 1 runs
local-git end-to-end; github target needs a local clone (see Troubleshooting).
| Need | Requirement |
|---|---|
| Get the jar | Nothing to build — scripts/get-jar.sh downloads the released fat jar (cached). Building from source instead needs any JDK 17+. |
| Run the jar | JDK 21+ on PATH — verify java -version reports 21 or later |
| Analysis target | A directory with a .git/ folder |
| API analysis (recommended) | apiAnalysis.enabled: true; ideally classpathDirectories for symbol resolution |
| Coverage signal (recommended) | A JaCoCo XML report from the same build |
A convenience wrapper, scripts/run-analysis.sh,
resolves the jar (downloading the released fat jar if needed) and runs analyze.
The steps below show the explicit form.
Get the jar. No build required — the resolver downloads the released fat jar to a cache (or reuses a local build, or builds from source as a fallback):
JAR="$(skills/hotspot-analysis/scripts/get-jar.sh)" # prints the jar path
# manual alternative (no clone needed):
# curl -fsSL https://github.com/baekchangjoon/hotspot-analysis/releases/latest/download/hotspot.jar -o hotspot.jar
# JAR=hotspot.jar
# from-source alternative (needs the repo + a JDK): ./gradlew bootJar
No JDK at all? Use the Docker image, mounting the target repo at /work:
docker run --rm -v "$PWD":/work ghcr.io/baekchangjoon/hotspot-analysis:latest analyze --config /work/hotspot.yml
Generate a config.
java -jar "$JAR" init -o hotspot.yml
Configure for endpoint prioritization. Point analysis.target.path at
the target repo, enable API analysis, and (if available) supply a JaCoCo
report. If the user can't hand-write the YAML, run the interview below
("Configure interactively") — ask, fill
defaults, write the file. See Config reference for every key. The key block:
analysis:
apiAnalysis:
enabled: true
sharedComponentMode: BOTH # CUMULATIVE | SEPARATE | BOTH
classpathDirectories: # optional but improves call-graph resolution
- build/libs
jacocoReportPath: build/reports/jacoco/test/jacocoTestReport.xml
output:
formats: [yaml, md, html]
apiLayout: BOTH # COMBINED | STANDALONE | BOTH
topN: 30
Analyze (add --strict in CI).
java -jar "$JAR" analyze --config hotspot.yml --strict
# or, in one shot (resolves the jar for you):
# skills/hotspot-analysis/scripts/run-analysis.sh hotspot.yml --strict
Outputs land in output.path. With API analysis on and apiLayout: BOTH:
hotspot-report/
├── api_report.yml ← STANDALONE: apiHotspots + sharedComponents (agent input)
├── hotspots.yml ← COMBINED: file + method + api + shared in one doc
├── hotspots.md / .html ← human-readable
├── file_hotspots.csv
└── method_hotspots.csv
Consume the ranking for RestAssured. Read api_report.yml and iterate
apiHotspots in compositeRank order. Field-by-field schema:
references/api-report-schema.md. Each row
carries:
| Field | Use for test generation |
|---|---|
httpMethod, route | The request: given()...when().<method>(route) |
fqcn, method, parameters | Controller signature → request body/param shape |
callGraph | Reachable methods → which downstream logic the endpoint exercises |
coverageMultiplier / lineCoverage | How under-tested the endpoint's logic is |
compositeRank | The order to write tests in |
sharedComponents[] | Methods many endpoints depend on — high-leverage to cover once |
Generate tests highest-rank first, targeting the least-covered paths in each endpoint's call graph. Do not fabricate the ranking — run the CLI and read the actual file.
A freshly-installed user usually can't write hotspot.yml cold. Don't make
them. Generate a starting file (init), then fill it by Q&A: auto-detect what
you can, ask only what's ambiguous, confirm, and write the file.
Procedure:
Detect first, then ask. Inspect the target repo to pre-fill defaults so most questions become a yes/no confirmation:
grep -rl "@RestController\|@RequestMapping" <repo>/src → if
hits, default apiAnalysis.enabled: true.src/main/java root → include both globs.**/jacoco/**/*.xml (e.g.
build/reports/jacoco/test/jacocoTestReport.xml) → default jacocoReportPath.build/libs, build/classes → default
apiAnalysis.classpathDirectories.git -C <repo> log -1 --format=%cd → if older than a
year, propose absolute window.since/until instead of days.Ask, one decision at a time (skip any you confidently detected — just state the default and let the user correct it):
| # | Question | Maps to | Default |
|---|---|---|---|
| 1 | Which repo to analyze? (path to the .git/ working tree) | analysis.target.path | — (required) |
| 2 | Prioritize REST API endpoints for test generation? | apiAnalysis.enabled | true if Spring detected |
| 3 | Count churn over the last N days, or an absolute date range? | window.days or window.since/until | days: 365 |
| 4 | File-level, method-level, or both? | scope.granularity | [file, method] |
| 5 | Have a JaCoCo coverage report? Where? | analysis.jacocoReportPath | detected path, else omit |
| 6 | Dirs with built classes/dep jars (improves call graph)? | apiAnalysis.classpathDirectories | detected, else [] |
| 7 | Shared-method handling? | apiAnalysis.sharedComponentMode | BOTH |
| 8 | Output formats / where / how many rows? | output.formats/path/topN | [csv,yaml,md,html], ./hotspot-report, 30 |
| 9 | Fail the run if the result is empty (CI)? | pass --strict at run time | no |
Write hotspot.yml from the answers, show it back to the user for a
final OK, then run step 4. If a tool like AskUserQuestion is available,
prefer it for crisp multiple-choice prompts; otherwise ask in plain text.
Keep it short: a typical session is "confirm repo path → confirm Spring/API on → accept window default → confirm the detected JaCoCo path → go".
Four input factors → two scores. Full per-granularity derivations (with source
references and worked examples) live in docs/scoring/:
file ·
method ·
REST API endpoint ·
shared component.
| Factor / score | Definition |
|---|---|
| Revisions | Commits in the window that touched the artifact (method: diff-hunk overlap) |
| Recency Decay | Σ exp(-ln(2)·Δt / halfLife) over those commits — recent weighs more |
| Cognitive Complexity | SonarQube-style AST walk (file = sum of its methods) |
| Coverage Multiplier | 1/(lineCoverage + 0.1) from JaCoCo; 1.0 if no report |
| Simple Score | Revisions × LOC (Tornhill's original) |
| Composite Score | Cognitive Complexity × Recency Decay × Coverage Multiplier |
For an API endpoint, each factor is aggregated over the controller method
plus its whole call graph; coverage is the average over those methods.
Sorted by Composite DESC, ties broken deterministically (route,httpMethod).
analysis:
target:
type: local-git # local-git | github (Phase 1 CLI: local-git end-to-end)
path: /path/to/target/repo # must contain a .git/ folder
window:
days: 365 # Mode A: relative window from now
# since: "2024-01-01" # Mode B: absolute ISO range (use INSTEAD of days)
# until: "2026-01-01"
scope:
granularity: [file, method]
include:
- "src/main/java/**/*.java" # single-module repos
- "**/src/main/java/**/*.java" # multi-module repos (list both if unsure)
exclude:
- "**/generated/**"
- "**/test/**"
- "**/build/**"
scoring:
decayHalfLifeDays: 90 # half-life for recency decay (days)
excludeCoverage: false # true → Composite = CC × Decay; coverage shown raw, not scored
apiAnalysis:
enabled: true # off by default; required for api/shared granularities
sharedComponentMode: BOTH # CUMULATIVE | SEPARATE | BOTH
classpathDirectories: [] # dirs with dependency jars/classes for symbol resolution
jacocoReportPath: build/reports/jacoco/test/jacocoTestReport.xml # optional
output:
formats: [csv, yaml, md, html] # case-insensitive; ≥1 required
apiLayout: BOTH # COMBINED (into hotspots.*) | STANDALONE (api_report.*) | BOTH
coverageBreakdown: false # true → also write coverage_breakdown.yml: the audit
# trail behind every coverage number (per-file counts;
# per-endpoint per-method covered/executable lines)
path: ./hotspot-report
topN: 30 # 0 = all rows
Env vars substitute as ${VAR_NAME} in any string value; YAML comment lines
(#) are left untouched.
sharedComponentModeCUMULATIVE — shared methods counted inside every endpoint's aggregate; no separate list.SEPARATE — shared methods excluded from endpoint aggregates and reported once on their own.BOTH (default) — endpoint aggregates include them and a separate shared list is emitted.analyze options| Option | Effect |
|---|---|
--config, -c <file> | Path to the YAML config (required) |
--quiet, -q | Suppress the stdout summary |
--strict, -s | Exit code 3 on empty result (zero commits or zero files) — for CI gating |
Exit codes: 0 ok · 1 config/pipeline failure · 2 usage error · 3 --strict empty result.
api_report.yml and iterate apiHotspots by ascending compositeRank.apiHotspots is empty but the app clearly has endpoints THEN check,
in order: apiAnalysis.enabled: true, controllers carry
@RestController/@Controller + a mapping annotation, and
apiAnalysis.classpathDirectories includes the dependency jars/classes so
cross-type calls resolve. Do not report "no endpoints".coverageMultiplier is 10 (or every lineCoverage is 0)
THEN the JaCoCo report doesn't match the analyzed sources — supply a report
from the same build/module; do not conclude "nothing is tested".Files: 0 THEN fix scope.include (single-module
needs src/main/java/**/*.java, multi-module needs **/src/main/java/**/*.java;
list both).Commits: 0 THEN widen window.days or switch to
absolute window.since/window.until overlapping real activity.target.type is github THEN clone the repo locally and re-run with
target.type: local-git (Phase 1 wires only local-git end-to-end).--strict so an empty result fails loudly.scoring.excludeCoverage: true (Composite becomes CC × Decay).api_report.yml; the whole point is determinism, not a model guess.apiHotspots as "no endpoints." It almost always
means apiAnalysis is off or the call graph couldn't resolve (missing
classpathDirectories).UnsupportedClassVersionError).**/generated/**,
**/build/**, **/target/**, **/test/** in scope.exclude.compositeRank, not
simpleRank, is the test-priority signal.The project's own suite exercises every layer (parser, scoring, output, E2E):
./gradlew test # comprehensive; run before trusting a build
Skill-level smoke check — analyze this very repo and assert a non-empty result:
bash -n skills/hotspot-analysis/scripts/get-jar.sh skills/hotspot-analysis/scripts/run-analysis.sh
JAR="$(skills/hotspot-analysis/scripts/get-jar.sh)" # resolves/downloads the jar
java -jar "$JAR" init -o /tmp/h.yml -f
# set analysis.target.path in /tmp/h.yml to this repo's absolute path, then:
java -jar "$JAR" analyze --config /tmp/h.yml --strict
echo "exit=$?" # 0 = produced output; 3 = empty (misconfigured)
A green ./gradlew test plus a 0 exit on the smoke run means the skill's
toolchain is sound end-to-end.
output.coverageBreakdown writes coverage_breakdown.yml, the calculation
trace behind every coverage number; releases enforce 4-way version
consistency (tag = gradle = CLI = plugin/marketplace manifests).release button + skills-validation CI gate + tag
protection; the button reliably fans out to jar/image via workflow_call
(a GITHUB_TOKEN-created release doesn't re-trigger event workflows).gh skill publish) auto-attaches hotspot.jar and builds the
Docker image, so a new release never breaks the download. license added to
frontmatter.hotspot.jar asset) + ghcr Docker image; a missing jacocoReportPath now
warns and disables coverage instead of silently penalizing every artifact.apiAnalysis + JaCoCo + --strict exposed; per-granularity scoring docs.references/api-report-schema.md.docs/scoring/.scripts/get-jar.sh · scripts/run-analysis.sh.docs/ (architecture, advanced techniques, theory).Provides a checklist for code reviews covering functionality, security, performance, maintainability, tests, and quality. Use for pull requests, audits, team standards, and developer training.
npx claudepluginhub baekchangjoon/hotspot-analysis --plugin hotspot-analysis