# TRUEREF-0014 — Repository Version Management

**Priority:** P1
**Status:** Pending
**Depends On:** TRUEREF-0003
**Blocks:** —

---

## Overview

Support indexing specific git tags and branches as distinct versioned snapshots of a repository. Users can query documentation for a specific version using the `/owner/repo/version` library ID format. Versions are registered via `trueref.json`'s `previousVersions` field or manually via the API.

---

## Acceptance Criteria

- [ ] `GET /api/v1/libs/:id/versions` — list all indexed versions for a repository
- [ ] `POST /api/v1/libs/:id/versions` — add a new version (tag or branch)
- [ ] `DELETE /api/v1/libs/:id/versions/:versionTag` — remove a version and its snippets
- [ ] `POST /api/v1/libs/:id/versions/:versionTag/index` — trigger indexing for a specific version
- [ ] Version-specific queries: `/api/v1/context?libraryId=/facebook/react/v18.3.0`
- [ ] Default branch queries: `/api/v1/context?libraryId=/facebook/react` (no version suffix)
- [ ] `previousVersions` from `trueref.json` automatically registered during indexing (state: `pending`)
- [ ] GitHub tag list endpoint used to validate tag existence before indexing
- [ ] Version snippets stored with `versionId` FK; default branch snippets have `versionId = NULL`

---

## Version ID Convention

```
Version ID format: {repositoryId}/{tag}

Examples:
  /facebook/react/v18.3.0
  /facebook/react/v17.0.2
  /vercel/next.js/v14.3.0-canary.1
```

---

## API Endpoints

### `GET /api/v1/libs/:id/versions`

Response `200`:
```json
{
  "versions": [
    {
      "id": "/facebook/react/v18.3.0",
      "repositoryId": "/facebook/react",
      "tag": "v18.3.0",
      "title": "React v18.3.0",
      "state": "indexed",
      "totalSnippets": 892,
      "indexedAt": "2026-03-22T10:00:00Z"
    }
  ]
}
```

### `POST /api/v1/libs/:id/versions`

Request body:
```json
{
  "tag": "v18.3.0",
  "title": "React v18.3.0",
  "autoIndex": true
}
```

Response `201`:
```json
{
  "version": { ...RepositoryVersion },
  "job": { "id": "uuid", "status": "queued" }
}
```

### `DELETE /api/v1/libs/:id/versions/:tag`

Deletes the version record and all associated documents/snippets via cascade.
Response `204`.

### `POST /api/v1/libs/:id/versions/:tag/index`

Queues an indexing job for this specific version tag.
Response `202` with job details.

---

## GitHub Tag Discovery

```typescript
async function listGitHubTags(
  owner: string,
  repo: string,
  token?: string
): Promise<Array<{ name: string; commit: { sha: string } }>> {
  const headers: Record<string, string> = {
    'Accept': 'application/vnd.github.v3+json',
    'User-Agent': 'TrueRef/1.0',
  };
  if (token) headers['Authorization'] = `Bearer ${token}`;

  const response = await fetch(
    `https://api.github.com/repos/${owner}/${repo}/tags?per_page=100`,
    { headers }
  );

  if (!response.ok) throw new GitHubApiError(response.status);
  return response.json();
}
```

---

## Query Routing

In the search/context endpoints, the `libraryId` is parsed to extract the optional version:

```typescript
function resolveSearchTarget(libraryId: string): {
  repositoryId: string;
  versionId?: string;
} {
  const { repositoryId, version } = parseLibraryId(libraryId);

  if (!version) {
    // Query default branch: versionId = NULL
    return { repositoryId };
  }

  // Look up versionId from tag
  const versionRecord = db.prepare(
    `SELECT id FROM repository_versions WHERE repository_id = ? AND tag = ?`
  ).get(repositoryId, version) as { id: string } | undefined;

  if (!versionRecord) {
    throw new NotFoundError(
      `Version "${version}" not found for library "${repositoryId}"`
    );
  }

  return { repositoryId, versionId: versionRecord.id };
}
```

Snippets with `version_id IS NULL` belong to the default branch; snippets with a `version_id` belong to that specific version. Search queries filter by `version_id = ?` or `version_id IS NULL` accordingly.

---

## Version Service

```typescript
export class VersionService {
  constructor(private db: BetterSQLite3.Database) {}

  list(repositoryId: string): RepositoryVersion[]

  add(repositoryId: string, tag: string, title?: string): RepositoryVersion

  remove(repositoryId: string, tag: string): void

  getByTag(repositoryId: string, tag: string): RepositoryVersion | null

  registerFromConfig(
    repositoryId: string,
    previousVersions: { tag: string; title: string }[]
  ): RepositoryVersion[]
}
```

---

## Files to Create

- `src/lib/server/services/version.service.ts`
- `src/routes/api/v1/libs/[id]/versions/+server.ts` — GET, POST
- `src/routes/api/v1/libs/[id]/versions/[tag]/+server.ts` — DELETE
- `src/routes/api/v1/libs/[id]/versions/[tag]/index/+server.ts` — POST
- `src/lib/server/crawler/github-tags.ts`
