chore: initial project scaffold
This commit is contained in:
252
docs/features/TRUEREF-0002.md
Normal file
252
docs/features/TRUEREF-0002.md
Normal file
@@ -0,0 +1,252 @@
|
||||
# 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`
|
||||
Reference in New Issue
Block a user