186 lines
4.7 KiB
Markdown
186 lines
4.7 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`
|