Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
branches: [ main, master ]

permissions:
contents: read
contents: write
checks: write
pages: write
id-token: write
Expand Down
65 changes: 65 additions & 0 deletions JavaDuckerMcpServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,38 @@ public static void main(String[] args) throws Exception {
(String) a.get("action"),
(String) a.getOrDefault("directory", ""),
(String) a.getOrDefault("extensions", ""))))
// ── Explain tool ─────────────────────────────────────────────
.tool(tool("javaducker_explain",
"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.",
schema(props("file_path", str("Absolute path to the file to explain")), "file_path")),
(ex, a) -> call(() -> httpPost("/explain", Map.of("filePath", a.get("file_path")))))
// ── Git Blame tool ───────────────────────────────────────────
.tool(tool("javaducker_blame",
"Show who last changed each line of a file, with commit info. Groups consecutive lines by same commit. Optionally narrow to a line range.",
schema(props(
"file_path", str("Absolute path to the file"),
"start_line", intParam("Start line number (optional)"),
"end_line", intParam("End line number (optional)")),
"file_path")),
(ex, a) -> call(() -> {
Map<String, Object> body = new LinkedHashMap<>();
body.put("filePath", a.get("file_path"));
if (a.containsKey("start_line")) body.put("startLine", ((Number) a.get("start_line")).intValue());
if (a.containsKey("end_line")) body.put("endLine", ((Number) a.get("end_line")).intValue());
return httpPost("/blame", body);
}))
// ── Co-Change / Related Files tool ─────────────────────────
.tool(tool("javaducker_related",
"Find files commonly edited together with this file, based on git co-change history. " +
"Helps identify related files you might need to update.",
schema(props(
"file_path", str("Absolute path to the file"),
"max_results", intParam("Max results (default 10)")),
"file_path")),
(ex, a) -> call(() -> httpPost("/related", Map.of(
"filePath", a.get("file_path"),
"maxResults", ((Number) a.getOrDefault("max_results", 10)).intValue()))))
// ── Content Intelligence: write tools ────────────────────────
.tool(tool("javaducker_classify",
"Classify an artifact by doc type (ADR, DESIGN_DOC, PLAN, MEETING_NOTES, THREAD, SCRATCH, CODE, REFERENCE, TICKET).",
Expand Down Expand Up @@ -257,6 +289,12 @@ public static void main(String[] args) throws Exception {
"Health report for all concepts: active/stale doc counts, trend (active/fading/cold).",
"{}"),
(ex, a) -> call(() -> httpGet("/concept-health")))
// ── Index Health tool ────────────────────────────────────────
.tool(tool("javaducker_index_health",
"Check index health: how many files are current vs stale. Returns actionable recommendation. " +
"No parameters required — scans all indexed files.",
"{}"),
(ex, a) -> call(JavaDuckerMcpServer::indexHealth))
// ── Reladomo tools ───────────────────────────────────────────
.tool(tool("javaducker_reladomo_relationships",
"Get a Reladomo object's attributes, relationships, and metadata in one call.",
Expand Down Expand Up @@ -464,6 +502,33 @@ static Map<String, Object> dependents(String artifactId) throws Exception {
return r;
}

@SuppressWarnings("unchecked")
static Map<String, Object> indexHealth() throws Exception {
Map<String, Object> summary = httpGet("/stale/summary");
int staleCount = ((Number) summary.getOrDefault("stale_count", 0)).intValue();
double stalePercent = ((Number) summary.getOrDefault("stale_percentage", 0.0)).doubleValue();
long total = ((Number) summary.getOrDefault("total_checked", 0)).longValue();

String recommendation;
if (staleCount == 0) {
recommendation = "All " + total + " indexed files are current. No action needed.";
} else if (stalePercent > 10) {
recommendation = "More than 10% of indexed files are stale (" + staleCount + "/" + total
+ "). Consider running a full re-index with javaducker_index_directory.";
} else {
List<Map<String, Object>> staleFiles = (List<Map<String, Object>>) summary.get("stale");
List<String> paths = staleFiles != null
? staleFiles.stream().limit(5)
.map(f -> (String) f.get("original_client_path"))
.filter(Objects::nonNull).toList()
: List.of();
recommendation = staleCount + " file(s) are stale. Re-index them with javaducker_index_file: " + paths;
}
summary.put("recommendation", recommendation);
summary.put("health_status", stalePercent > 10 ? "degraded" : "healthy");
return summary;
}

// ── HTTP helpers ──────────────────────────────────────────────────────────

static Map<String, Object> httpGet(String path) throws Exception {
Expand Down
46 changes: 46 additions & 0 deletions drom-plans/quick-win-blame.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
title: Quick Win — javaducker_blame
status: completed
created: 2026-03-28
updated: 2026-03-28
current_chapter: 2
---

# Plan: Quick Win — javaducker_blame

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).

## Chapter 1: Git Blame Service
**Status:** completed
**Depends on:** none

- [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)`
- [x] Handle edge cases: file not in git, binary files, files outside PROJECT_ROOT, git not installed
- [x] Add method `blameForArtifact(artifactId)` — look up `original_client_path` from `artifacts` table, run blame on that path
- [x] Add method `blameForLines(filePath, startLine, endLine)` — blame a specific range (useful when Claude is looking at a search result with line numbers)
- [x] Write `GitBlameServiceTest` — test porcelain parsing, file-not-found, range queries

**Notes:**
> 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.

## Chapter 2: REST Endpoint & MCP Tool
**Status:** completed
**Depends on:** Chapter 1

- [x] Add `GET /api/blame/{artifactId}` endpoint — returns blame entries for the full file, enriched with artifact summary if available
- [x] Add `POST /api/blame` endpoint — body: `{filePath, startLine?, endLine?}` — blame by path with optional range
- [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."
- [x] Enrich blame response: for each unique commit, include the commit message. For the file, include artifact summary and dependency count if indexed
- [x] Write integration test — blame a real file in the repo, verify structure

**Notes:**
> 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'"

---

## Risks
- git must be available on PATH — fail gracefully with clear error if not
- Large files produce verbose blame — cap at 500 lines or summarize by commit

## Open Questions
- None — straightforward feature
45 changes: 45 additions & 0 deletions drom-plans/quick-win-explain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
title: Quick Win — javaducker_explain
status: completed
created: 2026-03-28
updated: 2026-03-28
current_chapter: 2
---

# Plan: Quick Win — javaducker_explain

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.

## Chapter 1: Explain Service
**Status:** completed
**Depends on:** none

- [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)
- [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)
- [x] Handle missing data gracefully — each section is optional. If a service throws or returns null, that section is omitted, not the whole response
- [x] Write `ExplainServiceTest` — test with full data, test with partial data (no blame, no co-change), test with unknown artifactId

**Notes:**
> 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.

## Chapter 2: REST Endpoint & MCP Tool
**Status:** completed
**Depends on:** Chapter 1

- [x] Add `GET /api/explain/{artifactId}` endpoint — returns the full explain bundle
- [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
- [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."
- [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
- [x] Write integration test — explain a real indexed file, verify all sections present

**Notes:**
> 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.

---

## Risks
- Response could be large if all sections are populated — enforce limits per section
- Depends on other quick wins (blame, related) for full richness, but works without them

## Open Questions
- Should explain also return recent search queries that matched this file? (might be noise)
48 changes: 48 additions & 0 deletions drom-plans/quick-win-related.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
title: Quick Win — javaducker_related
status: completed
created: 2026-03-28
updated: 2026-03-28
current_chapter: 2
---

# Plan: Quick Win — javaducker_related

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.

## Chapter 1: Co-Change Analysis Service
**Status:** completed
**Depends on:** none

- [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
- [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
- [x] Add method `buildCoChangeIndex()` — parse full git log, populate cache table. Idempotent (DELETE + INSERT)
- [x] Add method `getRelatedFiles(filePath, maxResults)` — query cache, return ranked list with co-change count and last shared commit date
- [x] Filter out noise: ignore files that appear in >50% of all commits (build scripts, lockfiles), ignore commits with >30 files (bulk renames/reformats)
- [x] Write `CoChangeServiceTest` — test parsing, ranking, noise filtering, empty repo

**Notes:**
> 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.

## Chapter 2: REST Endpoint & MCP Tool
**Status:** completed
**Depends on:** Chapter 1

- [x] Add `GET /api/related/{artifactId}` endpoint — look up original_client_path, return co-change partners ranked by frequency
- [x] Add `POST /api/related` endpoint — body: `{filePath, maxResults?, rebuild?}`. If `rebuild: true`, refresh the co-change cache first
- [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."
- [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)
- [x] Add `POST /api/rebuild-cochange` endpoint — force rebuild the cache
- [x] Write integration test — build index from real repo, query related files

**Notes:**
> This is one of the most useful tools for Claude — when making a change, knowing what usually changes together prevents incomplete PRs.

---

## Risks
- Git log parsing on very large repos (10K+ commits) could be slow — the 6-month window mitigates this
- Projects without git history return empty results — handle gracefully

## Open Questions
- Should the co-change cache auto-rebuild on a timer, or only on demand?
Loading
Loading