Files
trueref/docs/features/TRUEREF-0002.md
2026-03-27 02:23:01 +01:00

264 lines
5.9 KiB
Markdown

# 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`