Contributing Guide
How to contribute to Spry and help improve the project
Contributing Guide
Welcome to Spry! We're excited that you want to contribute. This guide covers code standards, testing practices, and contribution workflows.
Before You Start
- Browse existing issues and pull requests
- Check issues labeled
good first issueif you're new to the project - Review the Development Setup guide
Security Issues
Email maintainers directly instead of creating public issues for security vulnerabilities.
Types of Contributions
Bug Reports
Report issues with clear reproduction steps, expected vs actual behavior, and environment details.
Feature Requests
Propose features with use cases, examples, and considered alternatives.
Code
Submit bug fixes, features, performance improvements, or tests.
Documentation
Improve guides, API docs, examples, or fix clarity issues.
Code Standards
TypeScript Style
Naming Conventions
// Files: kebab-case
// contained-in-section.ts, code-frontmatter.ts
// Types/Interfaces: PascalCase
interface GraphEdge { ... }
type ExecutableTask = ...
// Functions/Variables: camelCase
function buildGraph() { ... }
const taskRunner = ...
// Constants: UPPER_SNAKE_CASE or camelCase
const DEFAULT_TIMEOUT = 120;
const typicalRules = [...];Type Annotations
// Explicit return types for public functions
function parseCell(code: Code): ParsedCell {
// ...
}
// Infer types for local variables
const result = parseCell(node);
// Use unknown instead of any
function processUnknown(value: unknown): void {
if (typeof value === "string") {
// Type narrowing
}
}Import Organization
// 1. Standard library
import { join } from "std/path/mod.ts";
// 2. External dependencies
import { unified } from "unified";
import { visit } from "unist-util-visit";
// 3. Internal absolute imports
import { graph } from "@spry/axiom/graph.ts";
// 4. Relative imports
import { helper } from "./helper.ts";
import type { MyType } from "./types.ts";Module Structure
// 1. Imports
import { ... } from "...";
// 2. Types/Interfaces
export interface MyInterface { ... }
export type MyType = ...;
// 3. Constants
const DEFAULT_VALUE = 42;
// 4. Helper functions (private)
function helperFunction() { ... }
// 5. Main exports (public)
export function mainFunction() { ... }
export class MainClass { ... }Function Design
Keep functions small and focused:
// Good: Small, focused functions
function validateTask(task: Task): ValidationResult {
const errors = [];
if (!task.taskId()) errors.push("Missing task ID");
if (task.taskDeps()?.some(d => !isValidId(d))) {
errors.push("Invalid dependency");
}
return { valid: errors.length === 0, errors };
}Guidelines:
- Keep files under 300 lines when possible
- One primary export per file
- Split large functions into testable units
Error Handling
Result Types for Expected Failures
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
function parseConfig(content: string): Result<Config, ParseError> {
try {
const parsed = JSON.parse(content);
return { ok: true, value: parsed as Config };
} catch (e) {
return { ok: false, error: new ParseError(e.message) };
}
}Exceptions for Unexpected Errors
// Programming errors
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
// Input validation at boundaries
function processFile(path: string): void {
if (!path) throw new Error("Path is required");
// ...
}Testing Guidelines
Test Structure
Test files use *_test.ts suffix adjacent to source files:
import { assertEquals, assertThrows } from "@std/assert";
Deno.test("executionPlan creates valid topological order", () => {
// Arrange
const tasks = createTestTasks();
// Act
const plan = executionPlan(tasks);
// Assert
assertEquals(plan.layers.length, 3);
assertEquals(plan.dag.length, tasks.length);
});
Deno.test("executionPlan throws on circular dependencies", () => {
const circularTasks = createCircularTasks();
assertThrows(
() => executionPlan(circularTasks),
Error,
"Circular dependency detected"
);
});Test Coverage
Cover these scenarios:
Happy Path
Main use case with valid inputs
Edge Cases
Boundary conditions, empty inputs
Error Conditions
Invalid inputs and failure modes
Integration
Components working together
Group Related Tests
Deno.test("TaskExecutor", async (t) => {
await t.step("executes single task", () => {
// Test implementation
});
await t.step("handles dependencies", () => {
// Test implementation
});
await t.step("reports failures", () => {
// Test implementation
});
});Run Tests
# Run all tests
deno test --parallel --allow-all
# Format code
deno fmt
# Lint code
deno lintDocumentation
JSDoc Comments
Document all public APIs:
/**
* Builds a semantic graph from an MDAST root.
*
* @param root - The MDAST root node
* @param rules - Edge rules to apply
* @returns A graph with discovered relationships
*
* @example
* ```ts
* const g = graph(root, typicalRules());
* console.log(g.edges.length);
* ```
*/
export function graph(
root: Root,
rules: EdgeRule[]
): Graph {
// ...
}When to document:
- All public exports
- Complex algorithms
- Non-obvious behavior
- API boundaries
When NOT to document:
- Self-explanatory code
- Internal implementation details
- Obvious getters/setters
Performance
Prefer Generators for Large Collections
// Good: Lazy evaluation
function* findMatches(nodes: Node[]): Generator<Node> {
for (const node of nodes) {
if (matches(node)) yield node;
}
}
// Use when needed
for (const match of findMatches(largeArray)) {
// Process one at a time
}Avoid Unnecessary Allocations
// Better: Lazy evaluate when appropriate
function* getIds(tasks: Task[]): Generator<string> {
for (const task of tasks) {
yield task.taskId();
}
}Commit Message Format
Follow conventional commits:
<type>(<scope>): <description>
[optional body]
[optional footer]Commit Types
| Type | Description |
|---|---|
feat | New feature |
fix | Bug fix |
docs | Documentation changes |
test | Adding/updating tests |
refactor | Code refactoring |
perf | Performance improvements |
chore | Maintenance tasks |
ci | CI/CD configuration |
Commit Scopes
| Scope | Description |
|---|---|
axiom | Graph engine and core abstractions |
runbook | Runbook execution and orchestration |
sqlpage | SQLPage playbook integration |
task | Task execution and planning |
shell | Shell command execution |
courier | Data movement and transformation |
Examples
feat(axiom): add support for custom edge rules
Add ability to register custom edge rules for graph building.
This enables extensibility for domain-specific relationships.
Closes #123fix(runbook): handle missing task dependencies gracefully
Previously, missing dependencies caused a crash. Now they are
reported as warnings and skipped.
Fixes #456feat(task)!: change task execution API signature
BREAKING CHANGE: The executeTask function now requires
an options object instead of individual parameters.
Migration guide available in docs/migration.mdPull Request Guidelines
PR Title Format
Use the same format as commit messages:
feat(axiom): add custom edge rule support
fix(runbook): resolve dependency cycle detection
docs: improve contributing guide clarityPR Description Template
## What does this change?
Brief description of the changes
## Why is this needed?
Explain the motivation and use case
## How was it tested?
- [ ] Unit tests added/updated
- [ ] Manual testing performed
- [ ] Documentation updated
## Breaking changes?
None / List any breaking changes
## Related issues
Closes #123Review Process
Automated Checks
All tests, linting, and formatting must pass.
Code Review
Maintainer reviews and provides feedback.
Address Feedback
Make requested changes.
Merge
PR will be squashed and merged once approved.
File Organization
lib/axiom/projection/
├── flexible.ts # Main implementation
├── flexible_test.ts # Tests
├── playbook.ts
├── playbook_test.ts
└── fixture/ # Test fixtures
├── sample.md
└── complex.mdRules:
- One main export per file when possible
- Tests adjacent to source files (
*_test.ts) - Fixtures in
fixture/subdirectories - Index files (
mod.ts) for module exports
Getting Help
GitHub Issues
Check existing issues or create new ones.
Discussions
Ask questions in GitHub Discussions.
Documentation
Browse the documentation for guides and API reference.
Code of Conduct
All contributors are expected to:
- Be respectful and considerate
- Welcome newcomers
- Give and receive constructive feedback gracefully
- Focus on what's best for the community
- Show empathy towards others
License
By contributing to Spry, you agree that your contributions will be licensed under the project's MIT License.
Thank You
Every contribution, no matter how small, helps make Spry better for everyone. We appreciate your time and effort!
How is this guide?
Last updated on