1. Domain types
The vocabulary every other module imports.
src/domain.ts is the only file in the project with no internal
imports. It's the shared vocabulary the rest of the app speaks.
What lives here
export type Brain = {
id: string;
name: string;
folderPath: string;
workspace: string;
status: "indexing" | "indexed" | "error";
fileCount: number;
chunkCount: number;
createdAt: string;
updatedAt: string;
lastIndexedAt?: string;
lastError?: string;
};
export type LocalDocument = {
relativePath: string;
content: string;
checksum: string;
bytes: number;
};
export type BrowserDocumentInput = {
relativePath: string;
content: string;
bytes: number;
};
export type TextChunk = {
id: string;
brainId: string;
relativePath: string;
chunkIndex: number;
content: string;
checksum: string;
};
export type ChatMessage = {
role: "user" | "assistant" | "system";
content: string;
};
export type Citation = {
id: string;
relativePath: string;
chunkIndex: number;
score?: number;
};
export type SearchHit = Citation & { content: string };
export type ChatAnswer = { answer: string; citations: Citation[] };
export type CreateBrainFromFilesInput = {
name: string;
folderName: string;
documents: BrowserDocumentInput[];
};
export type CreateBrainFromFolderInput = {
name: string;
folderPath: string;
};
export class AppError extends Error {
constructor(
message: string,
readonly status = 400,
) {
super(message);
this.name = "AppError";
}
}Why this file exists
Two reasons:
- It pins the shape of data moving between modules.
Brain,TextChunk,SearchHit, andChatAnswerget touched by every other file. Putting them in one place stops three modules from each having slightly different ideas of what aTextChunklooks like. - It keeps
AppErrorreusable. Any module can throw anAppErrorwith an HTTP-style status, and the server maps it straight to a response. Without this, you end up with bespoke error types in every layer.
The seam between local and browser
LocalDocument and BrowserDocumentInput are deliberately
separate.
LocalDocumentcarries achecksumbecause the local-folder walker can compute one from disk.BrowserDocumentInputdoesn't — the file picker hands us content and bytes; the checksum gets computed downstream by the file adapter.
Keeping these two types apart means the workflow doesn't have to special-case "this brain came from disk" vs "this brain came from a browser upload" until the very last step.
Citation has a score, SearchHit has content
SearchHit extends Citation. The split matters:
- A
SearchHitis whatragSearchreturns. The chunk content is included because the prompt builder needs it. - A
Citationis what we return to the caller. The chunk content is intentionally dropped — the caller doesn't need to render the source verbatim, just link to it.
A tiny pattern, but it stops chunk text from leaking into HTTP responses.
What you can run
After writing only domain.ts:
bun run typecheck…will pass. Nothing else compiles yet because nothing else exists, but the domain file alone has no errors.
Next up: the chunker and grounded prompt, which imports these types and starts using them.