Skip to content

Commit ba47643

Browse files
committed
feat: add isolated store interface types for SQLite migration
Self-contained type definitions for the new storage layer replacing Graphology + JSON. Covers all 6 graphs, cross-graph links, tags, attachments, projects, team, and workspace metadata. Key design decisions: - All entity IDs numeric (autoincrement), slugs for external use - Store is fully synchronous (better-sqlite3), embeddings passed from outside - SearchQuery supports text (FTS5), embedding (sqlite-vec), or both - Cross-graph links via junction table, no proxy nodes - Tags and attachments as shared stores across all graphs - ProjectScopedStore for multi-project workspaces - TeamStore at workspace level with numeric member IDs - MetaMixin on all stores for arbitrary key-value metadata - Detail types (NoteDetail, TaskDetail, etc.) include relations + crossLinks
1 parent 132c84b commit ba47643

13 files changed

Lines changed: 830 additions & 0 deletions

File tree

src/store/types/attachments.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { GraphName } from './common';
2+
3+
// ---------------------------------------------------------------------------
4+
// Attachments Store (shared across all graphs)
5+
// ---------------------------------------------------------------------------
6+
7+
export interface AttachmentMeta {
8+
filename: string;
9+
mimeType: string;
10+
size: number;
11+
addedAt: number;
12+
}
13+
14+
export interface AttachmentRecord extends AttachmentMeta {
15+
graph: GraphName;
16+
entityId: number;
17+
}
18+
19+
export interface AttachmentsStore {
20+
/** Add an attachment to an entity */
21+
add(graph: GraphName, entityId: number, filename: string, data: Buffer): AttachmentMeta;
22+
23+
/** Remove an attachment from an entity */
24+
remove(graph: GraphName, entityId: number, filename: string): void;
25+
26+
/** Remove all attachments for an entity (e.g. on delete) */
27+
removeAll(graph: GraphName, entityId: number): void;
28+
29+
/** List attachments for an entity */
30+
list(graph: GraphName, entityId: number): AttachmentMeta[];
31+
32+
/** Get attachment file path (for serving) */
33+
getPath(graph: GraphName, entityId: number, filename: string): string | null;
34+
}

src/store/types/code.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type { MetaMixin, PaginationOptions, SearchQuery, SearchResult } from './common';
2+
3+
// ---------------------------------------------------------------------------
4+
// Code Store (indexed)
5+
// ---------------------------------------------------------------------------
6+
7+
/** Parser decides what kinds exist per language (e.g. 'function', 'struct', 'trait', 'macro') */
8+
export type CodeNodeKind = string;
9+
10+
/** Parser decides what edge kinds exist per language (e.g. 'contains', 'imports', 'implements') */
11+
export type CodeEdgeKind = string;
12+
13+
export interface CodeSymbol {
14+
id: number;
15+
kind: CodeNodeKind;
16+
fileId: string;
17+
language: string;
18+
name: string;
19+
signature: string;
20+
docComment: string;
21+
body: string;
22+
startLine: number;
23+
endLine: number;
24+
isExported: boolean;
25+
}
26+
27+
export interface CodeEdge {
28+
fromId: number;
29+
toId: number;
30+
kind: CodeEdgeKind;
31+
}
32+
33+
export interface CodeFileEntry {
34+
id: number;
35+
fileId: string;
36+
language: string;
37+
symbolCount: number;
38+
mtime: number;
39+
}
40+
41+
export interface CodeStore extends MetaMixin {
42+
/**
43+
* Replace all symbols for a file (called by indexer).
44+
* Handles insert/update of nodes and intra-file edges (contains).
45+
* embeddings: symbolId → vector (keyed by symbol name or temp ref from parser)
46+
*/
47+
updateFile(fileId: string, symbols: Omit<CodeSymbol, 'id'>[], edges: Array<{ fromName: string; toName: string; kind: CodeEdgeKind }>, mtime: number, embeddings: Map<string, number[]>): void;
48+
49+
/** Remove all symbols and edges for a file */
50+
removeFile(fileId: string): void;
51+
52+
/** Resolve pending cross-file edges (imports, extends, implements) after full index */
53+
resolveEdges(edges: Array<{ fromName: string; toName: string; kind: CodeEdgeKind }>): void;
54+
55+
/** Get mtime for a file (null if not indexed) */
56+
getFileMtime(fileId: string): number | null;
57+
58+
/** List indexed files with symbol counts */
59+
listFiles(filter?: string, pagination?: PaginationOptions): { results: CodeFileEntry[]; total: number };
60+
61+
/** Get all symbols for a file, sorted by startLine */
62+
getFileSymbols(fileId: string): CodeSymbol[];
63+
64+
/** Get a single symbol with its edges */
65+
getSymbol(symbolId: number): (CodeSymbol & { edges: Array<{ id: number; kind: CodeEdgeKind; direction: 'in' | 'out' }> }) | null;
66+
67+
/** Search symbols (hybrid: FTS5 + sqlite-vec) */
68+
search(query: SearchQuery): SearchResult[];
69+
70+
/** Search files by path */
71+
searchFiles(query: SearchQuery): SearchResult[];
72+
73+
/** Find symbols by exact name (for cross-references, explain-symbol) */
74+
findByName(name: string): CodeSymbol[];
75+
}

