| tags |
|
||||
|---|---|---|---|---|---|
| aliases |
|
This guide covers development setup, coding conventions, testing practices, and the workflow for contributing to CodeRAG.
- Node.js >= 20
- pnpm >= 9 (package manager)
- Ollama (for local embedding and LLM, optional for tests)
# Clone the repository
git clone https://dev.azure.com/momc-pl/CodeRAG/_git/CodeRAG
cd CodeRAG
# Install dependencies
pnpm install
# Build all packages
pnpm build
# Run tests
pnpm testCodeRAG is a pnpm workspace monorepo with these packages:
packages/
core/ # Core library: ingestion, embedding, retrieval
cli/ # CLI tool (coderag init/index/search/serve/status)
mcp-server/ # MCP server (stdio + SSE transport)
benchmarks/ # Benchmark suite
vscode-extension/ # VS Code extension
api-server/ # REST API server for team/cloud deployments
viewer/ # Web-based visualization SPA
Each package has its own package.json, tsconfig.json, and builds independently. Cross-package dependencies use workspace protocol (workspace:*).
The project uses a shared tsconfig.base.json with strict settings:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"declaration": true,
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}Warning: >
noUncheckedIndexedAccessis enabled. Array/object index access returnsT | undefined, so you must handle theundefinedcase explicitly.
- No
any-- useunknownand narrow with type guards - No
ascasts without justification in a comment - ESM modules only -- use
import/export, neverrequire() - All imports must include the
.jsextension for NodeNext resolution
| Element | Convention | Example |
|---|---|---|
| Functions / variables | camelCase | parseChunks, topK |
| Types / classes | PascalCase | SearchResult, DependencyGraph |
| Constants | UPPER_SNAKE | BATCH_SIZE, DEFAULT_CONFIG |
| File names | kebab-case | tree-sitter-parser.ts, hybrid-search.ts |
- Prefer pure functions over classes with mutable state
- Use
readonlyon interface properties and function parameters - Minimize mutable state -- favor
map/filter/reduceover loops with mutation - Use
ReadonlyArray,ReadonlyMap,ReadonlySetfor immutable collections
CodeRAG uses the Result pattern via neverthrow instead of throwing exceptions:
import { ok, err, type Result } from 'neverthrow';
// Define a typed error class
export class ParseError extends Error {
constructor(message: string) {
super(message);
this.name = 'ParseError';
}
}
// Return Result instead of throwing
function parseConfig(raw: string): Result<Config, ParseError> {
try {
const parsed = JSON.parse(raw);
return ok(parsed as Config);
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
return err(new ParseError(`Invalid config: ${message}`));
}
}
// Consume with isOk/isErr
const result = parseConfig(input);
if (result.isErr()) {
console.error(result.error.message);
return;
}
const config = result.value; // Typed as ConfigNote: > Every public function that can fail should return
Result<T, E>. Exceptions are reserved for truly unexpected situations (programmer errors).
All external dependencies are behind interfaces. This enables:
- Easy testing with mocks
- Swapping implementations without changing consumers
- Clear dependency boundaries
// Define the interface
export interface EmbeddingProvider {
embed(texts: string[]): Promise<Result<number[][], EmbedError>>;
readonly dimensions: number;
}
// Implement it
export class OllamaEmbeddingProvider implements EmbeddingProvider {
// ...
}Key interfaces: EmbeddingProvider, VectorStore, BacklogProvider, AuthProvider, LLMProvider, Parser, Chunker, ReRanker.
All configuration flows through .coderag.yaml. No hardcoded values for anything that could vary between environments.
- Vitest is the test runner
- Tests are co-located with source files as
*.test.ts - Coverage target: 80%+ on
@code-rag/core
Use the describe/it pattern:
import { describe, it, expect, vi } from 'vitest';
import { parseConfig } from './config-loader.js';
describe('parseConfig', () => {
it('should parse a valid YAML config', async () => {
const result = await parseConfig('/path/to/config');
expect(result.isOk()).toBe(true);
});
it('should return an error for missing config', async () => {
const result = await parseConfig('/nonexistent');
expect(result.isErr()).toBe(true);
expect(result._unsafeUnwrapErr().message).toContain('not found');
});
});# Run all tests
pnpm test
# Run tests for a specific package
pnpm --filter @code-rag/core test
# Run with coverage
pnpm test -- --coverage
# Run a specific test file
pnpm test -- packages/core/src/embedding/ollama-embedding-provider.test.tsmain (protected)
feature/AB#XXXX-short-description (feature branches)
bugfix/AB#XXXX-fix-description (bugfix branches)
git checkout -b feature/AB#42-add-auth-middlewareCommit messages start with the Azure DevOps work item ID:
AB#42 Add authentication middleware for API server
Rules:
- AB#XXXX prefix links the commit to Azure DevOps work items
- No conventional commits prefix (
feat:,fix:, etc.) --AB#is the prefix - Multiple stories in one commit:
AB#42 AB#43 Description - Keep the description concise and meaningful
- Squash merge to main for clean history
- PR title should include the AB# reference
- PR body should summarize changes and link to the story
This step-by-step guide shows how to add a new provider (e.g., a new embedding backend).
Check packages/core/src/types/provider.ts for existing interfaces. If your provider type exists (e.g., EmbeddingProvider), skip to Step 2.
For a new provider category, define the interface:
// packages/core/src/types/provider.ts
export interface MyProvider {
readonly name: string;
initialize(config: Record<string, unknown>): Promise<Result<void, MyProviderError>>;
doSomething(input: string): Promise<Result<Output, MyProviderError>>;
}# Create the file (kebab-case)
touch packages/core/src/my-domain/my-new-provider.ts// packages/core/src/my-domain/my-new-provider.ts
import { ok, err, type Result } from 'neverthrow';
import type { MyProvider } from '../types/provider.js';
export interface MyNewProviderConfig {
readonly apiKey: string;
readonly endpoint: string;
}
export class MyNewProvider implements MyProvider {
readonly name = 'my-new-provider';
private readonly config: MyNewProviderConfig;
constructor(config: MyNewProviderConfig) {
this.config = config;
}
async initialize(config: Record<string, unknown>): Promise<Result<void, Error>> {
// Validate and connect
return ok(undefined);
}
async doSomething(input: string): Promise<Result<string, Error>> {
// Implementation
return ok(`result for ${input}`);
}
}touch packages/core/src/my-domain/my-new-provider.test.tsimport { describe, it, expect, vi } from 'vitest';
import { MyNewProvider } from './my-new-provider.js';
describe('MyNewProvider', () => {
it('should initialize successfully', async () => {
const provider = new MyNewProvider({ apiKey: 'test', endpoint: 'http://localhost' });
const result = await provider.initialize({});
expect(result.isOk()).toBe(true);
});
// ... more tests
});Add the export to packages/core/src/index.ts:
export { MyNewProvider } from './my-domain/my-new-provider.js';
export type { MyNewProviderConfig } from './my-domain/my-new-provider.js';Add the provider option to the config schema and factory logic so users can select it in .coderag.yaml.
pnpm build && pnpm test- Design Decisions -- architectural decisions and rationale
- Interfaces -- complete list of provider interfaces
- Core -- core library architecture