Skip to content
Merged
1 change: 1 addition & 0 deletions .please/docs/tracks.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
{"id":"registry-meta-20260407","type":"feature","status":"completed","phase":"implement","issue":"#5","created":"2026-04-07","section":"completed"}
{"id":"ecosystem-resolvers-20260407","type":"feature","status":"completed","phase":"implement","issue":"#6","created":"2026-04-07","section":"completed"}
{"id":"npm-publish-release-please-20260408","type":"chore","status":"planned","phase":"spec","issue":"#16","created":"2026-04-08","section":"active"}
{"id":"maven-resolver-20260408","type":"feature","status":"in_progress","phase":"implement","issue":"#15","created":"2026-04-08","section":"active"}
{"id":"extract-shared-registry-20260408","type":"refactor","status":"in_progress","phase":"implement","issue":"#14","created":"2026-04-08","section":"active"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"track_id": "maven-resolver-20260408",
"type": "feature",
"status": "review",
"created_at": "2026-04-08T00:00:00Z",
"updated_at": "2026-04-08T00:00:00Z",
"issue": "#15",
"pr": "#17",
"project": ""
}
149 changes: 149 additions & 0 deletions .please/docs/tracks/completed/maven-resolver-20260408/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Plan: Maven Ecosystem Resolver

> Track: maven-resolver-20260408
> Spec: [spec.md](./spec.md)

## Overview
- **Source**: /please:plan
- **Track**: maven-resolver-20260408
- **Created**: 2026-04-08
- **Approach**: Follow existing resolver pattern (NpmResolver/PypiResolver/PubResolver)

## Purpose

Add Maven Central support to the CLI resolver system, enabling `maven:groupId:artifactId` package resolution. This completes the Maven ecosystem support that currently only has registry aliases and auto-detection but no resolver.

## Context

The ASK CLI has three ecosystem resolvers (npm, pypi, pub) that map package identifiers to GitHub repositories. Maven auto-detection (`pom.xml` → `'maven'`) and registry aliases (`maven:group:artifact`) already exist, but `getResolver('maven')` returns `null`. Users with Maven projects get a "no resolver for 'maven'" error when the registry misses.

## Architecture Decision

**Approach**: Direct pattern replication with dual-source fallback.

- Follow the established `EcosystemResolver` interface exactly as npm/pypi/pub do
- Primary source: Maven Central Search API (JSON, fast, includes `scmUrl`)
- Fallback source: Direct POM XML download from Maven Central Repository (regex extraction of `<scm><url>` and `<url>` tags — no external XML parser needed)
- `name` parameter arrives as `groupId:artifactId` (split at last colon)
- POM path construction: `groupId.replace(/\./g, '/')` → `com/google/guava/guava/{version}/guava-{version}.pom`

**Why no XML parser**: POM files have predictable structure; regex extraction of 2 tags is simpler and avoids adding a dependency. The `parseRepoUrl()` utility handles URL normalization.

## Tasks

### T-1: Create MavenResolver class with Search API lookup
- **File**: `packages/cli/src/resolvers/maven.ts` (new)
- **Test**: `packages/cli/test/resolvers/maven.test.ts` (new)
- **FR**: FR-1, FR-2, FR-3, FR-5 (Search API scmUrl), FR-6, FR-7
- **Details**:
- Implement `MavenResolver` class with `resolve(name, version)` method
- Parse `name` as `groupId:artifactId` (split at last colon)
- Fetch `https://search.maven.org/solrsearch/select?q=g:{groupId}+AND+a:{artifactId}&rows=1&wt=json&core=gav` for latest
- For explicit version: add `&v={version}` filter
- Extract `response.docs[0].g`, `.a`, `.v`, `.repositoryUrl` or fall back to `scmUrl` from extended search
- Return `ResolveResult` with `v{version}` ref and `{version}` fallback
- **TDD**: RED → test mock Search API JSON → GREEN → implement resolver → REFACTOR