src/store/types/common.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/** Shared types used across all store modules. */
2+
3+
export type GraphName = 'code' | 'docs' | 'files' | 'knowledge' | 'tasks' | 'skills';
4+
5+
// ---------------------------------------------------------------------------
6+
// Search
7+
// ---------------------------------------------------------------------------
8+
9+
export type SearchMode = 'hybrid' | 'vector' | 'keyword';
10+
11+
export interface SearchQuery {
12+
/** Text query for FTS5 keyword search */
13+
text?: string;
14+
/** Embedding vector for sqlite-vec similarity search */
15+
embedding?: number[];
16+
/** Search strategy (default 'hybrid'). 'keyword' ignores embedding, 'vector' ignores text */
17+
searchMode?: SearchMode;
18+
/** Max vector candidates before fusion/ranking (default 50) */
19+
topK?: number;
20+
/** Max results to return (default 20) */
21+
maxResults?: number;
22+
/** Minimum relevance score 0-1 (default 0) */
23+
minScore?: number;
24+
}
25+
26+
export interface SearchResult {
27+
id: string;
28+
score: number;
29+
}
30+
31+
// ---------------------------------------------------------------------------
32+
// Meta (key-value, shared interface for all stores)
33+
// ---------------------------------------------------------------------------
34+
35+
export interface MetaMixin {
36+
getMeta(key: string): string | null;
37+
setMeta(key: string, value: string): void;
38+
deleteMeta(key: string): void;
39+
}
40+
41+
// ---------------------------------------------------------------------------
42+
// Pagination
43+
// ---------------------------------------------------------------------------
44+
45+
export interface PaginationOptions {
46+
limit?: number;
47+
offset?: number;
48+
}
49+
50+
// ---------------------------------------------------------------------------
51+
// Cross-graph links (replaces proxy nodes)
52+
// ---------------------------------------------------------------------------
53+
54+
export interface CrossLink {
55+
sourceGraph: GraphName;
56+
sourceId: number;
57+
targetGraph: GraphName;
58+
targetId: number;
59+
kind: string;
60+
}
61+
62+
export interface CrossLinkFilter {
63+
sourceGraph?: GraphName;
64+
sourceId?: number;
65+
targetGraph?: GraphName;
66+
targetId?: number;
67+
kind?: string;
68+
}
69+
70+
// ---------------------------------------------------------------------------
71+
// Version conflict
72+
// ---------------------------------------------------------------------------
73+
74+
export class VersionConflictError extends Error {
75+
constructor(
76+
public readonly current: number,
77+
public readonly expected: number,
78+
) {
79+
super(`Version conflict: expected ${expected}, current is ${current}`);
80+
this.name = 'VersionConflictError';
81+
}
82+
}
83+
84+
// ---------------------------------------------------------------------------
85+
// Relation (same-graph edge)
86+
// ---------------------------------------------------------------------------
87+
88+
export interface Relation {
89+
fromId: number;
90+
toId: number;
91+
kind: string;
92+
}
93+

src/store/types/docs.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type { MetaMixin, PaginationOptions, SearchQuery, SearchResult } from './common';
2+
3+
// ---------------------------------------------------------------------------
4+
// Docs Store (indexed)
5+
// ---------------------------------------------------------------------------
6+
7+
export interface DocChunk {
8+
id: number;
9+
fileId: string;
10+
title: string;
11+
content: string;
12+
level: number;
13+
language?: string;
14+
symbols: string[];
15+
}
16+
17+
export interface DocFileEntry {
18+
id: number;
19+
fileId: string;
20+
title: string;
21+
chunkCount: number;
22+
mtime: number;
23+
}
24+
25+
export interface DocsStore extends MetaMixin {
26+
/** Replace all chunks for a doc file. embeddings: temp chunk ref → vector */
27+
updateFile(fileId: string, chunks: Omit<DocChunk, 'id'>[], mtime: number, embeddings: Map<string, number[]>): void;
28+
29+
/** Remove all chunks for a file */
30+
removeFile(fileId: string): void;
31+
32+
/** Resolve pending cross-file link edges after full index */
33+
resolveLinks(edges: Array<{ fromFileId: string; toFileId: string }>): void;
34+
35+
/** Get mtime for a file (null if not indexed) */
36+
getFileMtime(fileId: string): number | null;
37+
38+
/** List doc files */
39+
listFiles(filter?: string, pagination?: PaginationOptions): { results: DocFileEntry[]; total: number };
40+
41+
/** Get all chunks for a file, sorted by level */
42+
getFileChunks(fileId: string): DocChunk[];
43+
44+
/** Get a single chunk */
45+
getNode(chunkId: number): DocChunk | null;
46+
47+
/** Search chunks */
48+
search(query: SearchQuery): SearchResult[];
49+
50+
/** Search doc files by path */
51+
searchFiles(query: SearchQuery): SearchResult[];
52+
53+
/** List code snippets (chunks with language set), optionally filtered by language */
54+
listSnippets(language?: string, pagination?: PaginationOptions): { results: DocChunk[]; total: number };
55+
56+
/** Search code snippets */
57+
searchSnippets(query: SearchQuery, language?: string): SearchResult[];
58+
59+
/** Find chunks that reference a given symbol name */
60+
findBySymbol(symbol: string): DocChunk[];
61+
}

