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

186 lines
4.6 KiB
Markdown

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