### T-2: Add POM XML fallback for GitHub URL extraction
- **File**: `packages/cli/src/resolvers/maven.ts`
- **Test**: `packages/cli/test/resolvers/maven.test.ts`
- **FR**: FR-4, FR-5 (POM scm.url, POM url)
- **Details**:
- When Search API fails (non-200 or no scmUrl), download POM XML from `https://repo1.maven.org/maven2/{groupPath}/{artifactId}/{version}/{artifactId}-{version}.pom`
- Regex extract `<scm>` → `<url>` value, fallback to top-level `<url>` value
- Use `parseRepoUrl()` for normalization
- Requires resolved version — if version is `latest`, fetch latest version from Search API first, then use POM as fallback for repo URL only
- **TDD**: RED → test mock POM XML response → GREEN → implement fallback → REFACTOR
- **Depends on**: T-1

### T-3: Register MavenResolver in resolver index
- **File**: `packages/cli/src/resolvers/index.ts`
- **Test**: `packages/cli/test/resolvers/index.test.ts`
- **FR**: FR-8
- **Details**:
- Add `'maven'` to `SupportedEcosystem` union type
- Add `maven: new MavenResolver()` to `resolvers` record
- Add test: `getResolver('maven')` returns defined resolver with `.resolve` function
- **TDD**: RED → add test for `getResolver('maven')` → GREEN → register → REFACTOR
- **Depends on**: T-1

### T-4: Integration verification and edge cases
- **File**: `packages/cli/test/resolvers/maven.test.ts`
- **FR**: FR-9, AC-1 through AC-6
- **Details**:
- Test `maven:com.google.guava:guava` resolves correctly (AC-1)
- Test explicit version `@33.4.0-jre` (AC-2)
- Test Search API failure triggers POM fallback (AC-3)
- Test error when neither source has GitHub URL
- Test invalid `name` format (missing colon) throws clear error
- Verify all existing resolver tests still pass (AC-5)
- Ensure >80% coverage (AC-6)
- **Depends on**: T-1, T-2, T-3

## Dependencies

```
T-1 → T-2
T-1 → T-3
T-2 + T-3 → T-4
```

## Key Files

| File | Role |
|---|---|
| `packages/cli/src/resolvers/maven.ts` | New MavenResolver class |
| `packages/cli/src/resolvers/index.ts` | Register maven in SupportedEcosystem |
| `packages/cli/src/resolvers/utils.ts` | Shared `parseRepoUrl()` utility |
| `packages/cli/src/registry.ts` | `parseDocSpec()` + `detectEcosystem()` (no changes needed) |
| `packages/cli/test/resolvers/maven.test.ts` | New test file |
| `packages/cli/test/resolvers/index.test.ts` | Add maven registration test |

## Verification

```bash
bun test packages/cli/test/resolvers/maven.test.ts
bun test packages/cli/test/resolvers/index.test.ts
bun test packages/cli/test/resolvers/ # all resolver tests
bun run --cwd packages/cli lint
```

## Progress

- [x] T-1: MavenResolver + Search API
- [x] T-2: POM XML fallback
- [x] T-3: Register in index
- [x] T-4: Integration tests + edge cases

## Decision Log

| Decision | Rationale |
|---|---|
| Regex over XML parser for POM | Only need 2 tags; avoids new dependency |
| Last-colon split for groupId:artifactId | groupId contains dots, artifactId never contains colons |
| Search API `core=gav` parameter | Returns version-specific results for explicit version lookup |

## Surprises & Discoveries

- `parseDocSpec` uses first colon as ecosystem separator, so `maven:com.google.guava:guava` correctly splits into `ecosystem=maven`, `name=com.google.guava:guava`
- Maven auto-detection already maps `pom.xml`/`build.gradle`/`build.gradle.kts` to `'maven'` ecosystem
- No changes needed to `registry.ts` or `index.ts` — the resolver system is fully pluggable

## Outcomes & Retrospective

### What Was Shipped
- `MavenResolver` class with Search API + POM XML dual-source architecture
- 18 unit tests covering happy paths, fallback scenarios, and edge cases
- Registered in resolver index — `getResolver('maven')` now works

### What Went Well
- Existing resolver pattern made implementation straightforward
- TDD approach caught the scmUrl query core issue during review
- Spec compliance check identified missing POM fallback path early

### What Could Improve
- Initial implementation missed the Search API scmUrl priority (FR-5) — should verify API response schema before implementing
- Maven Central Search API `core=gav` vs default core behavior should be documented in gotchas

