Development Standards: Code Quality Rules
Code quality standards and best practices for Matimo. Covers both Python and TypeScript SDKs.
Table of Contents
Section titled “Table of Contents”- Python Standards
- TypeScript Standards
- Naming Conventions
- Error Handling
- Testing Standards
- Documentation Standards
- Security Standards
- Logging Standards
- Performance Standards
- Quality Metrics
Python Standards
Section titled “Python Standards”Strict Type Hints (Required)
Section titled “Strict Type Hints (Required)”All Python code must use type hints throughout. Checked by mypy in strict mode.
# pyproject.toml (mypy config)[tool.mypy]strict = truepython_version = "3.11"DO:
from __future__ import annotationsfrom typing import Any
def load_tool(path: str) -> ToolDefinition: ...
async def execute(tool: ToolDefinition, params: dict[str, Any]) -> Any: ...DON’T:
def load_tool(path): # No type hints ...
def execute(tool, params): # No types ...Pydantic for Runtime Validation
Section titled “Pydantic for Runtime Validation”All tool definitions, configs, and external inputs must use Pydantic models.
from pydantic import BaseModel, Field
class Parameter(BaseModel): type: str required: bool = False description: str | None = None
# Pydantic validates on construction: never trust raw dictstool = ToolDefinition.model_validate(raw_yaml)Code Style (Ruff + Mypy)
Section titled “Code Style (Ruff + Mypy)”# From python/ workspace root:make lint # ruff checkmake lint-fix # ruff check --fixmake format # ruff formatmake typecheck # mypy strict (via packages/core)
# Or directly with uv:uv run ruff check packages/ scripts/uv run ruff format packages/ scripts/cd packages/core && uv run mypy src/Rules enforced:
- No
# type: ignorewithout a comment explaining why - No
Anywithout explicit justification - Snake_case for all functions, variables, modules
from __future__ import annotationsat the top of every file
Async-First
Section titled “Async-First”All I/O operations must be async def. Use asyncio.run() only at the top-level entry point.
# DO: async methodsasync def execute(self, tool_name: str, params: dict[str, Any]) -> Any: ...
# DO: asyncio.run() only at entryif __name__ == '__main__': asyncio.run(main())
# DON'T: Synchronous I/O in the call stackdef execute(self, tool_name: str, ...): # DO NOT block the event loop result = requests.get(url) # Blocking!Python Naming Conventions
Section titled “Python Naming Conventions”| Construct | Convention | Example |
|---|---|---|
| Files | snake_case.py |
tool_loader.py |
| Classes | PascalCase |
ToolLoader, MatimoError |
| Functions/methods | snake_case |
load_tool_from_file() |
| Variables | snake_case |
tool_registry |
| Constants | UPPER_SNAKE_CASE |
MAX_RETRIES = 3 |
| Private | _leading_underscore |
_resolve_path() |
Python Error Handling
Section titled “Python Error Handling”Use Structured Errors
Section titled “Use Structured Errors”from matimo.errors import MatimoError, ErrorCode
# Throw with contextraise MatimoError( "Tool execution failed", ErrorCode.EXECUTION_FAILED, {"tool_name": tool.name, "reason": "timeout"},)Catching Pattern
Section titled “Catching Pattern”try: result = await executor.execute(tool, params) return resultexcept MatimoError: raise # re-raise structured errors as-isexcept Exception as exc: raise MatimoError( f"Unexpected error executing '{tool.name}': {exc}", ErrorCode.EXECUTION_FAILED, {"tool_name": tool.name}, ) from excPython Testing Standards
Section titled “Python Testing Standards”Framework: pytest + pytest-asyncio
Section titled “Framework: pytest + pytest-asyncio”# From python/ workspace root (recommended):make test # full suitemake test-unit # unit tests onlymake test-integration # integration tests onlymake test-coverage # tests + HTML coverage report
# Or with uv directly:uv run pytest packages/core/tests/ -vuv run pytest packages/core/tests/ --cov=packages/core/src/matimo --cov-report=term-missingTest Structure
Section titled “Test Structure”import pytestfrom matimo.core.loader import ToolLoader
class TestToolLoader: def setup_method(self): self.loader = ToolLoader()
@pytest.mark.asyncio async def test_load_valid_yaml(self, tmp_path): # Arrange yaml_content = "name: calculator\n..." path = tmp_path / "definition.yaml" path.write_text(yaml_content)
# Act tool = self.loader.load_tool_from_file(str(path))
# Assert assert tool.name == "calculator"
def test_raises_on_missing_file(self): with pytest.raises(MatimoError) as exc_info: self.loader.load_tool_from_file("./nonexistent.yaml") assert exc_info.value.code == ErrorCode.FILE_NOT_FOUNDCoverage Targets
Section titled “Coverage Targets”| Metric | Minimum |
|---|---|
| Statements | 95% |
| Branches | 87% |
| Functions | 97% |
| Lines | 95% |
Python Development Workflow
Section titled “Python Development Workflow”The python/ workspace root has a Makefile that mirrors the TypeScript package.json scripts. Run all commands from python/.
Command Reference
Section titled “Command Reference”| Makefile | Equivalent to TS | What it does |
|---|---|---|
make install |
pnpm install |
uv sync --all-extras --dev |
make test |
pnpm test |
Run full pytest suite |
make test-unit |
n/a | Unit tests only |
make test-integration |
n/a | Integration tests only |
make test-coverage |
pnpm test:coverage |
Tests + HTML coverage report |
make lint |
pnpm lint |
Ruff check (read-only) |
make lint-fix |
pnpm lint:fix |
Ruff check –fix |
make format |
pnpm format |
Ruff format (write) |
make format-check |
n/a | Ruff format –check (CI) |
make typecheck |
pnpm build |
mypy strict on packages/core/src |
make validate-tools |
pnpm validate-tools |
Validate all YAML definitions |
make build |
pnpm build |
Build all provider packages |
make clean |
pnpm clean |
Remove __pycache__, dist, .coverage |
cd python
make install # first-time setupmake test # run all testsmake lint-fix # auto-fix lint issuesmake format # auto-format codemake typecheck # strict mypymake validate-tools # validate all YAMLsScripts directory
Section titled “Scripts directory”Standard utility scripts live in python/scripts/:
| Script | Purpose |
|---|---|
scripts/validate_tools.py |
YAML tool definition validator (invoked by make validate-tools) |
scripts/build_providers.py |
Builds provider packages (invoked by make build) |
Before Committing
Section titled “Before Committing”cd pythonmake format # Formatmake lint # Lintmake typecheck # Type-checkmake test # TestsPre-Merge Checklist (Python)
Section titled “Pre-Merge Checklist (Python)”- All tests passing (
make test) - No mypy errors (
make typecheck) - Ruff clean (
make lint && make format-check) - New code well covered (
make test-coverage; CI enforces 95% onmatimo-core; provider packages have no floor, so aim for full coverage of new tools) - Pydantic models validate all external inputs
- No hardcoded secrets
-
from __future__ import annotationsin every file
TypeScript Standards
Section titled “TypeScript Standards”Strict Mode (Required)
Section titled “Strict Mode (Required)”All code must compile in TypeScript strict mode.
// tsconfig.json{ "compilerOptions": { "strict": true, "noImplicitAny": true, "noImplicitThis": true, "strictNullChecks": true, "strictFunctionTypes": true, "strictBindCallApply": true, "strictPropertyInitialization": true, "noImplicitReturns": true }}Type Safety
Section titled “Type Safety”DO:
// Explicit types everywherefunction loadTool(path: string): ToolDefinition { // implementation}
// Use interface for contractsinterface ToolDefinition { name: string; execute(params: Record<string, unknown>): Promise<Result>;}
// Use union types for variantstype ExecutionType = 'http' | 'function' | 'command';
// Use const assertions for immutable dataconst EXECUTION_TYPES = ['http', 'function', 'command'] as const;DON’T:
// No implicit anyfunction loadTool(path) {}
// No any typesfunction execute(tool: any, params: any): any {}
// No untyped variableslet result;Export Types
Section titled “Export Types”Export types alongside implementations:
// DO: Export types with implementationexport interface ToolDefinition { name: string; // ...}
export class ToolLoader { loadToolFromFile(path: string): ToolDefinition {}}
// DON'T: Types only in comments// ToolDefinition = { name: string, ... }class ToolLoader {}Naming Conventions
Section titled “Naming Conventions”// kebab-case for filestool - loader.ts;command - executor.ts;error - codes.ts;matimo - error.ts;
// Descriptive names// ✓ command-executor.ts// ✗ executor.ts// ✗ cmd-exec.tsClasses & Types
Section titled “Classes & Types”// PascalCase for classesclass ToolLoader {}class CommandExecutor {}class MatimoError extends Error {}
// PascalCase for types/interfacesinterface ToolDefinition {}interface HttpExecution {}type ExecutionType = 'http' | 'function' | 'command';Functions & Variables
Section titled “Functions & Variables”// camelCase for functions and variablesfunction loadTool() {}const toolRegistry = new Map();let executionCount = 0;
// Descriptive names// ✓ loadToolFromFile()// ✓ validateToolParameters()// ✗ load()// ✗ validate()Constants
Section titled “Constants”// UPPER_SNAKE_CASE for constantsconst MAX_RETRIES = 3;const DEFAULT_TIMEOUT = 5000;const SUPPORTED_TYPES = ['http', 'function', 'command'];
// Constants should be immutableconst readonly EXECUTION_TYPES = ['command', 'http'] as const;Error Handling
Section titled “Error Handling”Use Structured Errors
Section titled “Use Structured Errors”// Define standard error codesexport enum ErrorCode { INVALID_SCHEMA = 'INVALID_SCHEMA', EXECUTION_FAILED = 'EXECUTION_FAILED', AUTH_FAILED = 'AUTH_FAILED', TOOL_NOT_FOUND = 'TOOL_NOT_FOUND', FILE_NOT_FOUND = 'FILE_NOT_FOUND', VALIDATION_FAILED = 'VALIDATION_FAILED',}
// Use structured error classexport class MatimoError extends Error { constructor( message: string, public code: ErrorCode, public context?: Record<string, unknown> ) { super(message); this.name = 'MatimoError'; }}
// Throw with contextthrow new MatimoError('Tool execution failed', ErrorCode.EXECUTION_FAILED, { toolName: tool.name, reason: 'timeout', duration: 30000,});Error Message Guidelines
Section titled “Error Message Guidelines”// DO: Clear, actionable messagesthrow new MatimoError( 'Tool validation failed: missing required parameter "repo"', ErrorCode.VALIDATION_FAILED);
// DO: Include contextthrow new MatimoError('HTTP request failed', ErrorCode.EXECUTION_FAILED, { status: 500, endpoint: '/api/issues',});
// DON'T: Generic errorsthrow new Error('Something went wrong');
// DON'T: Include sensitive datathrow new Error(`Failed with token: ${apiKey}`);Matimo’s own built-in HTTP executor already does this HTTP-error →
MatimoErrorwrapping for you viafromHttpError()/from_http_error(), and doesn’t collapse every status toEXECUTION_FAILED, it maps 401/403 →AUTH_FAILED, 429 →RATE_LIMIT_EXCEEDED, and records aretryableflag ondetails. Reach for the pattern above only when writing a customtype: functionexecutor that talks to something other than a standard HTTP tool. See the Error Codes Reference.
Error Handling Pattern
Section titled “Error Handling Pattern”try { const result = await executor.execute(tool, params); return result;} catch (error) { if (error instanceof MatimoError) { logger.error('Execution failed', { code: error.code, message: error.message, context: error.context, }); throw error; }
// Convert unknown errors throw new MatimoError('Unknown error during execution', ErrorCode.EXECUTION_FAILED, { originalError: error.message, });}Testing Standards
Section titled “Testing Standards”Test File Organization
Section titled “Test File Organization”describe('ToolLoader', () => { describe('loadToolFromFile', () => { it('should load valid YAML tool definition', () => { // Arrange const filePath = './fixtures/calculator.yaml';
// Act const tool = loader.loadToolFromFile(filePath);
// Assert expect(tool.name).toBe('calculator'); expect(tool.parameters).toBeDefined(); });
it('should throw FileNotFoundError when file does not exist', () => { // Arrange const filePath = './fixtures/nonexistent.yaml';
// Act & Assert expect(() => { loader.loadToolFromFile(filePath); }).toThrow(FileNotFoundError); }); });});Test Naming
Section titled “Test Naming”// DO: Descriptive test namesit('should load valid YAML tool definition');it('should throw FileNotFoundError when file does not exist');it('should validate parameters against schema');it('should handle missing required parameters');it('should retry on temporary failure');
// DON'T: Vague test namesit('should work');it('tests loading');it('handles errors');Test Coverage
Section titled “Test Coverage”Minimum targets (enforced by jest.config.cjs):
- Statements: 95%+
- Branches: 87%+
- Functions: 97%+
- Lines: 95%+
# Check coveragepnpm test:coverage
# Expected:# Statements : 95%+# Branches : 87%+# Functions : 97%+# Lines : 95%+Mocking & Fixtures
Section titled “Mocking & Fixtures”// DO: Use fixtures for test dataconst validTool = loadYaml('./fixtures/calculator.yaml');
// DO: Mock external dependenciesconst mockAPI = jest.spyOn(axios, 'get').mockResolvedValue({ data: { id: 1 } });
// DO: Clean up mocksafterEach(() => { jest.clearAllMocks();});
// DO: Use AAA patternit('should execute tool', () => { // Arrange const tool = loadTool('calculator');
// Act const result = executor.execute(tool, { a: 1, b: 2 });
// Assert expect(result.output.result).toBe(3);});Documentation Standards
Section titled “Documentation Standards”JSDoc Comments
Section titled “JSDoc Comments”// DO: JSDoc for all public APIs/** * Load a tool definition from a YAML/JSON file. * * @param path - Path to tool definition file * @returns Loaded and validated tool definition * @throws {FileNotFoundError} If file doesn't exist * @throws {SchemaValidationError} If tool schema invalid * * @example * const tool = loader.loadToolFromFile('./tools/calculator.yaml'); */export function loadToolFromFile(path: string): ToolDefinition { // implementation}
// DON'T: Missing or vague documentationfunction load(p) {}
// DON'T: Obvious comments// Increment ii++;Code Comments
Section titled “Code Comments”Comments should explain WHY, not WHAT.
// DO: Explain why// We retry exponentially because the API has rate limits// and temporary network issues are commonconst delay = initialDelay * Math.pow(2, retries);
// DO: Document non-obvious logic// Skip first item because it's always metadatafor (let i = 1; i < items.length; i++) { // process item}
// DON'T: Describe what the code does// Calculate exponential backoffconst delay = initialDelay * Math.pow(2, retries);
// DON'T: Obvious comments// Get the first itemconst first = items[0];README Structure
Section titled “README Structure”# Tool Name
Brief description (1 sentence)
## Features
- Feature 1- Feature 2
## Installation
Step-by-step setup instructions
## Usage
Code examples
## API Reference
Methods and options
## Troubleshooting
Common issues and solutionsSecurity Standards
Section titled “Security Standards”Input Validation
Section titled “Input Validation”// DO: Validate all inputsconst validated = toolSchema.parameters.parse(params);const result = await executor.execute(tool, validated);
// DO: Use Zod for schema validationimport { z } from 'zod';
const paramSchema = z.object({ repo: z.string().regex(/^[^/]+\/[^/]+$/), issue: z.number().min(1)});
const validated = paramSchema.parse(params);
// DON'T: Trust user inputconst command = `git clone ${userUrl}`; // Dangerous!
### Secret Management
```typescript// DO: Use environment variables with MATIMO_ prefixconst apiKey = process.env.MATIMO_SLACK_API_KEY;if (!apiKey) { throw new MatimoError('Missing Slack API key', ErrorCode.AUTH_FAILED);}
// DO: Validate secrets exist before useconst token = process.env.MATIMO_GITHUB_TOKEN;if (!token || token.length === 0) { throw new MatimoError('Invalid GitHub token', ErrorCode.AUTH_FAILED);}
// DON'T: Hardcode secretsconst API_KEY = 'sk_live_abc123xyz789'; // NEVER!
// DON'T: Log secretslogger.info('Token retrieved:', apiKey); // WRONG!logger.info('Token retrieved'); // OKOutput Escaping
Section titled “Output Escaping”// DO: Escape shell commandsimport { shellEscape } from 'shell-escape';const escaped = shellEscape([command, ...args]);
// DO: Sanitize error messagesconst sanitized = error.message.replace(apiKey, '[REDACTED]');
// DON'T: Include secrets in error messagesthrow new Error(`Failed with key: ${apiKey}`);Logging Standards
Section titled “Logging Standards”Structured Logging
Section titled “Structured Logging”// DO: Use structured logginglogger.info('tool_execution', { traceId: context.traceId, toolName: tool.name, parameters: sanitized(params), duration: executionTime, status: 'success' | 'failed',});
// DO: Include trace IDslogger.error('execution_failed', { traceId: context.traceId, toolName: tool.name, error: error.message,});
// DON'T: Unstructured loggingconsole.log('Tool loaded');logger.info('tool loaded: ' + toolName);
// DON'T: Log secretslogger.info('Token: ' + apiKey);Log Levels
Section titled “Log Levels”logger.debug('Parsing tool definition'); // Detailed infologger.info('Tool loaded successfully'); // Informationallogger.warn('Tool schema drift detected'); // Warninglogger.error('Tool execution failed', error); // Error
// DON'T: Use console.log in productionconsole.log('Debug info');Performance Standards
Section titled “Performance Standards”Execution Time Targets
Section titled “Execution Time Targets”Simple tools (echo, time): <100msAPI tools (GitHub, Slack): <2 secondsData processing (CSV, JSON): <1 secondHeavy computation: <10 secondsMemory Targets
Section titled “Memory Targets”Tool loading: <10MBTool execution: <50MBRegistry loading: <20MBThroughput Targets
Section titled “Throughput Targets”Tools loaded: 1000+ in <1 secondConcurrent executions: 100+Requests/second: 1000+Quality Metrics
Section titled “Quality Metrics”Code Quality Checklist
Section titled “Code Quality Checklist”// Types- Strict TypeScript mode enabled- No `any` types used- All functions have type signatures- Return types explicitly declared
// Testing- 95% minimum test coverage on `@matimo/core` and `matimo-core`, enforced in CI- Unit tests for all modules- Integration tests for critical paths- Edge cases tested
// Documentation- JSDoc comments on all public APIs- README with examples- Code comments explaining WHY- Type definitions exported
// Error Handling- All errors use MatimoError- Errors include codes and context- No generic Error throws- Error messages are clear
// Security- No hardcoded secrets- All inputs validated- Sensitive data never logged- Shell commands properly escaped
// Performance- Tool loading <10MB- Execution meets time targets- No memory leaks- Concurrent operations supportedBuild Verification
Section titled “Build Verification”# Check TypeScriptpnpm build# Expected: No errors
# Check lintingpnpm lint# Expected: 0 errors, 0 warnings
# Check testspnpm test# Expected: All tests passing
# Check coveragepnpm test:coverage# Expected: 95%+ coverageDevelopment Workflow
Section titled “Development Workflow”Before Committing
Section titled “Before Committing”- Format code:
pnpm format - Check types:
pnpm build - Lint:
pnpm lint - Test:
pnpm test - Coverage:
pnpm test:coverage(verify 95%+)
Pre-Merge Checklist
Section titled “Pre-Merge Checklist”- All tests passing
- Coverage 95%+
- No TypeScript errors
- No ESLint warnings
- Code formatted with Prettier
- Commit messages follow guidelines
- Documentation updated
- PR reviewed and approved
Common Anti-Patterns
Section titled “Common Anti-Patterns”// DON'T: Use any typesfunction execute(tool: any, params: any): any { }
// DO: Use proper typesfunction execute( tool: ToolDefinition, params: Record<string, unknown>): Promise<ExecutionResult> { }
// DON'T: Generic error handlingcatch (error) { throw new Error('Failed');}
// DO: Structured error handlingcatch (error) { throw new MatimoError( 'Tool execution failed', ErrorCode.EXECUTION_FAILED, { toolName, reason: error.message } );}
// DON'T: Log secretslogger.info('Authenticated with key:', apiKey);
// DO: Redact sensitive datalogger.info('Authentication successful', { hasKey: !!apiKey });
// DON'T: Skip validationconst result = userInput.trim();
// DO: Validate all inputsconst validated = schema.parse(userInput);See Also
Section titled “See Also”- CONTRIBUTING.md: Contribution guide
- COMMIT_GUIDELINES.md: Commit standards
- QUICK_START.md: Get started
- API_REFERENCE.md: SDK documentation