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