### Tech Debt Created
- None
49 changes: 49 additions & 0 deletions .please/docs/tracks/completed/maven-resolver-20260408/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Maven Ecosystem Resolver

> Track: maven-resolver-20260408

## Overview

Add a Maven ecosystem resolver to the ASK CLI, enabling `maven:groupId:artifactId` spec format. The resolver maps Maven Central packages to their GitHub repositories, completing Maven ecosystem support alongside the existing registry aliases.

## Requirements

### Functional Requirements

- [ ] FR-1: Implement `MavenResolver` class implementing `EcosystemResolver` interface with `resolve(name, version)` method
- [ ] FR-2: Support `groupId:artifactId` package identifier format (colon-separated, Maven standard)
- [ ] FR-3: Primary lookup via Maven Central Search API (`https://search.maven.org/solrsearch/select?q=g:GROUP+AND+a:ARTIFACT&rows=1&wt=json`)
- [ ] FR-4: Fallback to direct POM XML download from Maven Central Repository (`https://repo1.maven.org/maven2/{group/path}/{artifact}/{version}/{artifact}-{version}.pom`) when Search API fails
- [ ] FR-5: Extract GitHub repository URL with priority: (1) Search API `scmUrl` field, (2) POM `<scm><url>` element, (3) POM `<url>` element. Use `parseRepoUrl()` utility for normalization
- [ ] FR-6: Version resolution — resolve `latest` to the most recent release version; support explicit version lookup via Search API `v:{version}` filter
- [ ] FR-7: Return `ResolveResult` with `repo`, `ref` (`v{version}` primary, `{version}` fallback), and `resolvedVersion`
- [ ] FR-8: Register `MavenResolver` in `resolvers/index.ts` — add `'maven'` to `SupportedEcosystem` union type and resolvers record
- [ ] FR-9: Verify integration with existing registry maven aliases (e.g., `maven:com.google.guava:guava` → registry lookup → resolver fallback)

### Non-functional Requirements

- [ ] NFR-1: Follow existing resolver patterns (NpmResolver, PypiResolver) for code structure and error handling
- [ ] NFR-2: Use `consola.debug()` for resolver trace logging
- [ ] NFR-3: Provide clear error messages when GitHub repo cannot be resolved from Maven metadata

## Acceptance Criteria

- [ ] AC-1: `ask docs add maven:com.google.guava:guava` resolves to `google/guava` GitHub repo with correct version tag
- [ ] AC-2: `ask docs add maven:com.google.guava:guava@33.4.0-jre` resolves to the specific version
- [ ] AC-3: When Search API is unavailable, POM XML fallback successfully resolves the package
- [ ] AC-4: `getResolver('maven')` returns a `MavenResolver` instance
- [ ] AC-5: All existing resolver tests continue to pass
- [ ] AC-6: Maven resolver has >80% test coverage

## Out of Scope

- Gradle-specific metadata or build file parsing
- Maven local repository (`~/.m2`) scanning
- Private/enterprise Maven repository support (e.g., Nexus, Artifactory)
- Multi-module Maven project resolution (only top-level artifact)

## Assumptions

- Maven Central Search API (`search.maven.org`) is publicly accessible without authentication
- Most popular Maven packages have a GitHub `scm.url` in their POM
- The `groupId:artifactId` colon format does not conflict with existing spec parsing (version separator is `@`)
4 changes: 3 additions & 1 deletion packages/cli/src/resolvers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MavenResolver } from './maven.js'
import { NpmResolver } from './npm.js'
import { PubResolver } from './pub.js'
import { PypiResolver } from './pypi.js'
Expand Down Expand Up @@ -31,9 +32,10 @@ export interface EcosystemResolver {
resolve: (name: string, version: string) => Promise<ResolveResult>
}

type SupportedEcosystem = 'npm' | 'pypi' | 'pub'
type SupportedEcosystem = 'maven' | 'npm' | 'pypi' | 'pub'

const resolvers: Record<SupportedEcosystem, EcosystemResolver> = {
maven: new MavenResolver(),
npm: new NpmResolver(),
pypi: new PypiResolver(),
pub: new PubResolver(),
Expand Down
Loading