Spry LogoSpry Docs

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

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

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 lint

Documentation

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

TypeDescription
featNew feature
fixBug fix
docsDocumentation changes
testAdding/updating tests
refactorCode refactoring
perfPerformance improvements
choreMaintenance tasks
ciCI/CD configuration

Commit Scopes

ScopeDescription
axiomGraph engine and core abstractions
runbookRunbook execution and orchestration
sqlpageSQLPage playbook integration
taskTask execution and planning
shellShell command execution
courierData 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 #123
fix(runbook): handle missing task dependencies gracefully

Previously, missing dependencies caused a crash. Now they are
reported as warnings and skipped.

Fixes #456
feat(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.md

Pull 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 clarity

PR 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 #123

Review 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.md

Rules:

  • 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

On this page