Rank Java files, methods, and REST API endpoints by a deterministic Composite Hotspot Score (recency-weighted churn × cognitive complexity × JaCoCo coverage gap) to prioritize test generation. Also a Claude Code skill/plugin.
SaferSkills independently audited hotspot-analysis (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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"):
scores and same ordering. No randomness, no model variance.
methods whose line ranges a commit's diff hunks touched, not the whole file.
(configurable half-life).
code; coverage can be a scoring input or an observational column.
--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.
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 bootJarNo 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
java -jar "$JAR" init -o hotspot.ymlanalysis.target.path atthe 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--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 --strictOutputs 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.csvapi_report.yml and iterateapiHotspots 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:
most questions become a yes/no confirmation:
grep -rl "@RestController\|@RequestMapping" <repo>/src → ifhits, 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 → defaultapiAnalysis.classpathDirectories.
git -C <repo> log -1 --format=%cd → if older than ayear, propose absolute window.since/until instead of days.
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 |
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/`](../../docs/scoring/README.en.md): 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 rowsEnv 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-moduleneeds src/main/java/**/*.java, multi-module needs **/src/main/java/**/*.java; list both).
Commits: 0 THEN widen window.days or switch toabsolute window.since/window.until overlapping real activity.
target.type is github THEN clone the repo locally and re-run withtarget.type: local-git (Phase 1 wires only local-git end-to-end).
--strict so an empty result fails loudly.THEN set scoring.excludeCoverage: true (Composite becomes CC × Decay).
api_report.yml; the whole point is determinism, not a model guess.
means apiAnalysis is off or the call graph couldn't resolve (missing classpathDirectories).
reads as 0% coverage → every multiplier maxes at 10 and the ranking is bogus.
UnsupportedClassVersionError).**/generated/**,**/build/**, **/target/**, **/test/** in scope.exclude.
evidence; surface the factors (churn, recency, complexity, coverage) so the choice is explainable.
compositeRank, notsimpleRank, is the test-priority signal.
The project's own suite exercises every layer (parser, scoring, output, E2E):
./gradlew test # comprehensive; run before trusting a buildSkill-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.
the call graph) instead of a mean of per-method ratios, so a large untested method can no longer hide behind a small covered one; new opt-in 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 + tagprotection; the button reliably fans out to jar/image via workflow_call (a GITHUB_TOKEN-created release doesn't re-trigger event workflows).
created by 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.
prioritization driving the Phase 1 CLI; RestAssured consumption guide; 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).~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.