# TRUEREF-0002 — Repository Management Service & REST API

**Priority:** P0
**Status:** Pending
**Depends On:** TRUEREF-0001
**Blocks:** TRUEREF-0009, TRUEREF-0010, TRUEREF-0015

---

## Overview

Implement the core `RepositoryService` that handles CRUD operations for repositories, and expose those operations via SvelteKit API routes. This feature establishes the management plane for all library sources.

---

## Acceptance Criteria

- [ ] `RepositoryService` class with full CRUD operations
- [ ] `GET /api/v1/libs` — list all repositories with metadata
- [ ] `POST /api/v1/libs` — add a new repository (GitHub URL or local path)
- [ ] `GET /api/v1/libs/:id` — get single repository details
- [ ] `PATCH /api/v1/libs/:id` — update repository metadata
- [ ] `DELETE /api/v1/libs/:id` — delete repository and all associated data
- [ ] `POST /api/v1/libs/:id/index` — trigger indexing job (queues job, returns job ID)
- [ ] Input validation with descriptive error messages
- [ ] All endpoints return JSON with consistent error shape
- [ ] Unit tests for `RepositoryService` covering all operations

---

## Repository ID Generation

GitHub repositories:
- Input URL: `https://github.com/facebook/react` or `github.com/facebook/react`
- Generated ID: `/facebook/react`

Local repositories:
- Input path: `/home/user/projects/my-sdk`
- Generated ID: `/local/my-sdk` (basename of path, slugified)
- Collision resolution: append `-2`, `-3`, etc.

Version-specific IDs: `/facebook/react/v18.3.0`

---

## Service Interface

```typescript
// src/lib/server/services/repository.service.ts

export interface AddRepositoryInput {
  source: 'github' | 'local';
  sourceUrl: string;           // GitHub URL or absolute local path
  title?: string;              // override auto-detected title
  description?: string;
  branch?: string;             // GitHub: default branch; Local: n/a
  githubToken?: string;        // for private GitHub repos
}

export interface UpdateRepositoryInput {
  title?: string;
  description?: string;
  branch?: string;
  githubToken?: string;
}

export class RepositoryService {
  constructor(private db: BetterSQLite3.Database) {}

  async list(options?: {
    state?: Repository['state'];
    limit?: number;
    offset?: number;
  }): Promise<Repository[]>

  async get(id: string): Promise<Repository | null>

  async add(input: AddRepositoryInput): Promise<Repository>

  async update(id: string, input: UpdateRepositoryInput): Promise<Repository>

  async remove(id: string): Promise<void>

  async getStats(id: string): Promise<{
    totalSnippets: number;
    totalTokens: number;
    totalDocuments: number;
    lastIndexedAt: Date | null;
  }>
}
```

---

## API Route Specifications

### `GET /api/v1/libs`

Query parameters:
- `state` (optional): filter by state (`pending`, `indexed`, `error`, etc.)
- `limit` (optional, default 50): max results
- `offset` (optional, default 0): pagination offset

Response `200`:
```json
{
  "libraries": [
    {
      "id": "/facebook/react",
      "title": "React",
      "description": "...",
      "source": "github",
      "state": "indexed",
      "totalSnippets": 1234,
      "totalTokens": 98000,
      "trustScore": 8.5,
      "stars": 228000,
      "lastIndexedAt": "2026-03-22T10:00:00Z",
      "versions": ["v18.3.0", "v17.0.2"]
    }
  ],
  "total": 12,
  "limit": 50,
  "offset": 0
}
```

### `POST /api/v1/libs`

Request body:
```json
{
  "source": "github",
  "sourceUrl": "https://github.com/facebook/react",
  "branch": "main",
  "githubToken": "ghp_...",
  "autoIndex": true
}
```

Response `201`:
```json
{
  "library": { ...Repository },
  "job": { "id": "uuid", "status": "queued" }
}
```

`autoIndex: true` (default) immediately queues an indexing job.

Response `409` if repository already exists:
```json
{ "error": "Repository /facebook/react already exists" }
```

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

`:id` must be URL-encoded (e.g., `%2Ffacebook%2Freact` for `/facebook/react`).

Response `200`: single `Repository` object with versions array.
Response `404`: `{ "error": "Repository not found" }`

### `PATCH /api/v1/libs/:id`

Request body: partial `UpdateRepositoryInput`.
Response `200`: updated `Repository`.

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

Cascades: deletes all documents, snippets, embeddings, jobs for this repository.
Response `204`: no body.
Response `404`: not found.

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

Triggers a new indexing job. If a job is already running for this repo, returns the existing job.

Request body (optional):
```json
{ "version": "v18.3.0" }
```

Response `202`:
```json
{
  "job": {
    "id": "uuid",
    "repositoryId": "/facebook/react",
    "status": "queued",
    "progress": 0,
    "createdAt": "2026-03-22T10:00:00Z"
  }
}
```

---

## Error Response Shape

All error responses follow:
```json
{
  "error": "Human-readable message",
  "code": "MACHINE_READABLE_CODE",
  "details": {}
}
```

Error codes:
- `NOT_FOUND`
- `ALREADY_EXISTS`
- `INVALID_INPUT`
- `INVALID_URL`
- `INDEXING_IN_PROGRESS`

---

## ID Resolution Logic

```typescript
function resolveGitHubId(url: string): string {
  // Parse owner/repo from URL variants:
  // https://github.com/facebook/react
  // https://github.com/facebook/react.git
  // github.com/facebook/react
  const match = url.match(/github\.com\/([^/]+)\/([^/\s.]+)/);
  if (!match) throw new Error('Invalid GitHub URL');
  return `/${match[1]}/${match[2]}`;
}

function resolveLocalId(path: string, existingIds: string[]): string {
  const base = slugify(path.split('/').at(-1)!);
  let id = `/local/${base}`;
  let counter = 2;
  while (existingIds.includes(id)) {
    id = `/local/${base}-${counter++}`;
  }
  return id;
}
```

---

## Files to Create

- `src/lib/server/services/repository.service.ts`
- `src/routes/api/v1/libs/+server.ts` — GET (list), POST (add)
- `src/routes/api/v1/libs/[id]/+server.ts` — GET, PATCH, DELETE
- `src/routes/api/v1/libs/[id]/index/+server.ts` — POST (trigger indexing)
- `src/lib/server/utils/id-resolver.ts` — ID generation helpers
- `src/lib/server/utils/validation.ts` — input validators
- `src/lib/server/services/repository.service.test.ts`