src/store/types/files.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type { MetaMixin, PaginationOptions, SearchQuery, SearchResult } from './common';
2+
3+
// ---------------------------------------------------------------------------
4+
// File Index Store (indexed)
5+
// ---------------------------------------------------------------------------
6+
7+
export interface FileEntry {
8+
id: number;
9+
filePath: string;
10+
fileName: string;
11+
directory: string;
12+
extension: string;
13+
language: string | null;
14+
mimeType: string | null;
15+
size: number;
16+
mtime: number;
17+
}
18+
19+
export interface DirectoryEntry {
20+
id: number;
21+
filePath: string;
22+
fileName: string;
23+
directory: string;
24+
size: number;
25+
fileCount: number;
26+
}
27+
28+
export interface FileListOptions extends PaginationOptions {
29+
/** Browse a specific directory (list children) */
30+
directory?: string;
31+
/** Filter by extension */
32+
extension?: string;
33+
/** Substring match on path */
34+
filter?: string;
35+
}
36+
37+
export interface FilesStore extends MetaMixin {
38+
/** Add or update a file entry */
39+
updateFile(filePath: string, size: number, mtime: number, embedding: number[]): void;
40+
41+
/** Remove a file entry (auto-cleans empty parent dirs) */
42+
removeFile(filePath: string): void;
43+
44+
/** Recompute directory aggregates (size, fileCount) */
45+
rebuildDirectoryStats(): void;
46+
47+
/** Get mtime for a file (null if not indexed) */
48+
getFileMtime(filePath: string): number | null;
49+
50+
/** List files/directories */
51+
listFiles(opts?: FileListOptions): { results: Array<FileEntry | DirectoryEntry>; total: number };
52+
53+
/** Get info for a single file or directory */
54+
getFileInfo(filePath: string): (FileEntry | DirectoryEntry) | null;
55+
56+
/** Search files by path */
57+
search(query: SearchQuery): SearchResult[];
58+
}

src/store/types/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export * from './common';
2+
export * from './code';
3+
export * from './docs';
4+
export * from './files';
5+
export * from './knowledge';
6+
export * from './tasks';
7+
export * from './skills';
8+
export * from './tags';
9+
export * from './attachments';
10+
export * from './projects';
11+
export * from './team';
12+
export * from './store';

src/store/types/knowledge.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { CrossLink, MetaMixin, PaginationOptions, Relation, SearchQuery, SearchResult } from './common';
2+
import type { AttachmentMeta } from './attachments';
3+
4+
// ---------------------------------------------------------------------------
5+
// Knowledge Store (user-managed)
6+
// ---------------------------------------------------------------------------
7+
8+
export interface NoteCreate {
9+
title: string;
10+
content: string;
11+
authorId?: number;
12+
}
13+
14+
export interface NotePatch {
15+
title?: string;
16+
content?: string;
17+
}
18+
19+
export interface NoteRecord {
20+
id: number;
21+
slug: string;
22+
title: string;
23+
content: string;
24+
tags: string[];
25+
attachments: AttachmentMeta[];
26+
createdAt: number;
27+
updatedAt: number;
28+
version: number;
29+
createdById: number | null;
30+
updatedById: number | null;
31+
}
32+
33+
export interface NoteDetail extends NoteRecord {
34+
relations: { incoming: Relation[]; outgoing: Relation[] };
35+
crossLinks: CrossLink[];
36+
}
37+
38+
export interface KnowledgeStore extends MetaMixin {
39+
// --- CRUD ---
40+
create(data: NoteCreate, embedding: number[]): NoteRecord;
41+
update(noteId: number, patch: NotePatch, embedding: number[] | null, authorId?: number, expectedVersion?: number): NoteRecord;
42+
delete(noteId: number): void;
43+
get(noteId: number): NoteDetail | null;
44+
getBySlug(slug: string): NoteDetail | null;
45+
list(filter?: string, tag?: string, pagination?: PaginationOptions): { results: NoteRecord[]; total: number };
46+
47+
// --- Search ---
48+
search(query: SearchQuery): SearchResult[];
49+
50+
// --- Same-graph relations ---
51+
createRelation(fromId: number, toId: number, kind: string): void;
52+
deleteRelation(fromId: number, toId: number): void;
53+
listRelations(noteId: number): { incoming: Relation[]; outgoing: Relation[] };
54+
55+
// --- Timestamps ---
56+
getUpdatedAt(noteId: number): number | null;
57+
}

0 commit comments

Comments
 (0)