Spry LogoOpsfolio
Contributing and Support

Contributing Guide

How to contribute to Spry and help improve the project

TypeScript Style

General Principles

  • Use TypeScript's type system fully
  • Prefer immutability
  • Write self-documenting code
  • Keep functions small and focused

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

// Prefer explicit return types for public functions
function parseCell(code: Code): ParsedCell {
  // ...
}

// Use type inference for local variables
const result = parseCell(node);  // Type is inferred

// Avoid `any` - use `unknown` if type is truly unknown
function processUnknown(value: unknown): void {
  if (typeof value === "string") {
    // Now TypeScript knows it's a string
  }
}

Avoid using any type. Use unknown when the type is truly unknown and perform type guards.

Import Organization

Standard library imports

import { join } from "std/path/mod.ts";

External dependencies

import { unified } from "unified";
import { visit } from "unist-util-visit";

Internal absolute imports

import { graph } from "@spry/axiom/graph.ts";

Relative imports

import { helper } from "./helper.ts";
import type { MyType } from "./types.ts";

Code Organization

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 { ... }

File Size

  • Keep files under 300 lines when possible
  • Split large files into focused modules
  • One primary export per file

Function Design

// 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 };
}
// Avoid: Large functions doing multiple things
// Split into smaller, testable functions

Error Handling

Use Result Types

// Prefer Result types over exceptions 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) };
  }
}

Prefer Result types over exceptions for expected failures to make error handling explicit.

Exceptions for Unexpected Errors

// Use exceptions for programming errors
function assertNever(value: never): never {
  throw new Error(`Unexpected value: ${value}`);
}

// Validate inputs at boundaries
function processFile(path: string): void {
  if (!path) throw new Error("Path is required");
  // ...
}

Testing Patterns

Test File Naming

module.ts       → module_test.ts
graph.ts        → graph_test.ts

Test Structure

import { assertEquals, assertThrows } from "std/assert/mod.ts";
import { describe, it } from "std/testing/bdd.ts";

describe("MyModule", () => {
  describe("myFunction", () => {
    it("should handle normal input", () => {
      const result = myFunction("input");
      assertEquals(result, "expected");
    });

    it("should throw on invalid input", () => {
      assertThrows(
        () => myFunction(""),
        Error,
        "Input required"
      );
    });
  });
});

Documentation

JSDoc Comments

/**
 * 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

// Avoid: Creates new array each call
function getIds(tasks: Task[]): string[] {
  return tasks.map(t => t.taskId());
}
// Better: Reuse or lazy evaluate if appropriate
function* getIds(tasks: Task[]): Generator<string> {
  for (const task of tasks) {
    yield task.taskId();
  }
}

Formatting

Use Deno's built-in formatter:

deno fmt

Key settings (in deno.jsonc):

  • Line width: 80
  • Indent: 2 spaces
  • Single quotes for strings
  • Trailing commas

Linting

Use Deno's linter:

deno lint

Address all warnings before committing.

Commit Messages

Follow conventional commits:

type(scope): subject

body (optional)

footer (optional)

Types

TypeDescription
featNew feature
fixBug fix
docsDocumentation
refactorCode change (no new feature or fix)
testAdding tests
choreMaintenance

Examples

feat(axiom): add support for nested dependencies
fix(cli): handle missing file gracefully
docs: update installation instructions
refactor(edge): simplify rule pipeline

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