Skip to content

Commit b69617b

Browse files
authored
Merge pull request #4 from drompincen/feature/claude-companion-features
Add blame, explain, related, session transcripts, and stale index warning
2 parents 76a4e11 + 5b27f2c commit b69617b

24 files changed

Lines changed: 2912 additions & 7 deletions

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
branches: [ main, master ]
88

99
permissions:
10-
contents: read
10+
contents: write
1111
checks: write
1212
pages: write
1313
id-token: write

JavaDuckerMcpServer.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,38 @@ public static void main(String[] args) throws Exception {
154154
(String) a.get("action"),
155155
(String) a.getOrDefault("directory", ""),
156156
(String) a.getOrDefault("extensions", ""))))
157+
// ── Explain tool ─────────────────────────────────────────────
158+
.tool(tool("javaducker_explain",
159+
"Get everything JavaDucker knows about a file: summary, dependencies, dependents, tags, " +
160+
"classification, related plans, blame highlights, and co-change partners. One call for full context.",
161+
schema(props("file_path", str("Absolute path to the file to explain")), "file_path")),
162+
(ex, a) -> call(() -> httpPost("/explain", Map.of("filePath", a.get("file_path")))))
163+
// ── Git Blame tool ───────────────────────────────────────────
164+
.tool(tool("javaducker_blame",
165+
"Show who last changed each line of a file, with commit info. Groups consecutive lines by same commit. Optionally narrow to a line range.",
166+
schema(props(
167+
"file_path", str("Absolute path to the file"),
168+
"start_line", intParam("Start line number (optional)"),
169+
"end_line", intParam("End line number (optional)")),
170+
"file_path")),
171+
(ex, a) -> call(() -> {
172+
Map<String, Object> body = new LinkedHashMap<>();
173+
body.put("filePath", a.get("file_path"));
174+
if (a.containsKey("start_line")) body.put("startLine", ((Number) a.get("start_line")).intValue());
175+
if (a.containsKey("end_line")) body.put("endLine", ((Number) a.get("end_line")).intValue());
176+
return httpPost("/blame", body);
177+
}))
178+
// ── Co-Change / Related Files tool ─────────────────────────
179+
.tool(tool("javaducker_related",
180+
"Find files commonly edited together with this file, based on git co-change history. " +
181+
"Helps identify related files you might need to update.",
182+
schema(props(
183+
"file_path", str("Absolute path to the file"),
184+
"max_results", intParam("Max results (default 10)")),
185+
"file_path")),
186+
(ex, a) -> call(() -> httpPost("/related", Map.of(
187+
"filePath", a.get("file_path"),
188+
"maxResults", ((Number) a.getOrDefault("max_results", 10)).intValue()))))
157189
// ── Content Intelligence: write tools ────────────────────────
158190
.tool(tool("javaducker_classify",
159191
"Classify an artifact by doc type (ADR, DESIGN_DOC, PLAN, MEETING_NOTES, THREAD, SCRATCH, CODE, REFERENCE, TICKET).",
@@ -257,6 +289,12 @@ public static void main(String[] args) throws Exception {
257289
"Health report for all concepts: active/stale doc counts, trend (active/fading/cold).",
258290
"{}"),
259291
(ex, a) -> call(() -> httpGet("/concept-health")))
292+
// ── Index Health tool ────────────────────────────────────────
293+
.tool(tool("javaducker_index_health",
294+
"Check index health: how many files are current vs stale. Returns actionable recommendation. " +
295+
"No parameters required — scans all indexed files.",
296+
"{}"),
297+
(ex, a) -> call(JavaDuckerMcpServer::indexHealth))
260298
// ── Reladomo tools ───────────────────────────────────────────
261299
.tool(tool("javaducker_reladomo_relationships",
262300
"Get a Reladomo object's attributes, relationships, and metadata in one call.",
@@ -464,6 +502,33 @@ static Map<String, Object> dependents(String artifactId) throws Exception {
464502
return r;
465503
}
466504

505+
@SuppressWarnings("unchecked")
506+
static Map<String, Object> indexHealth() throws Exception {
507+
Map<String, Object> summary = httpGet("/stale/summary");
508+
int staleCount = ((Number) summary.getOrDefault("stale_count", 0)).intValue();
509+
double stalePercent = ((Number) summary.getOrDefault("stale_percentage", 0.0)).doubleValue();
510+
long total = ((Number) summary.getOrDefault("total_checked", 0)).longValue();
511+
512+
String recommendation;
513+
if (staleCount == 0) {
514+
recommendation = "All " + total + " indexed files are current. No action needed.";
515+
} else if (stalePercent > 10) {
516+
recommendation = "More than 10% of indexed files are stale (" + staleCount + "/" + total
517+
+ "). Consider running a full re-index with javaducker_index_directory.";
518+
} else {
519+
List<Map<String, Object>> staleFiles = (List<Map<String, Object>>) summary.get("stale");
520+
List<String> paths = staleFiles != null
521+
? staleFiles.stream().limit(5)
522+
.map(f -> (String) f.get("original_client_path"))
523+
.filter(Objects::nonNull).toList()
524+
: List.of();
525+
recommendation = staleCount + " file(s) are stale. Re-index them with javaducker_index_file: " + paths;
526+
}
527+
summary.put("recommendation", recommendation);
528+
summary.put("health_status", stalePercent > 10 ? "degraded" : "healthy");
529+
return summary;
530+
}
531+
467532
// ── HTTP helpers ──────────────────────────────────────────────────────────
468533

469534
static Map<String, Object> httpGet(String path) throws Exception {

drom-plans/quick-win-blame.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
title: Quick Win — javaducker_blame
3+
status: completed
4+
created: 2026-03-28
5+
updated: 2026-03-28
6+
current_chapter: 2
7+
---
8+
9+
# Plan: Quick Win — javaducker_blame
10+
11+
Add a `javaducker_blame` MCP tool that wraps `git blame` with indexed context — given a file or artifact, return who last touched each section, when, and the commit message. Enriched with JavaDucker metadata (summary, tags, dependencies).
12+
13+
## Chapter 1: Git Blame Service
14+
**Status:** completed
15+
**Depends on:** none
16+
17+
- [x] Create `GitBlameService.java` in `server/service/` (~150 lines) — run `git blame --porcelain <file>` via `ProcessBuilder`, parse output into structured records: `BlameEntry(lineStart, lineEnd, commitHash, author, authorDate, commitMessage, content)`
18+
- [x] Handle edge cases: file not in git, binary files, files outside PROJECT_ROOT, git not installed
19+
- [x] Add method `blameForArtifact(artifactId)` — look up `original_client_path` from `artifacts` table, run blame on that path
20+
- [x] Add method `blameForLines(filePath, startLine, endLine)` — blame a specific range (useful when Claude is looking at a search result with line numbers)
21+
- [x] Write `GitBlameServiceTest` — test porcelain parsing, file-not-found, range queries
22+
23+
**Notes:**
24+
> Use `--porcelain` format for machine-readable output. Cache blame results in memory (LRU, 50 files) since blame is expensive. PROJECT_ROOT env var gives the repo root.
25+
26+
## Chapter 2: REST Endpoint & MCP Tool
27+
**Status:** completed
28+
**Depends on:** Chapter 1
29+
30+
- [x] Add `GET /api/blame/{artifactId}` endpoint — returns blame entries for the full file, enriched with artifact summary if available
31+
- [x] Add `POST /api/blame` endpoint — body: `{filePath, startLine?, endLine?}` — blame by path with optional range
32+
- [x] Add `javaducker_blame` MCP tool to `JavaDuckerMcpServer.java` — params: `file_path` (required), `start_line` (optional), `end_line` (optional). Description: "Show who last changed each line of a file, with commit info. Optionally narrow to a line range."
33+
- [x] Enrich blame response: for each unique commit, include the commit message. For the file, include artifact summary and dependency count if indexed
34+
- [x] Write integration test — blame a real file in the repo, verify structure
35+
36+
**Notes:**
37+
> Keep the MCP tool response concise — group consecutive lines by the same commit into ranges, don't return per-line entries for 500-line files. Example: "lines 1-45: alice, 2026-03-20, 'Add auth middleware'"
38+
39+
---
40+
41+
## Risks
42+
- git must be available on PATH — fail gracefully with clear error if not
43+
- Large files produce verbose blame — cap at 500 lines or summarize by commit
44+
45+
## Open Questions
46+
- None — straightforward feature

drom-plans/quick-win-explain.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
title: Quick Win — javaducker_explain
3+
status: completed
4+
created: 2026-03-28
5+
updated: 2026-03-28
6+
current_chapter: 2
7+
---
8+
9+
# Plan: Quick Win — javaducker_explain
10+
11+
Add a `javaducker_explain` MCP tool that returns everything JavaDucker knows about a file in one call: summary, dependency chain, dependents, tags, classification, related plans/ADRs, co-change partners, and blame highlights. A single-call context loader for Claude.
12+
13+
## Chapter 1: Explain Service
14+
**Status:** completed
15+
**Depends on:** none
16+
17+
- [x] Create `ExplainService.java` in `server/service/` (~200 lines) — aggregates data from existing services. Constructor-injected: `ArtifactService`, `DependencyService`, `SearchService`, `ContentIntelligenceService`. Optional: `GitBlameService`, `CoChangeService` (may not exist yet — use try-catch or Optional injection)
18+
- [x] Add method `explain(artifactId)` — returns a composite map with sections: `file` (name, path, size, status, indexed_at), `summary` (from artifact summary), `dependencies` (imports this file uses), `dependents` (files that import this one), `classification` (doc_type, tags, freshness), `salient_points` (decisions, risks, actions from content intelligence), `related_artifacts` (by concept links), `blame_highlights` (top 3 most recent committers + their commit messages, if GitBlameService available), `co_changes` (top 5 files commonly edited together, if CoChangeService available)
19+
- [x] Handle missing data gracefully — each section is optional. If a service throws or returns null, that section is omitted, not the whole response
20+
- [x] Write `ExplainServiceTest` — test with full data, test with partial data (no blame, no co-change), test with unknown artifactId
21+
22+
**Notes:**
23+
> This is a read-only aggregation service. It calls existing services — no new tables, no new data. Its value is combining 6-8 separate API calls into one.
24+
25+
## Chapter 2: REST Endpoint & MCP Tool
26+
**Status:** completed
27+
**Depends on:** Chapter 1
28+
29+
- [x] Add `GET /api/explain/{artifactId}` endpoint — returns the full explain bundle
30+
- [x] Add `POST /api/explain` endpoint — body: `{filePath}` — resolve to artifact_id first, then explain. If not indexed, return a minimal response with just the file path and a note that it's not indexed
31+
- [x] Add `javaducker_explain` MCP tool — params: `file_path` (required). Description: "Get everything JavaDucker knows about a file: summary, dependencies, dependents, tags, classification, related plans, blame highlights, and co-change partners. One call for full context."
32+
- [x] Keep response compact — summaries not full text, top-N for lists (5 deps, 5 dependents, 5 co-changes, 3 blame entries). Include counts so Claude knows there's more if needed
33+
- [x] Write integration test — explain a real indexed file, verify all sections present
34+
35+
**Notes:**
36+
> This will likely become the most-called MCP tool. Claude should use it before editing any file to understand full context. Keep the response under 2K tokens.
37+
38+
---
39+
40+
## Risks
41+
- Response could be large if all sections are populated — enforce limits per section
42+
- Depends on other quick wins (blame, related) for full richness, but works without them
43+
44+
## Open Questions
45+
- Should explain also return recent search queries that matched this file? (might be noise)

drom-plans/quick-win-related.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
title: Quick Win — javaducker_related
3+
status: completed
4+
created: 2026-03-28
5+
updated: 2026-03-28
6+
current_chapter: 2
7+
---
8+
9+
# Plan: Quick Win — javaducker_related
10+
11+
Add a `javaducker_related` MCP tool that finds files commonly edited together by analyzing git log co-change history. When Claude is editing a file, it can ask "what other files usually change with this one?" to avoid missing related updates.
12+
13+
## Chapter 1: Co-Change Analysis Service
14+
**Status:** completed
15+
**Depends on:** none
16+
17+
- [x] Create `CoChangeService.java` in `server/service/` (~180 lines) — run `git log --name-only --pretty=format:"COMMIT:%H" --since="6 months ago"` via `ProcessBuilder`, parse into commit→files map. For a given file, find all commits that touched it, then count co-occurrences of other files across those commits. Rank by frequency
18+
- [x] Add `cochange_cache` table to `SchemaBootstrap``file_a VARCHAR, file_b VARCHAR, co_change_count INTEGER, last_commit_date TIMESTAMP, PRIMARY KEY (file_a, file_b)` — precomputed cache, rebuilt on demand
19+
- [x] Add method `buildCoChangeIndex()` — parse full git log, populate cache table. Idempotent (DELETE + INSERT)
20+
- [x] Add method `getRelatedFiles(filePath, maxResults)` — query cache, return ranked list with co-change count and last shared commit date
21+
- [x] Filter out noise: ignore files that appear in >50% of all commits (build scripts, lockfiles), ignore commits with >30 files (bulk renames/reformats)
22+
- [x] Write `CoChangeServiceTest` — test parsing, ranking, noise filtering, empty repo
23+
24+
**Notes:**
25+
> The git log parse is expensive (~2-5s for large repos) so we cache in DuckDB. Rebuild on demand via endpoint or when staleness is detected. The 6-month window keeps results relevant.
26+
27+
## Chapter 2: REST Endpoint & MCP Tool
28+
**Status:** completed
29+
**Depends on:** Chapter 1
30+
31+
- [x] Add `GET /api/related/{artifactId}` endpoint — look up original_client_path, return co-change partners ranked by frequency
32+
- [x] Add `POST /api/related` endpoint — body: `{filePath, maxResults?, rebuild?}`. If `rebuild: true`, refresh the co-change cache first
33+
- [x] Add `javaducker_related` MCP tool — params: `file_path` (required), `max_results` (optional, default 10). Description: "Find files that are commonly edited together with this file, based on git history. Helps identify related files you might need to update."
34+
- [x] Enrich response: for each related file, include co-change count, last shared commit, and whether it's currently indexed in JavaDucker (with summary if so)
35+
- [x] Add `POST /api/rebuild-cochange` endpoint — force rebuild the cache
36+
- [x] Write integration test — build index from real repo, query related files
37+
38+
**Notes:**
39+
> This is one of the most useful tools for Claude — when making a change, knowing what usually changes together prevents incomplete PRs.
40+
41+
---
42+
43+
## Risks
44+
- Git log parsing on very large repos (10K+ commits) could be slow — the 6-month window mitigates this
45+
- Projects without git history return empty results — handle gracefully
46+
47+
## Open Questions
48+
- Should the co-change cache auto-rebuild on a timer, or only on demand?

0 commit comments

Comments
 (0)